import { type Dispatch, type MutableRefObject, type SetStateAction, useCallback, useMemo, } from 'react'; import type { ExternalGenerationJobStatusRecord } from '../../../packages/shared/src/contracts/externalGeneration'; import { getExternalGenerationJobStatus } from '../../services/external-generation'; import { resolveEditorImageReferenceDataUrl } from '../../services/image-editor/editorImageReference'; import { type EditorMediaAssetUploadType, uploadEditorMediaAssetFile, } from '../../services/image-editor/editorMediaAssetUploadClient'; import type { EditorAssetSnapshot, EditorCanvasGenerationCompletionInput, EditorProjectSnapshot, } from '../../services/image-editor/editorProjectClient'; import { editEditorImage, extractEditorUiDesignAssets, generateEditorBackgroundMusic, generateEditorCharacterAnimation, generateEditorIconSpritesheet, generateEditorImage, generateEditorSoundEffect, generateEditorVideo, loadEditorProject, } from '../../services/image-editor/editorProjectClient'; import type { CanvasGenerationDialogState, CanvasGenerationInputs, CanvasHistoryAction, CanvasLayer, CanvasTool, CanvasViewport, CharacterAnimationPanelState, CharacterReferenceImage, GenerateDialogState, QuickEditPanelState, SidebarPanel, } from './ImageCanvasEditorTypes'; import { createQuickEditGenerationDialogDraft } from './ImageCanvasGenerationDialogModel'; import { applyImageEditResultToSourceLayer, createAudioResultLayer, createCharacterAnimationResultLayer, createGeneratedResultLayer, createIconSpritesheetResultLayers, createQuickEditResultLayer, createVideoResultLayer, } from './ImageCanvasGenerationLayerModel'; import { buildCharacterAnimationGenerationInputs, buildEditGenerationInputs, buildQuickEditGenerationInputs, buildUiDesignAssetExtractionGenerationInputs, buildVideoGenerationInputs, DEFAULT_EDITOR_BGFILTER_SEG_MODEL, DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR, DEFAULT_IMAGE_MODEL, DEFAULT_VIDEO_MODEL, inferEditorImageAspectRatio, inferEditorImageSizeLabel, isCanvasGenerationDialog, isQuickEditUnsupportedAssetKind, normalizeEditorImageModel, resolveEditorImageGenerationPixelSize, resolveImageGenerationErrorMessage, } from './ImageCanvasGenerationModel'; import { chooseGenerationPlacement } from './ImageCanvasGenerationPlacementModel'; import { buildCharacterAnimationSubmissionPlan, buildIconSpritesheetGenerationSubmissionPlan, buildImageGenerationSubmissionPlan, resolveGenerationAssetLabel, } from './ImageCanvasGenerationSubmissionModel'; import type { UiAssetExtractionMark, UiAssetExtractionState, } from './ImageCanvasUiAssetExtractionModel'; import { resolveUiAssetExtractionGenerationPlan } from './ImageCanvasUiAssetExtractionModel'; import { renderUiDesignAssetExtractionMarkedImage } from './ImageCanvasUiAssetExtractionRasterModel'; type CanvasSize = { width: number; height: number }; const EDITOR_SPRITESHEET_SLICE_WARNING_PREFIX = '图集已生成,但自动拆分未完成:'; type CanvasGenerationDialogUpdater = ( dialog: CanvasGenerationDialogState, ) => CanvasGenerationDialogState | null; type UiDesignAssetExtractionOptions = { marks?: UiAssetExtractionMark[]; model?: string; references?: CharacterReferenceImage[]; suppressAlert?: boolean; }; type EditorGenerationMediaReference = { src: string; objectKey?: string | null; resourceId?: string | null; sourceAssetId?: string | null; }; type EditorGenerationMediaReferenceOptions = { allowRegisteredIds?: boolean; requireImageObjectReference?: boolean; }; let editorGenerationUploadFallbackCounter = 0; export function createEditorGenerationMediaUploadId() { const cryptoApi = globalThis.crypto; if (typeof cryptoApi?.randomUUID === 'function') { return cryptoApi.randomUUID(); } if (typeof cryptoApi?.getRandomValues === 'function') { const words = cryptoApi.getRandomValues(new Uint32Array(4)); return Array.from(words, (word) => word.toString(16).padStart(8, '0')).join( '', ); } editorGenerationUploadFallbackCounter += 1; return `${Date.now().toString(36)}-${editorGenerationUploadFallbackCounter.toString(36)}-${Math.random().toString(36).slice(2)}`; } function resolveEditorGenerationMediaReferenceSource( reference: EditorGenerationMediaReference, allowRegisteredIds: boolean, ) { const resourceId = reference.resourceId?.trim(); return ( reference.objectKey?.trim() || (allowRegisteredIds && resourceId && !resourceId.startsWith('local-resource-') && !resourceId.startsWith('generation-dialog:') ? resourceId : '') || (allowRegisteredIds ? reference.sourceAssetId?.trim() : '') || reference.src.trim() ); } function dataUrlToEditorGenerationFile(dataUrl: string, fileName: string) { const [header = '', payload = ''] = dataUrl.split(','); const mimeMatch = /^data:([^;]+)(;base64)?$/iu.exec(header); if (!mimeMatch) { throw new Error('生成参考图不是有效的 Data URL'); } const type = mimeMatch[1] ?? 'image/png'; const binary = mimeMatch[2] ? atob(payload) : decodeURIComponent(payload); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) { bytes[index] = binary.charCodeAt(index); } return new File([bytes], fileName, { type }); } async function inlineMediaSourceToEditorGenerationFile( source: string, mediaType: EditorMediaAssetUploadType, uploadId: string, ) { const fileName = `generation-reference-${uploadId}.${ mediaType === 'video' ? 'mp4' : mediaType === 'audio' ? 'mp3' : 'png' }`; if (/^data:/iu.test(source)) { return dataUrlToEditorGenerationFile(source, fileName); } const response = await fetch(source); if (!response.ok) { throw new Error('读取本地生成参考素材失败'); } const blob = await response.blob(); return new File([blob], fileName, { type: blob.type || `${mediaType}/*`, }); } async function uploadEditorGenerationInlineMediaSource( source: string, mediaType: EditorMediaAssetUploadType, projectId?: string | null, ) { const normalizedProjectId = projectId?.trim() || 'unscoped'; const uploadId = createEditorGenerationMediaUploadId(); const uploaded = await uploadEditorMediaAssetFile( await inlineMediaSourceToEditorGenerationFile(source, mediaType, uploadId), mediaType, { assetKind: `editor_generation_reference_${mediaType}`, pathSegments: [ 'editor', 'generation-references', normalizedProjectId, uploadId, ], entityId: normalizedProjectId, ...(projectId?.trim() ? { metadata: { editor_project_id: projectId.trim() } } : {}), }, ); return uploaded.objectKey; } export async function resolveEditorGenerationMediaReference( reference: EditorGenerationMediaReference, mediaType: EditorMediaAssetUploadType, projectId?: string | null, options: EditorGenerationMediaReferenceOptions = {}, ) { const resourceId = reference.resourceId?.trim(); const hasRegisteredReference = options.allowRegisteredIds !== false && ((resourceId && !resourceId.startsWith('local-resource-') && !resourceId.startsWith('generation-dialog:')) || reference.sourceAssetId?.trim()); const source = resolveEditorGenerationMediaReferenceSource( reference, options.allowRegisteredIds !== false, ); const inlineSource = /^(?:data|blob):/iu.test(source); const imageSourceRequiresUpload = options.requireImageObjectReference !== false && mediaType === 'image' && !reference.objectKey?.trim() && !hasRegisteredReference; if (!inlineSource && !imageSourceRequiresUpload) { return source; } const uploadSource = imageSourceRequiresUpload && !inlineSource ? await resolveEditorImageReferenceDataUrl(source) : source; return uploadEditorGenerationInlineMediaSource( uploadSource, mediaType, projectId, ); } async function uploadEditorGenerationImageDataUrl( dataUrl: string, projectId?: string | null, ) { return uploadEditorGenerationInlineMediaSource(dataUrl, 'image', projectId); } const EDITOR_GENERATION_QUEUE_POLL_INTERVAL_MS = 1600; const EDITOR_GENERATION_QUEUE_TIMEOUT_MS = 20 * 60 * 1000; const EDITOR_GENERATION_QUEUE_PROJECT_REFRESH_RETRY_MS = 650; type GenerationSubmissionWorkflowOptions = { layers: CanvasLayer[]; canvasSize: CanvasSize; viewport: CanvasViewport; layerCounterRef: MutableRefObject; quickEditPanel: QuickEditPanelState | null; quickEditSourceLayer: CanvasLayer | null; quickEditSelectionState: UiAssetExtractionState | null; canvasGenerationDialogs: CanvasGenerationDialogState[]; setQuickEditPanel: Dispatch>; characterAnimationPanel: CharacterAnimationPanelState | null; characterAnimationDialog: CanvasGenerationDialogState | null; characterAnimationSourceLayer: CanvasLayer | null; setCharacterAnimationPanel: Dispatch< SetStateAction >; setGenerateDialog: Dispatch>; openCanvasGenerationDialog: ( dialog: Omit, ) => string; updateCanvasGenerationDialogById: ( dialogId: string, updater: CanvasGenerationDialogUpdater, ) => void; hasCanvasGenerationDialogById: (dialogId: string) => boolean; getGeneratingDialogPlaceholder: ( dialog: GenerateDialogState, ) => GenerateDialogState['placeholder']; appendCanvasLayersWithResources: (nextLayers: CanvasLayer[]) => void; captureCanvasHistory: (action: CanvasHistoryAction) => void; updateSourceLayer: ( sourceLayerId: string, updater: (layer: CanvasLayer) => CanvasLayer, options?: { fit?: boolean; persist?: boolean }, ) => void; selectSingleLayer: (layerId: string | null) => void; fitLayers: ( targetLayers?: CanvasLayer[], options?: { captureHistory?: boolean }, ) => void; setActiveTool: Dispatch>; setActiveSidebarPanel: Dispatch>; rememberImageModel: (imageModel: string) => void; projectId?: string | null; assetFolderId?: string | null; upsertGeneratedAsset?: (asset: EditorAssetSnapshot) => void; applyProjectSnapshot?: (project: EditorProjectSnapshot) => void; onQueuedGenerationTask?: () => void; onWalletBalanceMayHaveChanged?: () => void; onGenerationWarning?: (message: string) => void; }; async function normalizeImageGenerationReferenceImages( input: Parameters[0], references: CharacterReferenceImage[], projectId?: string | null, ) { if (!input.referenceImageSrcs?.length) { return input; } return { ...input, referenceImageSrcs: await Promise.all( input.referenceImageSrcs.map((referenceImageSrc, index) => resolveEditorGenerationMediaReference( references[index] ? { ...references[index], src: referenceImageSrc } : { src: referenceImageSrc }, 'image', projectId, ), ), ), }; } function resolveImageGenerationDialogReferences( dialog: GenerateDialogState, ): CharacterReferenceImage[] { if (dialog.mode === 'spec') { return dialog.specReference ? [dialog.specReference] : []; } if (dialog.mode === 'character') { return [ ...(dialog.characterSpecReference ? [dialog.characterSpecReference] : []), ...(dialog.characterReferences ?? []), ]; } if (dialog.mode === 'ui-design') { return [ ...(dialog.uiDesignSpecReference ? [dialog.uiDesignSpecReference] : []), ...(dialog.generationReferences ?? []), ]; } if (dialog.mode === 'publication') { return dialog.publicationReferences ?? []; } return dialog.generationReferences ?? []; } async function normalizeVideoGenerationReferences( input: Parameters[0], references: CharacterReferenceImage[], projectId?: string | null, ) { const normalizeSources = async ( sources: string[] | undefined, mediaType: EditorMediaAssetUploadType, ) => { if (!sources?.length) { return undefined; } const matchingReferences = references.filter( (reference) => (reference.mediaType ?? 'image') === (mediaType === 'image' ? 'image' : mediaType), ); return Promise.all( sources.map((source, index) => resolveEditorGenerationMediaReference( matchingReferences[index] ? { ...matchingReferences[index], src: source } : { src: source }, mediaType, projectId, { allowRegisteredIds: false }, ), ), ); }; const [referenceImageSrcs, referenceVideoSrcs, referenceAudioSrcs] = await Promise.all([ normalizeSources(input.referenceImageSrcs, 'image'), normalizeSources(input.referenceVideoSrcs, 'video'), normalizeSources(input.referenceAudioSrcs, 'audio'), ]); return { ...input, ...(referenceImageSrcs ? { referenceImageSrcs } : {}), ...(referenceVideoSrcs ? { referenceVideoSrcs } : {}), ...(referenceAudioSrcs ? { referenceAudioSrcs } : {}), }; } function delay(ms: number) { return new Promise((resolve) => window.setTimeout(resolve, ms)); } function queuedStateFromResponse( response: { queueState?: ExternalGenerationJobStatusRecord | null } | null, ) { return response?.queueState ?? null; } function projectHasUnresolvedGenerationDialog( project: EditorProjectSnapshot, dialogId: string | null | undefined, ) { const normalizedDialogId = dialogId?.trim(); if (!normalizedDialogId) { return false; } return project.layers.some((item) => { if (item.itemType !== 'generation-dialog') { return false; } const dialog = item.dialog && typeof item.dialog === 'object' ? (item.dialog as Record) : null; return ( dialog?.id === normalizedDialogId && (dialog.status === 'generating' || typeof dialog.generatedLayerId !== 'string' || dialog.generatedLayerId.trim() === '') ); }); } function notifyWalletBalanceMayHaveChanged(callback?: () => void) { callback?.(); } function notifyEditorGenerationWarning( warning: string | null | undefined, callback?: (message: string) => void, ) { const reason = warning?.trim(); if (!reason) { return; } callback?.(reason); } function resolveEditorGenerationWarningMessage( warning: string | null | undefined, sliceWarning: string | null | undefined, ) { const commonReason = warning?.trim(); if (commonReason) { return commonReason; } const sliceReason = sliceWarning?.trim(); return sliceReason ? `${EDITOR_SPRITESHEET_SLICE_WARNING_PREFIX}${sliceReason}` : undefined; } async function runEditorGenerationWithWalletRefresh( operation: Promise, onWalletBalanceMayHaveChanged?: () => void, ): Promise { try { const response = await operation; if ( !queuedStateFromResponse( response as { queueState?: ExternalGenerationJobStatusRecord | null }, ) ) { notifyWalletBalanceMayHaveChanged(onWalletBalanceMayHaveChanged); } return response; } catch (error) { notifyWalletBalanceMayHaveChanged(onWalletBalanceMayHaveChanged); throw error; } } async function waitForEditorGenerationQueue( queueState: ExternalGenerationJobStatusRecord, ): Promise { const startedAt = Date.now(); let current = queueState; while (current.status === 'queued' || current.status === 'running') { if (Date.now() - startedAt > EDITOR_GENERATION_QUEUE_TIMEOUT_MS) { return null; } await delay(EDITOR_GENERATION_QUEUE_POLL_INTERVAL_MS); current = (await getExternalGenerationJobStatus(current.operationId)).job; } if (current.status === 'failed') { throw new Error(current.error?.trim() || '生成任务失败'); } return current; } export async function applyQueuedEditorGenerationProject( response: { queueState?: ExternalGenerationJobStatusRecord | null }, projectId: string | null | undefined, applyProjectSnapshot: ((project: EditorProjectSnapshot) => void) | undefined, onQueuedGenerationTask?: () => void, onWalletBalanceMayHaveChanged?: () => void, onGenerationWarning?: (message: string) => void, completionDialogId?: string | null, transformProjectSnapshot?: ( project: EditorProjectSnapshot, ) => EditorProjectSnapshot, ) { const queueState = queuedStateFromResponse(response); if (!queueState) { return false; } onQueuedGenerationTask?.(); let terminalState: ExternalGenerationJobStatusRecord | null = null; try { terminalState = await waitForEditorGenerationQueue(queueState); } finally { notifyWalletBalanceMayHaveChanged(onWalletBalanceMayHaveChanged); } if (!terminalState) { return true; } notifyEditorGenerationWarning(terminalState.warning, onGenerationWarning); if (projectId && applyProjectSnapshot) { const applyLoadedProject = (project: EditorProjectSnapshot) => { applyProjectSnapshot(transformProjectSnapshot?.(project) ?? project); }; const project = await loadEditorProject(projectId); applyLoadedProject(project); if (projectHasUnresolvedGenerationDialog(project, completionDialogId)) { await delay(EDITOR_GENERATION_QUEUE_PROJECT_REFRESH_RETRY_MS); applyLoadedProject(await loadEditorProject(projectId)); } } return true; } function positiveCanvasSize(value: number | null | undefined, fallback = 1) { const resolved = Number.isFinite(value) && (value ?? 0) > 0 ? value : fallback; return Math.max(1, Math.round(resolved ?? 1)); } function buildRightSideCanvasCompletionPlaceholder( sourceLayer: CanvasLayer, size?: { width?: number; height?: number; originalWidth?: number; originalHeight?: number; }, ): EditorCanvasGenerationCompletionInput['placeholder'] { const originalWidth = positiveCanvasSize( size?.originalWidth ?? size?.width, sourceLayer.originalWidth || sourceLayer.width || 1, ); const originalHeight = positiveCanvasSize( size?.originalHeight ?? size?.height, sourceLayer.originalHeight || sourceLayer.height || 1, ); const width = positiveCanvasSize(size?.width, originalWidth); const height = positiveCanvasSize(size?.height, originalHeight); return { x: sourceLayer.x + sourceLayer.width + 32, y: sourceLayer.y, width, height, originalWidth, originalHeight, }; } function isVideoQuickEditSource(layer: CanvasLayer) { return layer.mediaType === 'video' || layer.assetKind === 'video'; } export function useImageCanvasGenerationSubmissionWorkflow({ layers, canvasSize, viewport, layerCounterRef, quickEditPanel, quickEditSourceLayer, quickEditSelectionState, canvasGenerationDialogs, setQuickEditPanel, characterAnimationPanel, characterAnimationDialog, characterAnimationSourceLayer, setCharacterAnimationPanel, setGenerateDialog, openCanvasGenerationDialog, updateCanvasGenerationDialogById, hasCanvasGenerationDialogById, getGeneratingDialogPlaceholder, appendCanvasLayersWithResources, captureCanvasHistory, updateSourceLayer, selectSingleLayer, fitLayers, setActiveTool, setActiveSidebarPanel, rememberImageModel, projectId, assetFolderId, upsertGeneratedAsset, applyProjectSnapshot, onQueuedGenerationTask, onWalletBalanceMayHaveChanged, onGenerationWarning, }: GenerationSubmissionWorkflowOptions) { const addGeneratedLayersToCanvas = useCallback( (nextLayers: CanvasLayer[]) => { if (!nextLayers.length) { return; } captureCanvasHistory({ type: 'generate-image', count: nextLayers.length, }); appendCanvasLayersWithResources(nextLayers); nextLayers.forEach((layer) => { const asset = layer.generatedAssetSnapshot; if (asset) { upsertGeneratedAsset?.(asset); } }); }, [ appendCanvasLayersWithResources, captureCanvasHistory, upsertGeneratedAsset, ], ); const addGeneratedResultLayer = useCallback( ( generated: Parameters[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], { captureHistory: false }); } }, [ addGeneratedLayersToCanvas, canvasSize, fitLayers, layerCounterRef, selectSingleLayer, setActiveSidebarPanel, setActiveTool, setGenerateDialog, updateCanvasGenerationDialogById, hasCanvasGenerationDialogById, viewport, ], ); const addQuickEditResultLayer = useCallback( ( generated: Parameters[0]['generated'], sourceLayer: CanvasLayer, generationInputs: CanvasGenerationInputs, mode: QuickEditPanelState['mode'] = 'quick-edit', options: { frame?: GenerateDialogState['placeholder']; dialogId?: string; title?: 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, title: options.title, }); 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], { captureHistory: false }); }, [ addGeneratedLayersToCanvas, fitLayers, layerCounterRef, selectSingleLayer, setActiveSidebarPanel, setActiveTool, setQuickEditPanel, updateCanvasGenerationDialogById, hasCanvasGenerationDialogById, ], ); const applyQuickEditResultToSourceLayer = useCallback( ( generated: Parameters< typeof applyImageEditResultToSourceLayer >[0]['generated'], sourceLayer: CanvasLayer, generationInputs: CanvasGenerationInputs, dialogId?: string, title?: string, ) => { updateSourceLayer( sourceLayer.id, (layer) => ({ ...applyImageEditResultToSourceLayer({ generated, sourceLayer: layer, generationInputs, }), ...(title ? { title } : {}), }), { fit: false, persist: false }, ); setQuickEditPanel(null); setActiveTool('select'); setActiveSidebarPanel('layers'); if (dialogId) { updateCanvasGenerationDialogById(dialogId, () => null); } }, [ setActiveSidebarPanel, setActiveTool, setQuickEditPanel, updateCanvasGenerationDialogById, updateSourceLayer, ], ); 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[0]['generated'], title: string, generationInputs: CanvasGenerationInputs, frame?: GenerateDialogState['placeholder'], dialogId?: string, options: { sourceLayer?: CanvasLayer; assetKind?: CanvasLayer['assetKind']; } = {}, ) => { if (dialogId && !hasCanvasGenerationDialogById(dialogId)) { return; } layerCounterRef.current += 1; const generatedIndex = layerCounterRef.current; const nextLayer = createVideoResultLayer({ generated, generatedIndex, title, canvasSize, viewport, generationInputs, frame, sourceLayer: options.sourceLayer, assetKind: options.assetKind, }); 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[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, options: { sourceLayer?: CanvasLayer; } = {}, ) => { if (dialogId && !hasCanvasGenerationDialogById(dialogId)) { return; } layerCounterRef.current += 1; const generatedIndex = layerCounterRef.current; const nextLayer = createCharacterAnimationResultLayer({ generated, generatedIndex, title, canvasSize, viewport, generationInputs, frame, sourceLayer: options.sourceLayer, }); 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, options: UiDesignAssetExtractionOptions = {}, ) => { if (sourceLayer.assetKind !== 'ui-design') { return; } try { let sourceImageSrc: string; if (options.marks?.length) { const renderSourceImageSrc = await resolveEditorImageReferenceDataUrl( sourceLayer.objectKey?.trim() || sourceLayer.src, ); const markedImageDataUrl = await renderUiDesignAssetExtractionMarkedImage({ source: renderSourceImageSrc, marks: options.marks, }); sourceImageSrc = await uploadEditorGenerationImageDataUrl( markedImageDataUrl, projectId, ); } else { sourceImageSrc = await resolveEditorGenerationMediaReference( sourceLayer, 'image', projectId, ); } const screenColor = DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR; const segModel = DEFAULT_EDITOR_BGFILTER_SEG_MODEL; const generationInputs = buildUiDesignAssetExtractionGenerationInputs( sourceLayer, options.references, ); const referenceImageSrcs = await Promise.all( (options.references ?? []).map((reference) => resolveEditorGenerationMediaReference( reference, 'image', projectId, ), ), ); const extractionPlan = resolveUiAssetExtractionGenerationPlan( options.marks?.length ?? 0, ); const extractionSize = resolveEditorImageGenerationPixelSize({ model: options.model ?? DEFAULT_IMAGE_MODEL, aspectRatio: extractionPlan.aspectRatio, imageSize: extractionPlan.imageSize, }); const extractionFrame = { x: sourceLayer.x + sourceLayer.width + 32, y: sourceLayer.y, width: extractionSize.width, height: extractionSize.height, originalWidth: extractionSize.width, originalHeight: extractionSize.height, }; const generated = await runEditorGenerationWithWalletRefresh( extractEditorUiDesignAssets({ sourceImageSrc, model: normalizeEditorImageModel( options.model ?? DEFAULT_IMAGE_MODEL, ), screenColor, segModel, ...(referenceImageSrcs.length ? { referenceImageSrcs } : {}), projectId, generationInputs, assetFolderId, spritesheetLabel: `${sourceLayer.title} 素材图集`, aspectRatio: extractionPlan.aspectRatio, imageSize: extractionPlan.imageSize, ...(projectId ? { canvasCompletion: { title: `${sourceLayer.title} 素材图集`, placeholder: extractionFrame, }, } : {}), }), onWalletBalanceMayHaveChanged, ); notifyEditorGenerationWarning( resolveEditorGenerationWarningMessage( generated.warning?.reason, generated.sliceWarning?.reason, ), onGenerationWarning, ); if ( await applyQueuedEditorGenerationProject( generated, projectId, applyProjectSnapshot, onQueuedGenerationTask, onWalletBalanceMayHaveChanged, onGenerationWarning, ) ) { return; } if (generated.project && applyProjectSnapshot) { applyProjectSnapshot(generated.project); return; } const nextLayers = addIconSpritesheetResultLayers( generated, generated.iconImageSrcs ?? [], generationInputs, extractionFrame, undefined, { spritesheetTitle: `${sourceLayer.title} 素材图集`, }, ); if (nextLayers.length) { fitLayers([sourceLayer, ...nextLayers], { captureHistory: false }); } } catch (error) { if (options.suppressAlert) { throw error; } window.alert( error instanceof Error && error.message.trim() ? error.message : '提取素材失败', ); } }, [ addIconSpritesheetResultLayers, applyProjectSnapshot, fitLayers, projectId, assetFolderId, onGenerationWarning, onQueuedGenerationTask, onWalletBalanceMayHaveChanged, ], ); 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, layerCounterRef.current + 1, ); if (submissionPlan.ok === false) { if (canvasDialog) { setSubmittingIconDialog({ ...canvasDialog, status: 'failed', composerOpen: true, errorMessage: submissionPlan.errorMessage, }); } return; } if (!canvasDialog) { return; } const iconSpecReference = dialog.iconSpecReference; if (!iconSpecReference) { return; } setSubmittingIconDialog({ ...canvasDialog, iconDescriptions: submissionPlan.iconDescriptions, status: 'generating', composerOpen: false, errorMessage: undefined, }); try { const canvasCompletionPlaceholder = getGeneratingDialogPlaceholder(dialog); const referenceImageSrc = await resolveEditorGenerationMediaReference( iconSpecReference, 'image', projectId, ); const referenceImageSrcs = await Promise.all( (dialog.generationReferences ?? []).map((reference) => resolveEditorGenerationMediaReference( reference, 'image', projectId, ), ), ); const generated = await runEditorGenerationWithWalletRefresh( generateEditorIconSpritesheet({ ...submissionPlan.input, referenceImageSrc, ...(referenceImageSrcs.length ? { referenceImageSrcs } : {}), projectId, generationInputs: submissionPlan.generationInputs, assetFolderId, assetLabel: submissionPlan.resultTitle, ...(projectId && canvasCompletionPlaceholder ? { canvasCompletion: { dialogId: canvasDialog.id, title: submissionPlan.resultTitle, placeholder: canvasCompletionPlaceholder, }, } : {}), }), onWalletBalanceMayHaveChanged, ); rememberImageModel(submissionPlan.rememberImageModel); notifyEditorGenerationWarning( resolveEditorGenerationWarningMessage( generated.warning?.reason, generated.sliceWarning?.reason, ), onGenerationWarning, ); if ( await applyQueuedEditorGenerationProject( generated, projectId, applyProjectSnapshot, onQueuedGenerationTask, onWalletBalanceMayHaveChanged, onGenerationWarning, ) ) { return; } if (generated.project && applyProjectSnapshot) { applyProjectSnapshot(generated.project); return; } addIconSpritesheetResultLayers( generated, generated.iconImageSrcs ?? [], submissionPlan.generationInputs, canvasCompletionPlaceholder, canvasDialog.id, { spritesheetTitle: submissionPlan.resultTitle }, ); } catch (error) { setSubmittingIconDialog({ ...canvasDialog, iconDescriptions: submissionPlan.iconDescriptions, status: 'failed', composerOpen: true, errorMessage: resolveImageGenerationErrorMessage(error), }); } }, [ addIconSpritesheetResultLayers, applyProjectSnapshot, getGeneratingDialogPlaceholder, layerCounterRef, rememberImageModel, projectId, assetFolderId, updateCanvasGenerationDialogById, onGenerationWarning, onQueuedGenerationTask, onWalletBalanceMayHaveChanged, ], ); const submitQuickEdit = useCallback(async () => { if (!quickEditPanel || !quickEditSourceLayer) { return; } if (isQuickEditUnsupportedAssetKind(quickEditSourceLayer)) { setQuickEditPanel({ ...quickEditPanel, status: 'failed', errorMessage: '图标类素材不支持快速编辑', }); return; } const panelMode = quickEditPanel.mode ?? 'quick-edit'; const quickEditResultTitle = resolveGenerationAssetLabel( quickEditPanel.assetLabel, `${quickEditSourceLayer.title} ${panelMode === 'redraw' ? '重绘' : '快速编辑'}`, ); const quickEditReferences = panelMode === 'redraw' || panelMode === 'quick-edit' ? [] : (quickEditPanel.quickEditReferences ?? []); const quickEditAspectRatio = quickEditPanel.aspectRatio ?? inferEditorImageAspectRatio( quickEditSourceLayer.originalWidth, quickEditSourceLayer.originalHeight, ); const quickEditImageSize = quickEditPanel.imageSize ?? inferEditorImageSizeLabel( quickEditSourceLayer.originalWidth, quickEditSourceLayer.originalHeight, ); const normalizedQuickEditModel = normalizeEditorImageModel( quickEditPanel.model, ); const quickEditOutputSize = isVideoQuickEditSource(quickEditSourceLayer) ? { width: quickEditSourceLayer.originalWidth, height: quickEditSourceLayer.originalHeight, } : resolveEditorImageGenerationPixelSize({ model: normalizedQuickEditModel, aspectRatio: quickEditAspectRatio, imageSize: quickEditImageSize, }); const quickEditSize = `${quickEditOutputSize.width}x${quickEditOutputSize.height}`; const basePrompt = quickEditPanel.prompt.trim() || (panelMode === 'redraw' ? '重绘图片' : '快速编辑图片'); const normalizedPrompt = panelMode === 'redraw' ? basePrompt : basePrompt; let quickEditDialogId: string | undefined; const shouldUseCanvasPlaceholder = panelMode === 'quick-edit' && isVideoQuickEditSource(quickEditSourceLayer); if (shouldUseCanvasPlaceholder) { const quickEditDraft = createQuickEditGenerationDialogDraft({ sourceLayer: quickEditSourceLayer, prompt: normalizedPrompt, status: 'generating', references: quickEditReferences, model: quickEditPanel.model, aspectRatio: quickEditAspectRatio, imageSize: quickEditImageSize, frame: { width: quickEditOutputSize.width, height: quickEditOutputSize.height, }, assetLabel: quickEditResultTitle, }); const quickEditPlacement = quickEditDraft.placeholder ? chooseGenerationPlacement({ canvasSize, viewport, frame: quickEditDraft.placeholder, layers, generationDialogs: canvasGenerationDialogs, }) : quickEditDraft.placeholder; quickEditDialogId = openCanvasGenerationDialog({ ...quickEditDraft, // 中文注释:视频快速编辑仍落到独立视频结果占位,图片快速编辑直接替换源图。 placeholder: quickEditPlacement, }); setQuickEditPanel(null); selectSingleLayer(null); } else { setQuickEditPanel({ ...quickEditPanel, aspectRatio: quickEditAspectRatio, imageSize: quickEditImageSize, size: quickEditSize, prompt: normalizedPrompt, status: 'generating', errorMessage: undefined, }); } try { const needsExtraReferenceImageSrcs = panelMode === 'quick-edit' || panelMode === 'redraw' || isVideoQuickEditSource(quickEditSourceLayer); const extraReferenceImageSrcs = needsExtraReferenceImageSrcs ? await Promise.all( quickEditReferences.map((reference) => resolveEditorGenerationMediaReference( reference, 'image', projectId, ), ), ) : []; const quickEditFrame = quickEditDialogId ? getGeneratingDialogPlaceholder({ id: quickEditDialogId, mode: 'quick-edit', prompt: normalizedPrompt, status: 'generating', }) : undefined; const quickEditCanvasCompletionPlaceholder = quickEditFrame ?? buildRightSideCanvasCompletionPlaceholder(quickEditSourceLayer, { width: quickEditOutputSize.width, height: quickEditOutputSize.height, originalWidth: quickEditOutputSize.width, originalHeight: quickEditOutputSize.height, }); if ( panelMode === 'quick-edit' && isVideoQuickEditSource(quickEditSourceLayer) ) { const videoModel = DEFAULT_VIDEO_MODEL; const videoResolution = '480p' as const; const videoDurationSeconds = 4 as const; const sourceVideoSrc = await resolveEditorGenerationMediaReference( quickEditSourceLayer, 'video', projectId, { allowRegisteredIds: false }, ); const generationInputs = buildVideoGenerationInputs( normalizedPrompt, quickEditReferences, ); const generated = await runEditorGenerationWithWalletRefresh( generateEditorVideo({ prompt: normalizedPrompt, model: videoModel, aspectRatio: '16:9', durationSeconds: videoDurationSeconds, resolution: videoResolution, mode: 'std', sound: 'off', ...(extraReferenceImageSrcs.length ? { referenceImageSrcs: extraReferenceImageSrcs } : {}), referenceVideoSrcs: [sourceVideoSrc], projectId, generationInputs, sourceResourceId: quickEditSourceLayer.resourceId, assetKind: quickEditSourceLayer.assetKind, assetFolderId, assetLabel: quickEditResultTitle, ...(projectId && quickEditCanvasCompletionPlaceholder ? { canvasCompletion: { dialogId: quickEditDialogId, title: quickEditResultTitle, placeholder: quickEditCanvasCompletionPlaceholder, }, } : {}), }), onWalletBalanceMayHaveChanged, ); if ( await applyQueuedEditorGenerationProject( generated, projectId, applyProjectSnapshot, onQueuedGenerationTask, onWalletBalanceMayHaveChanged, ) ) { return; } if (generated.project && applyProjectSnapshot) { applyProjectSnapshot(generated.project); if (generated.asset) { upsertGeneratedAsset?.(generated.asset); } return; } addVideoResultLayer( generated, quickEditResultTitle, generationInputs, quickEditFrame, quickEditDialogId, { sourceLayer: quickEditSourceLayer, assetKind: quickEditSourceLayer.assetKind, }, ); return; } const quickEditSelectionMarks = panelMode === 'quick-edit' && quickEditSelectionState?.sourceLayerId === quickEditSourceLayer.id ? quickEditSelectionState.marks : []; let sourceReferenceImageSrc: string; if (quickEditSelectionMarks.length > 0) { const renderSourceImageSrc = await resolveEditorImageReferenceDataUrl( quickEditSourceLayer.objectKey?.trim() || quickEditSourceLayer.src, ); const markedImageDataUrl = await renderUiDesignAssetExtractionMarkedImage({ source: renderSourceImageSrc, marks: quickEditSelectionMarks, showIndexLabels: true, }); sourceReferenceImageSrc = await uploadEditorGenerationImageDataUrl( markedImageDataUrl, projectId, ); } else { sourceReferenceImageSrc = await resolveEditorGenerationMediaReference( quickEditSourceLayer, 'image', projectId, ); } const generationInputs = panelMode === 'redraw' ? buildEditGenerationInputs( '重绘提示词', normalizedPrompt, quickEditSourceLayer, ) : buildQuickEditGenerationInputs( '快速编辑提示词', normalizedPrompt, quickEditSourceLayer, quickEditReferences, ); const isCharacterRedraw = panelMode === 'redraw' && quickEditSourceLayer.assetKind === 'character'; const generated = panelMode === 'quick-edit' ? await runEditorGenerationWithWalletRefresh( editEditorImage({ prompt: normalizedPrompt, sourceImageSrc: sourceReferenceImageSrc, size: quickEditSize, model: normalizedQuickEditModel, aspectRatio: quickEditAspectRatio, imageSize: quickEditImageSize, ...(extraReferenceImageSrcs.length ? { referenceImageSrcs: extraReferenceImageSrcs } : {}), projectId, assetKind: quickEditSourceLayer.assetKind, generationInputs, assetFolderId, assetLabel: quickEditResultTitle, sourceResourceId: quickEditSourceLayer.resourceId, targetLayerId: quickEditSourceLayer.id, }), onWalletBalanceMayHaveChanged, ) : await runEditorGenerationWithWalletRefresh( generateEditorImage({ prompt: normalizedPrompt, size: quickEditSize, aspectRatio: quickEditAspectRatio, imageSize: quickEditImageSize, kind: isCharacterRedraw ? 'character' : 'quick-edit', model: normalizedQuickEditModel, referenceImageSrcs: [ ...extraReferenceImageSrcs, sourceReferenceImageSrc, ], projectId, assetKind: quickEditSourceLayer.assetKind, generationInputs, assetFolderId, assetLabel: quickEditResultTitle, sourceResourceId: quickEditSourceLayer.resourceId, ...(projectId && quickEditCanvasCompletionPlaceholder ? { canvasCompletion: { dialogId: quickEditDialogId, title: quickEditResultTitle, placeholder: quickEditCanvasCompletionPlaceholder, }, } : {}), }), onWalletBalanceMayHaveChanged, ); notifyEditorGenerationWarning( generated.warning?.reason, onGenerationWarning, ); if ( await applyQueuedEditorGenerationProject( generated, projectId, applyProjectSnapshot, onQueuedGenerationTask, onWalletBalanceMayHaveChanged, onGenerationWarning, ) ) { if (panelMode === 'quick-edit') { setQuickEditPanel(null); setActiveTool('select'); setActiveSidebarPanel('layers'); } return; } if (generated.project && applyProjectSnapshot) { applyProjectSnapshot(generated.project); if (panelMode === 'quick-edit') { setQuickEditPanel(null); setActiveTool('select'); setActiveSidebarPanel('layers'); } return; } if (panelMode === 'quick-edit') { applyQuickEditResultToSourceLayer( generated, quickEditSourceLayer, generationInputs, quickEditDialogId, quickEditResultTitle, ); } else { addQuickEditResultLayer( generated, quickEditSourceLayer, generationInputs, panelMode, quickEditDialogId ? { frame: quickEditFrame, dialogId: quickEditDialogId, title: quickEditResultTitle, } : { title: quickEditResultTitle }, ); } } catch (error) { if (quickEditDialogId) { updateCanvasGenerationDialogById( quickEditDialogId, (currentDialog) => ({ ...currentDialog, prompt: normalizedPrompt, status: 'failed', composerOpen: false, errorMessage: resolveImageGenerationErrorMessage(error), }), ); } setQuickEditPanel({ ...quickEditPanel, aspectRatio: quickEditAspectRatio, imageSize: quickEditImageSize, size: quickEditSize, prompt: normalizedPrompt, status: 'failed', errorMessage: resolveImageGenerationErrorMessage(error), }); } }, [ addQuickEditResultLayer, addVideoResultLayer, applyQuickEditResultToSourceLayer, applyProjectSnapshot, canvasGenerationDialogs, canvasSize, getGeneratingDialogPlaceholder, layers, openCanvasGenerationDialog, projectId, assetFolderId, upsertGeneratedAsset, quickEditPanel, quickEditSourceLayer, quickEditSelectionState, selectSingleLayer, setActiveSidebarPanel, setActiveTool, setQuickEditPanel, updateCanvasGenerationDialogById, viewport, onGenerationWarning, onQueuedGenerationTask, onWalletBalanceMayHaveChanged, ]); 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 resolveEditorGenerationMediaReference( submissionPlan.sourceLayer, 'image', projectId, ); const canvasCompletionPlaceholder = getGeneratingDialogPlaceholder(dialog); const editCanvasCompletionPlaceholder = canvasCompletionPlaceholder ?? buildRightSideCanvasCompletionPlaceholder( submissionPlan.sourceLayer, { width: submissionPlan.sourceLayer.originalWidth, height: submissionPlan.sourceLayer.originalHeight, originalWidth: submissionPlan.sourceLayer.originalWidth, originalHeight: submissionPlan.sourceLayer.originalHeight, }, ); const generated = await runEditorGenerationWithWalletRefresh( editEditorImage({ prompt: submissionPlan.normalizedPrompt, sourceImageSrc: referenceImageSrc, size: `${submissionPlan.sourceLayer.originalWidth}x${submissionPlan.sourceLayer.originalHeight}`, projectId, assetKind: submissionPlan.sourceLayer.assetKind, generationInputs: submissionPlan.generationInputs, assetFolderId, assetLabel: submissionPlan.resultTitle, sourceResourceId: submissionPlan.sourceLayer.resourceId, ...(projectId && editCanvasCompletionPlaceholder ? { canvasCompletion: { dialogId: canvasDialog?.id, title: submissionPlan.resultTitle, placeholder: editCanvasCompletionPlaceholder, }, } : {}), }), onWalletBalanceMayHaveChanged, ); if ( await applyQueuedEditorGenerationProject( generated, projectId, applyProjectSnapshot, onQueuedGenerationTask, onWalletBalanceMayHaveChanged, ) ) { return; } if (generated.project && applyProjectSnapshot) { applyProjectSnapshot(generated.project); return; } addGeneratedResultLayer(generated, { sourceLayer: submissionPlan.sourceLayer, title: submissionPlan.resultTitle, generationInputs: submissionPlan.generationInputs, }); } else if (submissionPlan.kind === 'quick-edit') { const quickEditSelectionMarks = quickEditSelectionState?.sourceLayerId === submissionPlan.sourceLayer.id ? quickEditSelectionState.marks : []; let referenceImageSrc: string; if (quickEditSelectionMarks.length > 0) { const renderSourceImageSrc = await resolveEditorImageReferenceDataUrl( submissionPlan.sourceLayer.objectKey?.trim() || submissionPlan.sourceLayer.src, ); const markedImageDataUrl = await renderUiDesignAssetExtractionMarkedImage({ source: renderSourceImageSrc, marks: quickEditSelectionMarks, showIndexLabels: true, }); referenceImageSrc = await uploadEditorGenerationImageDataUrl( markedImageDataUrl, projectId, ); } else { referenceImageSrc = await resolveEditorGenerationMediaReference( submissionPlan.sourceLayer, 'image', projectId, ); } const generated = await runEditorGenerationWithWalletRefresh( editEditorImage({ prompt: submissionPlan.normalizedPrompt, sourceImageSrc: referenceImageSrc, ...submissionPlan.editInput, projectId, assetKind: submissionPlan.result.assetKind, generationInputs: submissionPlan.result.generationInputs, assetFolderId, assetLabel: submissionPlan.result.title, sourceResourceId: submissionPlan.sourceLayer.resourceId, targetLayerId: submissionPlan.sourceLayer.id, }), onWalletBalanceMayHaveChanged, ); if ( await applyQueuedEditorGenerationProject( generated, projectId, applyProjectSnapshot, onQueuedGenerationTask, onWalletBalanceMayHaveChanged, ) ) { return; } if (generated.project && applyProjectSnapshot) { applyProjectSnapshot(generated.project); if (generated.asset) { upsertGeneratedAsset?.(generated.asset); } return; } applyQuickEditResultToSourceLayer( generated, submissionPlan.sourceLayer, submissionPlan.result.generationInputs, canvasDialog?.id, submissionPlan.result.title, ); } else if (submissionPlan.kind === 'video') { const canvasCompletionPlaceholder = getGeneratingDialogPlaceholder(dialog); const videoGenerationInput = await normalizeVideoGenerationReferences( submissionPlan.input, dialog.generationReferences ?? [], projectId, ); const generated = await runEditorGenerationWithWalletRefresh( generateEditorVideo({ ...videoGenerationInput, projectId, generationInputs: submissionPlan.result.generationInputs, assetFolderId, assetLabel: submissionPlan.result.title, assetKind: 'video', ...(projectId && canvasCompletionPlaceholder ? { canvasCompletion: { dialogId: canvasDialog?.id, title: submissionPlan.result.title, placeholder: canvasCompletionPlaceholder, }, } : {}), }), onWalletBalanceMayHaveChanged, ); if ( await applyQueuedEditorGenerationProject( generated, projectId, applyProjectSnapshot, onQueuedGenerationTask, onWalletBalanceMayHaveChanged, ) ) { return; } if (generated.project && applyProjectSnapshot) { applyProjectSnapshot(generated.project); if (generated.asset) { upsertGeneratedAsset?.(generated.asset); } return; } addVideoResultLayer( generated, submissionPlan.result.title, submissionPlan.result.generationInputs, canvasCompletionPlaceholder, canvasDialog?.id, ); } else if (submissionPlan.kind === 'audio') { const canvasCompletionPlaceholder = getGeneratingDialogPlaceholder(dialog); const generated = await runEditorGenerationWithWalletRefresh( submissionPlan.audioKind === 'sound-effect' ? generateEditorSoundEffect({ ...submissionPlan.input, projectId, generationInputs: submissionPlan.result.generationInputs, assetFolderId, assetLabel: submissionPlan.result.title, ...(projectId && canvasCompletionPlaceholder ? { canvasCompletion: { dialogId: canvasDialog?.id, title: submissionPlan.result.title, placeholder: canvasCompletionPlaceholder, }, } : {}), }) : generateEditorBackgroundMusic({ ...submissionPlan.input, projectId, generationInputs: submissionPlan.result.generationInputs, assetFolderId, assetLabel: submissionPlan.result.title, ...(projectId && canvasCompletionPlaceholder ? { canvasCompletion: { dialogId: canvasDialog?.id, title: submissionPlan.result.title, placeholder: canvasCompletionPlaceholder, }, } : {}), }), onWalletBalanceMayHaveChanged, ); if ( await applyQueuedEditorGenerationProject( generated, projectId, applyProjectSnapshot, onQueuedGenerationTask, onWalletBalanceMayHaveChanged, ) ) { return; } if (generated.project && applyProjectSnapshot) { applyProjectSnapshot(generated.project); if (generated.asset) { upsertGeneratedAsset?.(generated.asset); } return; } addAudioResultLayer( generated, submissionPlan.result.title, submissionPlan.result.generationInputs, canvasCompletionPlaceholder, canvasDialog?.id, ); } else { const imageGenerationInput = await normalizeImageGenerationReferenceImages( submissionPlan.input, resolveImageGenerationDialogReferences(dialog), projectId, ); const resultTitle = submissionPlan.result.title ?? `生成图片 ${layerCounterRef.current + 1}`; const canvasCompletionPlaceholder = getGeneratingDialogPlaceholder(dialog); const generated = await runEditorGenerationWithWalletRefresh( generateEditorImage({ ...imageGenerationInput, projectId, assetKind: submissionPlan.result.assetKind, generationInputs: submissionPlan.result.generationInputs, assetFolderId, assetLabel: resultTitle, ...(projectId && canvasCompletionPlaceholder ? { canvasCompletion: { dialogId: canvasDialog?.id, title: resultTitle, placeholder: canvasCompletionPlaceholder, }, } : {}), }), onWalletBalanceMayHaveChanged, ); notifyEditorGenerationWarning( generated.warning?.reason, onGenerationWarning, ); if ( await applyQueuedEditorGenerationProject( generated, projectId, applyProjectSnapshot, onQueuedGenerationTask, onWalletBalanceMayHaveChanged, onGenerationWarning, ) ) { return; } if (submissionPlan.rememberImageModel) { rememberImageModel(submissionPlan.rememberImageModel); } if (generated.project && applyProjectSnapshot) { applyProjectSnapshot(generated.project); if (generated.asset) { upsertGeneratedAsset?.(generated.asset); } return; } if (canvasDialog && projectId) { updateCanvasGenerationDialogById( canvasDialog.id, (currentDialog) => currentDialog.generatedLayerId ? currentDialog : { ...currentDialog, status: 'idle', composerOpen: true, errorMessage: undefined, }, ); return; } addGeneratedResultLayer(generated, { frame: canvasCompletionPlaceholder, assetKind: submissionPlan.result.assetKind, title: resultTitle, 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, applyQuickEditResultToSourceLayer, getGeneratingDialogPlaceholder, layerCounterRef, layers, quickEditSelectionState, rememberImageModel, projectId, assetFolderId, applyProjectSnapshot, setGenerateDialog, updateCanvasGenerationDialogById, upsertGeneratedAsset, onGenerationWarning, onQueuedGenerationTask, onWalletBalanceMayHaveChanged, ], ); const submitCharacterAnimation = useCallback(async () => { if (!characterAnimationPanel || !characterAnimationSourceLayer) { return; } if ( characterAnimationPanel.status === 'generating' || (characterAnimationDialog?.mode === 'character-animation' && characterAnimationDialog.status === 'generating') ) { 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, assetLabel: submissionPlan.resultTitle, characterAnimationResult: undefined, status: 'generating', composerOpen: false, errorMessage: undefined, })); } const nextPanel = { ...characterAnimationPanel, promptText: submissionPlan.promptText, assetLabel: submissionPlan.resultTitle, status: 'generating' as const, errorMessage: undefined, result: undefined, }; if (!canvasDialog) { setCharacterAnimationPanel(nextPanel); } try { const generationInputs = buildCharacterAnimationGenerationInputs( submissionPlan.promptText, characterAnimationSourceLayer, ); const canvasCompletionPlaceholder = canvasDialog ? getGeneratingDialogPlaceholder(canvasDialog) : undefined; const sourceImageSrc = await resolveEditorGenerationMediaReference( characterAnimationSourceLayer, 'image', projectId, { allowRegisteredIds: false, requireImageObjectReference: true, }, ); const result = await runEditorGenerationWithWalletRefresh( generateEditorCharacterAnimation({ ...submissionPlan.input, sourceImageSrc, projectId, generationInputs, sourceResourceId: characterAnimationSourceLayer.resourceId, assetFolderId, ...(canvasDialog && projectId && canvasCompletionPlaceholder ? { canvasCompletion: { dialogId: canvasDialog.id, title: submissionPlan.resultTitle, placeholder: canvasCompletionPlaceholder, }, } : {}), }), onWalletBalanceMayHaveChanged, ); if ( await applyQueuedEditorGenerationProject( result, projectId, applyProjectSnapshot, onQueuedGenerationTask, onWalletBalanceMayHaveChanged, ) ) { return; } if (result.project && applyProjectSnapshot) { applyProjectSnapshot(result.project); return; } if (canvasDialog) { addCharacterAnimationResultLayer( result, submissionPlan.resultTitle, generationInputs, canvasCompletionPlaceholder, canvasDialog.id, { sourceLayer: characterAnimationSourceLayer, }, ); 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: resolveImageGenerationErrorMessage( error, '生成角色动画失败', ), })); return; } setCharacterAnimationPanel((currentPanel) => currentPanel ? { ...currentPanel, status: 'failed', errorMessage: resolveImageGenerationErrorMessage( error, '生成角色动画失败', ), } : currentPanel, ); } }, [ addCharacterAnimationResultLayer, assetFolderId, characterAnimationDialog, characterAnimationPanel, characterAnimationSourceLayer, getGeneratingDialogPlaceholder, applyProjectSnapshot, projectId, setCharacterAnimationPanel, updateCanvasGenerationDialogById, onWalletBalanceMayHaveChanged, onQueuedGenerationTask, ]); return useMemo( () => ({ submitIconSpritesheetGeneration, extractUiDesignAssets, submitQuickEdit, submitImageGeneration, submitCharacterAnimation, }), [ submitCharacterAnimation, extractUiDesignAssets, submitIconSpritesheetGeneration, submitImageGeneration, submitQuickEdit, ], ); }