8abeb333bb
- 使用白名单控制快速编辑功能的显示 - 同步浮动工具栏、右键菜单、打开入口和提交门禁 - 禁用角色动作序列帧(整体)、音频和单个图标(explain: backend reject)的快速编辑 - 把快速编辑api改为只接受resource Id | asset Id,删除 image_src 参数, 以保证校验 其中包含生产数据迁移。迁移用于清理历史普通图片错误持久化的 assetKind="image",覆盖 asset → project-resource → showcase → canvas 四个作用域,并包含 operator 鉴权、分批 dry-run、批次 SHA-256 绑定 apply、active 结构化画 > 布迁移摘要同步及最终零残留复核。 --------- Co-authored-by: kdletters <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/140 Co-authored-by: 王德宇 <kvtodev@outlook.com> Co-committed-by: 王德宇 <kvtodev@outlook.com>
2869 lines
95 KiB
TypeScript
2869 lines
95 KiB
TypeScript
import {
|
|
type Dispatch,
|
|
type MutableRefObject,
|
|
type SetStateAction,
|
|
useCallback,
|
|
useLayoutEffect,
|
|
useMemo,
|
|
useRef,
|
|
} 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,
|
|
uploadEditorMediaAssetObjectFile,
|
|
} from '../../services/image-editor/editorMediaAssetUploadClient';
|
|
import type {
|
|
EditorAssetSnapshot,
|
|
EditorCanvasGenerationCompletionInput,
|
|
EditorProjectSnapshot,
|
|
} from '../../services/image-editor/editorProjectClient';
|
|
import {
|
|
editEditorImage,
|
|
extractEditorUiDesignAssets,
|
|
generateEditorBackgroundMusic,
|
|
generateEditorCharacterAnimation,
|
|
generateEditorIconSpec,
|
|
generateEditorIconSpritesheet,
|
|
generateEditorImage,
|
|
generateEditorScene,
|
|
generateEditorSoundEffect,
|
|
generateEditorVideo,
|
|
loadEditorProject,
|
|
} from '../../services/image-editor/editorProjectClient';
|
|
import {
|
|
findCanvasGenerationDialogRecords,
|
|
isUnresolvedCanvasGenerationDialogRecord,
|
|
} from './ImageCanvasEditorModel';
|
|
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,
|
|
createVideoResultLayer,
|
|
} from './ImageCanvasGenerationLayerModel';
|
|
import {
|
|
buildCharacterAnimationGenerationInputs,
|
|
buildQuickEditGenerationInputs,
|
|
buildUiDesignAssetExtractionGenerationInputs,
|
|
buildVideoGenerationInputs,
|
|
createCanvasLayerReference,
|
|
DEFAULT_EDITOR_BGFILTER_SEG_MODEL,
|
|
DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR,
|
|
DEFAULT_IMAGE_MODEL,
|
|
DEFAULT_VIDEO_MODEL,
|
|
inferEditorImageAspectRatio,
|
|
inferEditorImageSizeLabel,
|
|
isCanvasGenerationDialog,
|
|
isQuickEditSupportedLayer,
|
|
normalizeEditorImageModel,
|
|
resolveEditorImageGenerationPixelSize,
|
|
resolveImageGenerationErrorMessage,
|
|
} from './ImageCanvasGenerationModel';
|
|
import { chooseGenerationPlacement } from './ImageCanvasGenerationPlacementModel';
|
|
import {
|
|
buildCharacterAnimationSubmissionPlan,
|
|
buildIconSpritesheetGenerationSubmissionPlan,
|
|
buildImageGenerationSubmissionPlan,
|
|
resolveGenerationAssetLabel,
|
|
resolveImageGenerationDialogPrompt,
|
|
resolveRegisteredEditorReferenceId,
|
|
} from './ImageCanvasGenerationSubmissionModel';
|
|
import { validateSoundEffectPrompt } from './ImageCanvasSoundEffectPromptModel';
|
|
import type {
|
|
UiAssetExtractionMark,
|
|
UiAssetExtractionState,
|
|
} from './ImageCanvasUiAssetExtractionModel';
|
|
import { resolveUiAssetExtractionGenerationPlan } from './ImageCanvasUiAssetExtractionModel';
|
|
import { renderUiDesignAssetExtractionMarkedImage } from './ImageCanvasUiAssetExtractionRasterModel';
|
|
import type { BackgroundMusicPromptAssistController } from './useImageCanvasBackgroundMusicPromptAssist';
|
|
import type { SoundEffectPromptAssistController } from './useImageCanvasSoundEffectPromptAssist';
|
|
|
|
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;
|
|
// 中文注释:需要 unknown 重放的同步操作由调用方传入稳定 id;普通入口不传时仍为每次
|
|
// 上传生成随机路径,避免并发参考图互相覆盖。
|
|
uploadId?: string;
|
|
// 中文注释:由调用方的阶段预算驱动,并贯穿源读取、Data URL 转换以及
|
|
// ticket → PUT → confirm,不能只在最外层停止 await。
|
|
signal?: AbortSignal;
|
|
};
|
|
|
|
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 throwIfEditorGenerationMediaUploadAborted(signal?: AbortSignal) {
|
|
signal?.throwIfAborted();
|
|
}
|
|
|
|
function dataUrlToEditorGenerationFile(
|
|
dataUrl: string,
|
|
fileName: string,
|
|
signal?: AbortSignal,
|
|
) {
|
|
throwIfEditorGenerationMediaUploadAborted(signal);
|
|
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);
|
|
throwIfEditorGenerationMediaUploadAborted(signal);
|
|
const bytes = new Uint8Array(binary.length);
|
|
for (let index = 0; index < binary.length; index += 1) {
|
|
bytes[index] = binary.charCodeAt(index);
|
|
}
|
|
throwIfEditorGenerationMediaUploadAborted(signal);
|
|
return new File([bytes], fileName, { type });
|
|
}
|
|
|
|
async function inlineMediaSourceToEditorGenerationFile(
|
|
source: string,
|
|
mediaType: EditorMediaAssetUploadType,
|
|
uploadId: string,
|
|
signal?: AbortSignal,
|
|
) {
|
|
throwIfEditorGenerationMediaUploadAborted(signal);
|
|
const fileName = `generation-reference-${uploadId}.${
|
|
mediaType === 'video' ? 'mp4' : mediaType === 'audio' ? 'mp3' : 'png'
|
|
}`;
|
|
if (/^data:/iu.test(source)) {
|
|
return dataUrlToEditorGenerationFile(source, fileName, signal);
|
|
}
|
|
const response = await fetch(source, { signal });
|
|
throwIfEditorGenerationMediaUploadAborted(signal);
|
|
if (!response.ok) {
|
|
throw new Error('读取本地生成参考素材失败');
|
|
}
|
|
const blob = await response.blob();
|
|
throwIfEditorGenerationMediaUploadAborted(signal);
|
|
return new File([blob], fileName, {
|
|
type: blob.type || `${mediaType}/*`,
|
|
});
|
|
}
|
|
|
|
async function uploadEditorGenerationInlineMediaSource(
|
|
source: string,
|
|
mediaType: EditorMediaAssetUploadType,
|
|
projectId?: string | null,
|
|
signal?: AbortSignal,
|
|
stableUploadId?: string | null,
|
|
) {
|
|
const normalizedProjectId = projectId?.trim() || 'unscoped';
|
|
const uploadId =
|
|
stableUploadId?.trim() || createEditorGenerationMediaUploadId();
|
|
const uploaded = await uploadEditorMediaAssetObjectFile(
|
|
await inlineMediaSourceToEditorGenerationFile(
|
|
source,
|
|
mediaType,
|
|
uploadId,
|
|
signal,
|
|
),
|
|
mediaType,
|
|
{
|
|
assetKind: `editor_generation_reference_${mediaType}`,
|
|
pathSegments: [
|
|
'editor',
|
|
'generation-references',
|
|
normalizedProjectId,
|
|
uploadId,
|
|
],
|
|
entityId: normalizedProjectId,
|
|
signal,
|
|
...(projectId?.trim()
|
|
? { metadata: { editor_project_id: projectId.trim() } }
|
|
: {}),
|
|
},
|
|
);
|
|
return uploaded.objectKey;
|
|
}
|
|
|
|
export async function resolveEditorGenerationMediaReference(
|
|
reference: EditorGenerationMediaReference,
|
|
mediaType: EditorMediaAssetUploadType,
|
|
projectId?: string | null,
|
|
options: EditorGenerationMediaReferenceOptions = {},
|
|
) {
|
|
throwIfEditorGenerationMediaUploadAborted(options.signal);
|
|
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, options.signal)
|
|
: source;
|
|
throwIfEditorGenerationMediaUploadAborted(options.signal);
|
|
return uploadEditorGenerationInlineMediaSource(
|
|
uploadSource,
|
|
mediaType,
|
|
projectId,
|
|
options.signal,
|
|
options.uploadId,
|
|
);
|
|
}
|
|
|
|
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;
|
|
|
|
function getCanvasCompletionPlaceholderSizeFromPlan({
|
|
sourceLayer,
|
|
outputSize,
|
|
}: {
|
|
sourceLayer: CanvasLayer;
|
|
outputSize: string | null | undefined;
|
|
}) {
|
|
const match = /^(\d+)x(\d+)$/i.exec(outputSize?.trim() ?? '');
|
|
if (!match) {
|
|
return {
|
|
width: sourceLayer.originalWidth,
|
|
height: sourceLayer.originalHeight,
|
|
};
|
|
}
|
|
const width = Number(match[1]);
|
|
const height = Number(match[2]);
|
|
return {
|
|
width:
|
|
Number.isFinite(width) && width > 0 ? width : sourceLayer.originalWidth,
|
|
height:
|
|
Number.isFinite(height) && height > 0
|
|
? height
|
|
: sourceLayer.originalHeight,
|
|
};
|
|
}
|
|
|
|
type GenerationSubmissionWorkflowOptions = {
|
|
layers: CanvasLayer[];
|
|
canvasSize: CanvasSize;
|
|
viewport: CanvasViewport;
|
|
layerCounterRef: MutableRefObject<number>;
|
|
quickEditPanel: QuickEditPanelState | null;
|
|
quickEditSourceLayer: CanvasLayer | null;
|
|
quickEditSelectionState: UiAssetExtractionState | null;
|
|
canvasGenerationDialogs: CanvasGenerationDialogState[];
|
|
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;
|
|
getCanvasGenerationDialogById: (
|
|
dialogId: string,
|
|
) => CanvasGenerationDialogState | undefined;
|
|
activeCanvasGenerationDialogId?: string | null;
|
|
backgroundMusicPromptAssist: BackgroundMusicPromptAssistController;
|
|
soundEffectPromptAssist: SoundEffectPromptAssistController;
|
|
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<SetStateAction<CanvasTool>>;
|
|
setActiveSidebarPanel: Dispatch<SetStateAction<SidebarPanel | null>>;
|
|
rememberImageModel: (imageModel: string) => void;
|
|
projectId?: string | null;
|
|
currentUserId?: string | null;
|
|
assetFolderId?: string | null;
|
|
upsertGeneratedAsset?: (asset: EditorAssetSnapshot) => void;
|
|
applyProjectSnapshot?: (project: EditorProjectSnapshot) => void;
|
|
onQueuedGenerationTask?: () => void;
|
|
onWalletBalanceMayHaveChanged?: () => void;
|
|
onGenerationWarning?: (message: string) => void;
|
|
};
|
|
|
|
async function normalizeGenerationReferenceImages<
|
|
T extends { referenceImageSrcs?: string[] },
|
|
>(
|
|
input: T,
|
|
references: CharacterReferenceImage[],
|
|
projectId?: string | null,
|
|
): Promise<T> {
|
|
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<typeof generateEditorVideo>[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,
|
|
) {
|
|
return findCanvasGenerationDialogRecords(project, dialogId).some(
|
|
isUnresolvedCanvasGenerationDialogRecord,
|
|
);
|
|
}
|
|
|
|
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,
|
|
) {
|
|
// 风格归一化和像素规整产生的通用 warning 可以与 sliceWarning 并存,
|
|
// 这里按“通用在前、拆分在后”拼接成单条提示,不允许其中任何一条被丢弃。
|
|
const commonReason = warning?.trim();
|
|
const sliceReason = sliceWarning?.trim();
|
|
const prefixedSliceReason = sliceReason
|
|
? `${EDITOR_SPRITESHEET_SLICE_WARNING_PREFIX}${sliceReason}`
|
|
: undefined;
|
|
if (commonReason && prefixedSliceReason) {
|
|
return `${commonReason} ${prefixedSliceReason}`;
|
|
}
|
|
return commonReason || prefixedSliceReason;
|
|
}
|
|
|
|
async function runEditorGenerationWithWalletRefresh<T>(
|
|
operation: Promise<T>,
|
|
onWalletBalanceMayHaveChanged?: () => void,
|
|
): Promise<T> {
|
|
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<ExternalGenerationJobStatusRecord | null> {
|
|
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,
|
|
getCanvasGenerationDialogById,
|
|
activeCanvasGenerationDialogId,
|
|
backgroundMusicPromptAssist,
|
|
soundEffectPromptAssist,
|
|
getGeneratingDialogPlaceholder,
|
|
appendCanvasLayersWithResources,
|
|
captureCanvasHistory,
|
|
updateSourceLayer,
|
|
selectSingleLayer,
|
|
fitLayers,
|
|
setActiveTool,
|
|
setActiveSidebarPanel,
|
|
rememberImageModel,
|
|
projectId,
|
|
currentUserId,
|
|
assetFolderId,
|
|
upsertGeneratedAsset,
|
|
applyProjectSnapshot,
|
|
onQueuedGenerationTask,
|
|
onWalletBalanceMayHaveChanged,
|
|
onGenerationWarning,
|
|
}: GenerationSubmissionWorkflowOptions) {
|
|
const currentBackgroundMusicSubmissionScopeRef = useRef({
|
|
currentUserId: currentUserId ?? null,
|
|
projectId: projectId ?? null,
|
|
activeCanvasGenerationDialogId,
|
|
version: 0,
|
|
mounted: false,
|
|
});
|
|
useLayoutEffect(() => {
|
|
const currentScope = currentBackgroundMusicSubmissionScopeRef.current;
|
|
const scopeChanged =
|
|
!Object.is(currentScope.currentUserId, currentUserId ?? null) ||
|
|
!Object.is(currentScope.projectId, projectId ?? null);
|
|
currentBackgroundMusicSubmissionScopeRef.current = {
|
|
currentUserId: currentUserId ?? null,
|
|
projectId: projectId ?? null,
|
|
activeCanvasGenerationDialogId,
|
|
version: scopeChanged ? currentScope.version + 1 : currentScope.version,
|
|
mounted: true,
|
|
};
|
|
return () => {
|
|
currentBackgroundMusicSubmissionScopeRef.current.mounted = false;
|
|
};
|
|
}, [activeCanvasGenerationDialogId, currentUserId, projectId]);
|
|
|
|
const isBackgroundMusicSubmissionUiTargetCurrent = useCallback(
|
|
(scope: {
|
|
currentUserId: string | null;
|
|
projectId: string | null;
|
|
dialogId: string;
|
|
scopeVersion: number;
|
|
}) => {
|
|
const currentScope = currentBackgroundMusicSubmissionScopeRef.current;
|
|
const currentDialog = getCanvasGenerationDialogById(scope.dialogId);
|
|
return (
|
|
currentScope.mounted &&
|
|
currentScope.version === scope.scopeVersion &&
|
|
Object.is(scope.currentUserId, currentScope.currentUserId) &&
|
|
Object.is(scope.projectId, currentScope.projectId) &&
|
|
currentDialog?.mode === 'audio-background-music'
|
|
);
|
|
},
|
|
[getCanvasGenerationDialogById],
|
|
);
|
|
|
|
const isSoundEffectSubmissionUiTargetCurrent = useCallback(
|
|
(scope: {
|
|
currentUserId: string | null;
|
|
projectId: string | null;
|
|
dialogId: string;
|
|
scopeVersion: number;
|
|
}) => {
|
|
const currentScope = currentBackgroundMusicSubmissionScopeRef.current;
|
|
const currentDialog = getCanvasGenerationDialogById(scope.dialogId);
|
|
return (
|
|
currentScope.mounted &&
|
|
currentScope.version === scope.scopeVersion &&
|
|
Object.is(scope.currentUserId, currentScope.currentUserId) &&
|
|
Object.is(scope.projectId, currentScope.projectId) &&
|
|
currentDialog?.mode === 'audio-sound-effect'
|
|
);
|
|
},
|
|
[getCanvasGenerationDialogById],
|
|
);
|
|
|
|
const isBackgroundMusicSubmissionAccountCurrent = useCallback(
|
|
(scope: { currentUserId: string | null }) => {
|
|
const currentScope = currentBackgroundMusicSubmissionScopeRef.current;
|
|
return (
|
|
currentScope.mounted &&
|
|
Object.is(scope.currentUserId, currentScope.currentUserId)
|
|
);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const isBackgroundMusicSubmissionProjectCurrent = useCallback(
|
|
(scope: { currentUserId: string | null; projectId: string | null }) => {
|
|
const currentScope = currentBackgroundMusicSubmissionScopeRef.current;
|
|
return (
|
|
currentScope.mounted &&
|
|
Object.is(scope.currentUserId, currentScope.currentUserId) &&
|
|
Object.is(scope.projectId, currentScope.projectId)
|
|
);
|
|
},
|
|
[],
|
|
);
|
|
|
|
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<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], { captureHistory: false });
|
|
}
|
|
},
|
|
[
|
|
addGeneratedLayersToCanvas,
|
|
canvasSize,
|
|
fitLayers,
|
|
layerCounterRef,
|
|
selectSingleLayer,
|
|
setActiveSidebarPanel,
|
|
setActiveTool,
|
|
setGenerateDialog,
|
|
updateCanvasGenerationDialogById,
|
|
hasCanvasGenerationDialogById,
|
|
viewport,
|
|
],
|
|
);
|
|
|
|
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<typeof createVideoResultLayer>[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<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,
|
|
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,
|
|
{
|
|
model: options.model ?? DEFAULT_IMAGE_MODEL,
|
|
imageSize: resolveUiAssetExtractionGenerationPlan(
|
|
options.marks?.length ?? 0,
|
|
).imageSize,
|
|
},
|
|
);
|
|
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;
|
|
}
|
|
setSubmittingIconDialog({
|
|
...canvasDialog,
|
|
iconDescriptions: submissionPlan.iconDescriptions,
|
|
status: 'generating',
|
|
composerOpen: false,
|
|
errorMessage: undefined,
|
|
});
|
|
|
|
try {
|
|
const canvasCompletionPlaceholder =
|
|
getGeneratingDialogPlaceholder(dialog);
|
|
const referenceImageSrcs = await Promise.all(
|
|
(dialog.generationReferences ?? []).map((reference) =>
|
|
resolveEditorGenerationMediaReference(
|
|
reference,
|
|
'image',
|
|
projectId,
|
|
),
|
|
),
|
|
);
|
|
const generated = await runEditorGenerationWithWalletRefresh(
|
|
generateEditorIconSpritesheet({
|
|
...submissionPlan.input,
|
|
...(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 (!isQuickEditSupportedLayer(quickEditSourceLayer)) {
|
|
setQuickEditPanel({
|
|
...quickEditPanel,
|
|
status: 'failed',
|
|
errorMessage: '当前素材类型不支持快速编辑',
|
|
});
|
|
return;
|
|
}
|
|
|
|
const quickEditResultTitle = resolveGenerationAssetLabel(
|
|
quickEditPanel.assetLabel,
|
|
`${quickEditSourceLayer.title} 快速编辑`,
|
|
);
|
|
const quickEditReferences: CharacterReferenceImage[] = [];
|
|
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 normalizedPrompt = quickEditPanel.prompt.trim() || '快速编辑图片';
|
|
let quickEditDialogId: string | undefined;
|
|
const shouldUseCanvasPlaceholder =
|
|
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 extraReferenceImageSrcs = 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 (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,
|
|
[createCanvasLayerReference(quickEditSourceLayer)],
|
|
{
|
|
model: videoModel,
|
|
aspectRatio: '16:9',
|
|
resolution: videoResolution,
|
|
durationSeconds: videoDurationSeconds,
|
|
sound: 'off',
|
|
},
|
|
);
|
|
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 =
|
|
quickEditSelectionState?.sourceLayerId === quickEditSourceLayer.id
|
|
? quickEditSelectionState.marks
|
|
: [];
|
|
const sourceReferenceId =
|
|
resolveRegisteredEditorReferenceId(quickEditSourceLayer);
|
|
let markedReferenceImageSrc: string | null = null;
|
|
if (quickEditSelectionMarks.length > 0) {
|
|
const renderSourceImageSrc = await resolveEditorImageReferenceDataUrl(
|
|
quickEditSourceLayer.objectKey?.trim() || quickEditSourceLayer.src,
|
|
);
|
|
const markedImageDataUrl =
|
|
await renderUiDesignAssetExtractionMarkedImage({
|
|
source: renderSourceImageSrc,
|
|
marks: quickEditSelectionMarks,
|
|
showIndexLabels: true,
|
|
});
|
|
markedReferenceImageSrc = await uploadEditorGenerationImageDataUrl(
|
|
markedImageDataUrl,
|
|
projectId,
|
|
);
|
|
}
|
|
const imageEditReferenceSrcs = [
|
|
...extraReferenceImageSrcs,
|
|
...(markedReferenceImageSrc ? [markedReferenceImageSrc] : []),
|
|
];
|
|
const generationInputs = buildQuickEditGenerationInputs(
|
|
'快速编辑提示词',
|
|
normalizedPrompt,
|
|
quickEditSourceLayer,
|
|
quickEditReferences,
|
|
{
|
|
model: normalizedQuickEditModel,
|
|
aspectRatio: quickEditAspectRatio,
|
|
imageSize: quickEditImageSize,
|
|
},
|
|
);
|
|
const generated = await runEditorGenerationWithWalletRefresh(
|
|
editEditorImage({
|
|
prompt: normalizedPrompt,
|
|
sourceReferenceId,
|
|
size: quickEditSize,
|
|
model: normalizedQuickEditModel,
|
|
aspectRatio: quickEditAspectRatio,
|
|
imageSize: quickEditImageSize,
|
|
...(imageEditReferenceSrcs.length
|
|
? { referenceImageSrcs: imageEditReferenceSrcs }
|
|
: {}),
|
|
projectId,
|
|
generationInputs,
|
|
assetFolderId,
|
|
assetLabel: quickEditResultTitle,
|
|
targetLayerId: quickEditSourceLayer.id,
|
|
}),
|
|
onWalletBalanceMayHaveChanged,
|
|
);
|
|
notifyEditorGenerationWarning(
|
|
generated.warning?.reason,
|
|
onGenerationWarning,
|
|
);
|
|
if (
|
|
await applyQueuedEditorGenerationProject(
|
|
generated,
|
|
projectId,
|
|
applyProjectSnapshot,
|
|
onQueuedGenerationTask,
|
|
onWalletBalanceMayHaveChanged,
|
|
onGenerationWarning,
|
|
)
|
|
) {
|
|
setQuickEditPanel(null);
|
|
setActiveTool('select');
|
|
setActiveSidebarPanel('layers');
|
|
return;
|
|
}
|
|
if (generated.project && applyProjectSnapshot) {
|
|
applyProjectSnapshot(generated.project);
|
|
setQuickEditPanel(null);
|
|
setActiveTool('select');
|
|
setActiveSidebarPanel('layers');
|
|
return;
|
|
}
|
|
applyQuickEditResultToSourceLayer(
|
|
generated,
|
|
quickEditSourceLayer,
|
|
generationInputs,
|
|
quickEditDialogId,
|
|
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),
|
|
});
|
|
}
|
|
}, [
|
|
addVideoResultLayer,
|
|
applyQuickEditResultToSourceLayer,
|
|
applyProjectSnapshot,
|
|
canvasGenerationDialogs,
|
|
canvasSize,
|
|
getGeneratingDialogPlaceholder,
|
|
layers,
|
|
openCanvasGenerationDialog,
|
|
projectId,
|
|
assetFolderId,
|
|
upsertGeneratedAsset,
|
|
quickEditPanel,
|
|
quickEditSourceLayer,
|
|
quickEditSelectionState,
|
|
selectSingleLayer,
|
|
setActiveSidebarPanel,
|
|
setActiveTool,
|
|
setQuickEditPanel,
|
|
updateCanvasGenerationDialogById,
|
|
viewport,
|
|
onGenerationWarning,
|
|
onQueuedGenerationTask,
|
|
onWalletBalanceMayHaveChanged,
|
|
]);
|
|
|
|
const resetProjectBackedGenerationDialog = useCallback(
|
|
(canvasDialog: CanvasGenerationDialogState | null) => {
|
|
if (!canvasDialog || !projectId) {
|
|
return false;
|
|
}
|
|
updateCanvasGenerationDialogById(canvasDialog.id, (currentDialog) =>
|
|
currentDialog.generatedLayerId
|
|
? currentDialog
|
|
: {
|
|
...currentDialog,
|
|
status: 'idle',
|
|
composerOpen: true,
|
|
errorMessage: undefined,
|
|
},
|
|
);
|
|
return true;
|
|
},
|
|
[projectId, updateCanvasGenerationDialogById],
|
|
);
|
|
|
|
const submitImageGeneration = useCallback(
|
|
async (dialog: GenerateDialogState) => {
|
|
const canvasDialog = isCanvasGenerationDialog(dialog) ? dialog : null;
|
|
const backgroundMusicDialog =
|
|
canvasDialog?.mode === 'audio-background-music' ? canvasDialog : null;
|
|
const soundEffectDialog =
|
|
canvasDialog?.mode === 'audio-sound-effect' ? canvasDialog : null;
|
|
if (dialog.mode === 'audio-background-music' && !backgroundMusicDialog) {
|
|
return;
|
|
}
|
|
if (dialog.mode === 'audio-sound-effect' && !soundEffectDialog) {
|
|
return;
|
|
}
|
|
const backgroundMusicClaim = backgroundMusicDialog
|
|
? backgroundMusicPromptAssist.beginSubmission(backgroundMusicDialog.id)
|
|
: null;
|
|
if (backgroundMusicDialog && !backgroundMusicClaim) {
|
|
return;
|
|
}
|
|
const soundEffectClaim = soundEffectDialog
|
|
? soundEffectPromptAssist.beginSubmission(soundEffectDialog.id)
|
|
: null;
|
|
if (soundEffectDialog && !soundEffectClaim) {
|
|
const validation = validateSoundEffectPrompt(soundEffectDialog.prompt);
|
|
if (!validation.ok) {
|
|
updateCanvasGenerationDialogById(
|
|
soundEffectDialog.id,
|
|
(currentDialog) =>
|
|
currentDialog.mode === 'audio-sound-effect'
|
|
? {
|
|
...currentDialog,
|
|
prompt: validation.prompt,
|
|
status: 'failed',
|
|
composerOpen: true,
|
|
errorMessage:
|
|
validation.reason === 'empty'
|
|
? '音效描述不能为空'
|
|
: '音效描述不能超过 2048 个字符',
|
|
}
|
|
: currentDialog,
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
const backgroundMusicSubmission =
|
|
backgroundMusicDialog && backgroundMusicClaim
|
|
? {
|
|
currentUserId: currentUserId ?? null,
|
|
projectId: projectId ?? null,
|
|
dialogId: backgroundMusicDialog.id,
|
|
operation: backgroundMusicClaim.operation,
|
|
prompt: backgroundMusicClaim.prompt,
|
|
scopeVersion:
|
|
currentBackgroundMusicSubmissionScopeRef.current.version,
|
|
}
|
|
: null;
|
|
const soundEffectSubmission =
|
|
soundEffectDialog && soundEffectClaim
|
|
? {
|
|
currentUserId: currentUserId ?? null,
|
|
projectId: projectId ?? null,
|
|
dialogId: soundEffectDialog.id,
|
|
...soundEffectClaim,
|
|
scopeVersion:
|
|
currentBackgroundMusicSubmissionScopeRef.current.version,
|
|
}
|
|
: null;
|
|
const normalizedPrompt =
|
|
backgroundMusicSubmission?.prompt ??
|
|
soundEffectSubmission?.prompt ??
|
|
resolveImageGenerationDialogPrompt(dialog);
|
|
if (
|
|
!backgroundMusicSubmission &&
|
|
!soundEffectSubmission &&
|
|
canvasDialog
|
|
) {
|
|
updateCanvasGenerationDialogById(canvasDialog.id, (currentDialog) => ({
|
|
...currentDialog,
|
|
prompt: normalizedPrompt,
|
|
status: 'generating',
|
|
composerOpen: false,
|
|
}));
|
|
} else if (!backgroundMusicSubmission && !soundEffectSubmission) {
|
|
setGenerateDialog({
|
|
...dialog,
|
|
prompt: normalizedPrompt,
|
|
status: 'generating',
|
|
composerOpen: dialog.mode === 'edit',
|
|
});
|
|
}
|
|
|
|
let backgroundMusicSubmissionAccepted = false;
|
|
let backgroundMusicSubmissionUiOwned = false;
|
|
let backgroundMusicQueueAcceptedWhileCurrentAndOpen = false;
|
|
let soundEffectSubmissionAccepted = false;
|
|
let soundEffectSubmissionUiOwned = false;
|
|
let soundEffectQueueAcceptedWhileCurrentAndOpen = false;
|
|
try {
|
|
const dialogForSubmission = soundEffectSubmission
|
|
? {
|
|
...dialog,
|
|
prompt: soundEffectSubmission.prompt,
|
|
soundDurationMode: soundEffectSubmission.durationMode,
|
|
soundDurationSeconds: soundEffectSubmission.manualDurationSeconds,
|
|
soundLoop: soundEffectSubmission.loop,
|
|
}
|
|
: dialog;
|
|
const submissionPlan = buildImageGenerationSubmissionPlan({
|
|
dialog: dialogForSubmission,
|
|
layers,
|
|
nextGeneratedIndex: layerCounterRef.current + 1,
|
|
canonicalBackgroundMusicPrompt: backgroundMusicSubmission?.prompt,
|
|
});
|
|
if (submissionPlan.kind === 'edit') {
|
|
const sourceReferenceId = resolveRegisteredEditorReferenceId(
|
|
submissionPlan.sourceLayer,
|
|
);
|
|
const editPlaceholderSize =
|
|
getCanvasCompletionPlaceholderSizeFromPlan({
|
|
sourceLayer: submissionPlan.sourceLayer,
|
|
outputSize: submissionPlan.editInput.size,
|
|
});
|
|
const canvasCompletionPlaceholder =
|
|
getGeneratingDialogPlaceholder(dialog);
|
|
const editCanvasCompletionPlaceholder =
|
|
canvasCompletionPlaceholder ??
|
|
buildRightSideCanvasCompletionPlaceholder(
|
|
submissionPlan.sourceLayer,
|
|
{
|
|
width: editPlaceholderSize.width,
|
|
height: editPlaceholderSize.height,
|
|
originalWidth: editPlaceholderSize.width,
|
|
originalHeight: editPlaceholderSize.height,
|
|
},
|
|
);
|
|
const generated = await runEditorGenerationWithWalletRefresh(
|
|
editEditorImage({
|
|
prompt: submissionPlan.normalizedPrompt,
|
|
sourceReferenceId,
|
|
...submissionPlan.editInput,
|
|
projectId,
|
|
generationInputs: submissionPlan.generationInputs,
|
|
assetFolderId,
|
|
assetLabel: submissionPlan.resultTitle,
|
|
...(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 sourceReferenceId = resolveRegisteredEditorReferenceId(
|
|
submissionPlan.sourceLayer,
|
|
);
|
|
const quickEditSelectionMarks =
|
|
quickEditSelectionState?.sourceLayerId ===
|
|
submissionPlan.sourceLayer.id
|
|
? quickEditSelectionState.marks
|
|
: [];
|
|
let markedReferenceImageSrc: string | null = null;
|
|
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,
|
|
});
|
|
markedReferenceImageSrc = await uploadEditorGenerationImageDataUrl(
|
|
markedImageDataUrl,
|
|
projectId,
|
|
);
|
|
}
|
|
const normalizedReferenceImageSrcs = await Promise.all(
|
|
(submissionPlan.editInput.referenceImageSrcs ?? []).map(
|
|
(referenceImageSrc, index) =>
|
|
resolveEditorGenerationMediaReference(
|
|
dialog.generationReferences?.[index]
|
|
? {
|
|
...dialog.generationReferences[index],
|
|
src: referenceImageSrc,
|
|
}
|
|
: { src: referenceImageSrc },
|
|
'image',
|
|
projectId,
|
|
),
|
|
),
|
|
);
|
|
const referenceImageSrcs = [
|
|
...normalizedReferenceImageSrcs,
|
|
...(markedReferenceImageSrc ? [markedReferenceImageSrc] : []),
|
|
];
|
|
const generated = await runEditorGenerationWithWalletRefresh(
|
|
editEditorImage({
|
|
prompt: submissionPlan.normalizedPrompt,
|
|
sourceReferenceId,
|
|
...submissionPlan.editInput,
|
|
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
|
|
projectId,
|
|
generationInputs: submissionPlan.result.generationInputs,
|
|
assetFolderId,
|
|
assetLabel: submissionPlan.result.title,
|
|
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 === 'icon-spec') {
|
|
const iconSpecInput = submissionPlan.input;
|
|
const canvasCompletionPlaceholder =
|
|
getGeneratingDialogPlaceholder(dialog);
|
|
const generated = await runEditorGenerationWithWalletRefresh(
|
|
generateEditorIconSpec({
|
|
...iconSpecInput,
|
|
projectId,
|
|
assetFolderId,
|
|
assetLabel: submissionPlan.result.title,
|
|
...(projectId && canvasCompletionPlaceholder
|
|
? {
|
|
canvasCompletion: {
|
|
dialogId: canvasDialog?.id,
|
|
title: submissionPlan.result.title,
|
|
placeholder: canvasCompletionPlaceholder,
|
|
},
|
|
}
|
|
: {}),
|
|
}),
|
|
onWalletBalanceMayHaveChanged,
|
|
);
|
|
notifyEditorGenerationWarning(
|
|
generated.warning?.reason,
|
|
onGenerationWarning,
|
|
);
|
|
if (
|
|
await applyQueuedEditorGenerationProject(
|
|
generated,
|
|
projectId,
|
|
applyProjectSnapshot,
|
|
onQueuedGenerationTask,
|
|
onWalletBalanceMayHaveChanged,
|
|
onGenerationWarning,
|
|
canvasDialog?.id,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
if (generated.project && applyProjectSnapshot) {
|
|
applyProjectSnapshot(generated.project);
|
|
if (generated.asset) {
|
|
upsertGeneratedAsset?.(generated.asset);
|
|
}
|
|
return;
|
|
}
|
|
if (resetProjectBackedGenerationDialog(canvasDialog)) {
|
|
return;
|
|
}
|
|
addGeneratedResultLayer(generated, {
|
|
frame: canvasCompletionPlaceholder,
|
|
assetKind: submissionPlan.result.assetKind,
|
|
title: submissionPlan.result.title,
|
|
dialogId: canvasDialog?.id,
|
|
generationInputs:
|
|
generated.asset?.generationInputs ??
|
|
submissionPlan.result.generationInputs,
|
|
});
|
|
} 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;
|
|
}
|
|
if (resetProjectBackedGenerationDialog(canvasDialog)) {
|
|
return;
|
|
}
|
|
addVideoResultLayer(
|
|
generated,
|
|
submissionPlan.result.title,
|
|
submissionPlan.result.generationInputs,
|
|
canvasCompletionPlaceholder,
|
|
canvasDialog?.id,
|
|
);
|
|
} else if (submissionPlan.kind === 'audio') {
|
|
const canvasCompletionPlaceholder =
|
|
getGeneratingDialogPlaceholder(dialog);
|
|
// 直连回包的钱包刷新由共享 wrapper 触发,而 wrapper 不持有本次 submission scope。
|
|
// 这里补上与排队路径和 BGM 分支同一道账号门禁:切号后迟到的旧回包不得刷新新账号钱包。
|
|
const refreshSoundEffectWalletBalance =
|
|
soundEffectSubmission && onWalletBalanceMayHaveChanged
|
|
? () => {
|
|
if (
|
|
isBackgroundMusicSubmissionAccountCurrent(
|
|
soundEffectSubmission,
|
|
)
|
|
) {
|
|
onWalletBalanceMayHaveChanged();
|
|
}
|
|
}
|
|
: onWalletBalanceMayHaveChanged;
|
|
const generated =
|
|
submissionPlan.audioKind === 'sound-effect'
|
|
? await runEditorGenerationWithWalletRefresh(
|
|
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,
|
|
},
|
|
}
|
|
: {}),
|
|
}),
|
|
refreshSoundEffectWalletBalance,
|
|
)
|
|
: await 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,
|
|
},
|
|
}
|
|
: {}),
|
|
});
|
|
const isBackgroundMusic =
|
|
submissionPlan.audioKind === 'background-music';
|
|
const isSoundEffect = submissionPlan.audioKind === 'sound-effect';
|
|
const backgroundMusicQueueState = isBackgroundMusic
|
|
? queuedStateFromResponse(generated)
|
|
: null;
|
|
const soundEffectQueueState = isSoundEffect
|
|
? queuedStateFromResponse(generated)
|
|
: null;
|
|
if (isBackgroundMusic && backgroundMusicSubmission) {
|
|
backgroundMusicSubmissionAccepted = true;
|
|
const currentScope =
|
|
currentBackgroundMusicSubmissionScopeRef.current;
|
|
const currentDialog = getCanvasGenerationDialogById(
|
|
backgroundMusicSubmission.dialogId,
|
|
);
|
|
backgroundMusicQueueAcceptedWhileCurrentAndOpen =
|
|
Boolean(backgroundMusicQueueState) &&
|
|
currentScope.activeCanvasGenerationDialogId ===
|
|
backgroundMusicSubmission.dialogId &&
|
|
currentDialog?.composerOpen !== false;
|
|
backgroundMusicSubmissionUiOwned =
|
|
backgroundMusicPromptAssist.finishSubmission({
|
|
operation: backgroundMusicSubmission.operation,
|
|
accepted: true,
|
|
});
|
|
if (
|
|
backgroundMusicQueueState &&
|
|
backgroundMusicSubmissionUiOwned &&
|
|
isBackgroundMusicSubmissionUiTargetCurrent(
|
|
backgroundMusicSubmission,
|
|
)
|
|
) {
|
|
updateCanvasGenerationDialogById(
|
|
backgroundMusicSubmission.dialogId,
|
|
(currentDialog) =>
|
|
currentDialog.mode === 'audio-background-music'
|
|
? {
|
|
...currentDialog,
|
|
prompt: backgroundMusicSubmission.prompt,
|
|
status: 'generating',
|
|
composerOpen: false,
|
|
errorMessage: undefined,
|
|
}
|
|
: currentDialog,
|
|
);
|
|
}
|
|
if (
|
|
!backgroundMusicQueueState &&
|
|
isBackgroundMusicSubmissionAccountCurrent(
|
|
backgroundMusicSubmission,
|
|
)
|
|
) {
|
|
notifyWalletBalanceMayHaveChanged(onWalletBalanceMayHaveChanged);
|
|
}
|
|
}
|
|
if (isSoundEffect && soundEffectSubmission) {
|
|
soundEffectSubmissionAccepted = true;
|
|
const currentScope =
|
|
currentBackgroundMusicSubmissionScopeRef.current;
|
|
const currentDialog = getCanvasGenerationDialogById(
|
|
soundEffectSubmission.dialogId,
|
|
);
|
|
soundEffectQueueAcceptedWhileCurrentAndOpen =
|
|
Boolean(soundEffectQueueState) &&
|
|
currentScope.activeCanvasGenerationDialogId ===
|
|
soundEffectSubmission.dialogId &&
|
|
currentDialog?.composerOpen !== false;
|
|
soundEffectSubmissionUiOwned =
|
|
soundEffectPromptAssist.finishSubmission({
|
|
operation: soundEffectSubmission.operation,
|
|
accepted: true,
|
|
});
|
|
if (
|
|
soundEffectQueueState &&
|
|
soundEffectSubmissionUiOwned &&
|
|
isSoundEffectSubmissionUiTargetCurrent(soundEffectSubmission)
|
|
) {
|
|
updateCanvasGenerationDialogById(
|
|
soundEffectSubmission.dialogId,
|
|
(currentDialog) =>
|
|
currentDialog.mode === 'audio-sound-effect'
|
|
? {
|
|
...currentDialog,
|
|
prompt: soundEffectSubmission.prompt,
|
|
soundDurationMode: soundEffectSubmission.durationMode,
|
|
soundDurationSeconds:
|
|
soundEffectSubmission.manualDurationSeconds,
|
|
soundLoop: soundEffectSubmission.loop,
|
|
status: 'generating',
|
|
composerOpen: false,
|
|
errorMessage: undefined,
|
|
}
|
|
: currentDialog,
|
|
);
|
|
}
|
|
}
|
|
if (
|
|
await applyQueuedEditorGenerationProject(
|
|
generated,
|
|
projectId,
|
|
backgroundMusicSubmission && applyProjectSnapshot
|
|
? (projectSnapshot) => {
|
|
if (
|
|
backgroundMusicSubmissionUiOwned &&
|
|
isBackgroundMusicSubmissionUiTargetCurrent(
|
|
backgroundMusicSubmission,
|
|
)
|
|
) {
|
|
applyProjectSnapshot(projectSnapshot);
|
|
}
|
|
}
|
|
: soundEffectSubmission && applyProjectSnapshot
|
|
? (projectSnapshot) => {
|
|
if (
|
|
soundEffectSubmissionUiOwned &&
|
|
isSoundEffectSubmissionUiTargetCurrent(
|
|
soundEffectSubmission,
|
|
)
|
|
) {
|
|
applyProjectSnapshot(projectSnapshot);
|
|
}
|
|
}
|
|
: applyProjectSnapshot,
|
|
backgroundMusicSubmission && onQueuedGenerationTask
|
|
? () => {
|
|
if (
|
|
isBackgroundMusicSubmissionProjectCurrent(
|
|
backgroundMusicSubmission,
|
|
)
|
|
) {
|
|
onQueuedGenerationTask();
|
|
}
|
|
}
|
|
: soundEffectSubmission && onQueuedGenerationTask
|
|
? () => {
|
|
if (
|
|
isBackgroundMusicSubmissionProjectCurrent(
|
|
soundEffectSubmission,
|
|
)
|
|
) {
|
|
onQueuedGenerationTask();
|
|
}
|
|
}
|
|
: onQueuedGenerationTask,
|
|
backgroundMusicSubmission && onWalletBalanceMayHaveChanged
|
|
? () => {
|
|
if (
|
|
isBackgroundMusicSubmissionAccountCurrent(
|
|
backgroundMusicSubmission,
|
|
)
|
|
) {
|
|
onWalletBalanceMayHaveChanged();
|
|
}
|
|
}
|
|
: soundEffectSubmission && onWalletBalanceMayHaveChanged
|
|
? () => {
|
|
if (
|
|
isBackgroundMusicSubmissionAccountCurrent(
|
|
soundEffectSubmission,
|
|
)
|
|
) {
|
|
onWalletBalanceMayHaveChanged();
|
|
}
|
|
}
|
|
: onWalletBalanceMayHaveChanged,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
if (
|
|
backgroundMusicSubmission &&
|
|
(!backgroundMusicSubmissionUiOwned ||
|
|
!isBackgroundMusicSubmissionUiTargetCurrent(
|
|
backgroundMusicSubmission,
|
|
))
|
|
) {
|
|
return;
|
|
}
|
|
if (
|
|
soundEffectSubmission &&
|
|
(!soundEffectSubmissionUiOwned ||
|
|
!isSoundEffectSubmissionUiTargetCurrent(soundEffectSubmission))
|
|
) {
|
|
return;
|
|
}
|
|
if (generated.project && applyProjectSnapshot) {
|
|
applyProjectSnapshot(generated.project);
|
|
if (generated.asset) {
|
|
upsertGeneratedAsset?.(generated.asset);
|
|
}
|
|
return;
|
|
}
|
|
if (resetProjectBackedGenerationDialog(canvasDialog)) {
|
|
return;
|
|
}
|
|
addAudioResultLayer(
|
|
generated,
|
|
submissionPlan.result.title,
|
|
submissionPlan.result.generationInputs,
|
|
canvasCompletionPlaceholder,
|
|
canvasDialog?.id,
|
|
);
|
|
} else if (submissionPlan.kind === 'scene') {
|
|
const sceneGenerationInput = await normalizeGenerationReferenceImages(
|
|
submissionPlan.input,
|
|
resolveImageGenerationDialogReferences(dialog),
|
|
projectId,
|
|
);
|
|
const canvasCompletionPlaceholder =
|
|
getGeneratingDialogPlaceholder(dialog);
|
|
const generated = await runEditorGenerationWithWalletRefresh(
|
|
generateEditorScene({
|
|
...sceneGenerationInput,
|
|
projectId,
|
|
generationInputs: submissionPlan.input.generationInputs,
|
|
assetFolderId,
|
|
assetLabel: submissionPlan.result.title,
|
|
...(projectId && canvasCompletionPlaceholder
|
|
? {
|
|
canvasCompletion: {
|
|
dialogId: canvasDialog?.id,
|
|
title: submissionPlan.result.title,
|
|
placeholder: canvasCompletionPlaceholder,
|
|
},
|
|
}
|
|
: {}),
|
|
}),
|
|
onWalletBalanceMayHaveChanged,
|
|
);
|
|
notifyEditorGenerationWarning(
|
|
generated.warning?.reason,
|
|
onGenerationWarning,
|
|
);
|
|
if (
|
|
await applyQueuedEditorGenerationProject(
|
|
generated,
|
|
projectId,
|
|
applyProjectSnapshot,
|
|
onQueuedGenerationTask,
|
|
onWalletBalanceMayHaveChanged,
|
|
onGenerationWarning,
|
|
canvasDialog?.id,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
if (submissionPlan.rememberImageModel) {
|
|
rememberImageModel(submissionPlan.rememberImageModel);
|
|
}
|
|
if (generated.project && applyProjectSnapshot) {
|
|
applyProjectSnapshot(generated.project);
|
|
if (generated.asset) upsertGeneratedAsset?.(generated.asset);
|
|
return;
|
|
}
|
|
if (resetProjectBackedGenerationDialog(canvasDialog)) return;
|
|
addGeneratedResultLayer(generated, {
|
|
frame: canvasCompletionPlaceholder,
|
|
assetKind: submissionPlan.result.assetKind,
|
|
title: submissionPlan.result.title,
|
|
dialogId: canvasDialog?.id,
|
|
generationInputs: submissionPlan.result.generationInputs,
|
|
});
|
|
} else {
|
|
const imageGenerationInput = await normalizeGenerationReferenceImages(
|
|
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 (resetProjectBackedGenerationDialog(canvasDialog)) {
|
|
return;
|
|
}
|
|
addGeneratedResultLayer(generated, {
|
|
frame: canvasCompletionPlaceholder,
|
|
assetKind: submissionPlan.result.assetKind,
|
|
title: resultTitle,
|
|
dialogId: canvasDialog?.id,
|
|
generationInputs: submissionPlan.result.generationInputs,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
if (backgroundMusicSubmission) {
|
|
if (!backgroundMusicSubmissionAccepted) {
|
|
backgroundMusicSubmissionUiOwned =
|
|
backgroundMusicPromptAssist.finishSubmission({
|
|
operation: backgroundMusicSubmission.operation,
|
|
accepted: false,
|
|
});
|
|
if (
|
|
isBackgroundMusicSubmissionAccountCurrent(
|
|
backgroundMusicSubmission,
|
|
)
|
|
) {
|
|
notifyWalletBalanceMayHaveChanged(onWalletBalanceMayHaveChanged);
|
|
}
|
|
}
|
|
if (
|
|
backgroundMusicSubmissionUiOwned &&
|
|
isBackgroundMusicSubmissionUiTargetCurrent(
|
|
backgroundMusicSubmission,
|
|
)
|
|
) {
|
|
const currentScope =
|
|
currentBackgroundMusicSubmissionScopeRef.current;
|
|
const currentDialog = getCanvasGenerationDialogById(
|
|
backgroundMusicSubmission.dialogId,
|
|
);
|
|
const shouldReopenComposer =
|
|
currentScope.activeCanvasGenerationDialogId ===
|
|
backgroundMusicSubmission.dialogId &&
|
|
(currentDialog?.composerOpen !== false ||
|
|
backgroundMusicQueueAcceptedWhileCurrentAndOpen);
|
|
updateCanvasGenerationDialogById(
|
|
backgroundMusicSubmission.dialogId,
|
|
(latestDialog) =>
|
|
latestDialog.mode === 'audio-background-music'
|
|
? {
|
|
...latestDialog,
|
|
prompt: backgroundMusicSubmission.prompt,
|
|
status: 'failed',
|
|
composerOpen: shouldReopenComposer
|
|
? true
|
|
: latestDialog.composerOpen,
|
|
errorMessage: resolveImageGenerationErrorMessage(error),
|
|
}
|
|
: latestDialog,
|
|
);
|
|
}
|
|
} else if (soundEffectSubmission) {
|
|
if (!soundEffectSubmissionAccepted) {
|
|
soundEffectSubmissionUiOwned =
|
|
soundEffectPromptAssist.finishSubmission({
|
|
operation: soundEffectSubmission.operation,
|
|
accepted: false,
|
|
});
|
|
}
|
|
if (
|
|
soundEffectSubmissionUiOwned &&
|
|
isSoundEffectSubmissionUiTargetCurrent(soundEffectSubmission)
|
|
) {
|
|
const currentScope =
|
|
currentBackgroundMusicSubmissionScopeRef.current;
|
|
const currentDialog = getCanvasGenerationDialogById(
|
|
soundEffectSubmission.dialogId,
|
|
);
|
|
const shouldReopenComposer =
|
|
currentScope.activeCanvasGenerationDialogId ===
|
|
soundEffectSubmission.dialogId &&
|
|
(currentDialog?.composerOpen !== false ||
|
|
soundEffectQueueAcceptedWhileCurrentAndOpen);
|
|
updateCanvasGenerationDialogById(
|
|
soundEffectSubmission.dialogId,
|
|
(latestDialog) =>
|
|
latestDialog.mode === 'audio-sound-effect'
|
|
? {
|
|
...latestDialog,
|
|
prompt: soundEffectSubmission.prompt,
|
|
soundDurationMode: soundEffectSubmission.durationMode,
|
|
soundDurationSeconds:
|
|
soundEffectSubmission.manualDurationSeconds,
|
|
soundLoop: soundEffectSubmission.loop,
|
|
status: 'failed',
|
|
composerOpen: shouldReopenComposer
|
|
? true
|
|
: latestDialog.composerOpen,
|
|
errorMessage: resolveImageGenerationErrorMessage(error),
|
|
}
|
|
: latestDialog,
|
|
);
|
|
}
|
|
} else 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,
|
|
backgroundMusicPromptAssist,
|
|
currentUserId,
|
|
getCanvasGenerationDialogById,
|
|
getGeneratingDialogPlaceholder,
|
|
isBackgroundMusicSubmissionAccountCurrent,
|
|
isBackgroundMusicSubmissionProjectCurrent,
|
|
isBackgroundMusicSubmissionUiTargetCurrent,
|
|
isSoundEffectSubmissionUiTargetCurrent,
|
|
layerCounterRef,
|
|
layers,
|
|
quickEditSelectionState,
|
|
rememberImageModel,
|
|
resetProjectBackedGenerationDialog,
|
|
projectId,
|
|
assetFolderId,
|
|
applyProjectSnapshot,
|
|
setGenerateDialog,
|
|
soundEffectPromptAssist,
|
|
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 canvasDialog = characterAnimationDialog;
|
|
try {
|
|
const submissionPlan = buildCharacterAnimationSubmissionPlan({
|
|
panel: characterAnimationPanel,
|
|
sourceLayer: characterAnimationSourceLayer,
|
|
});
|
|
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);
|
|
}
|
|
const generationInputs = buildCharacterAnimationGenerationInputs(
|
|
submissionPlan.promptText,
|
|
characterAnimationSourceLayer,
|
|
{
|
|
resolution: characterAnimationPanel.resolution,
|
|
ratio: characterAnimationPanel.ratio,
|
|
frameCount: characterAnimationPanel.frameCount,
|
|
durationSeconds: characterAnimationPanel.durationSeconds,
|
|
},
|
|
);
|
|
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) {
|
|
if (projectId && !result.resource) {
|
|
throw new Error('角色动作画布完成缺少正式项目资源。');
|
|
}
|
|
if (!projectId && !result.asset) {
|
|
throw new Error('角色动作生成缺少正式账号素材。');
|
|
}
|
|
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: characterAnimationPanel.promptText.trim(),
|
|
characterAnimationResolution: characterAnimationPanel.resolution,
|
|
characterAnimationRatio: characterAnimationPanel.ratio,
|
|
characterAnimationFrameCount: characterAnimationPanel.frameCount,
|
|
characterAnimationDurationSeconds:
|
|
characterAnimationPanel.durationSeconds,
|
|
status: 'failed',
|
|
composerOpen: true,
|
|
errorMessage: resolveImageGenerationErrorMessage(
|
|
error,
|
|
'生成角色动画失败',
|
|
),
|
|
}));
|
|
return;
|
|
}
|
|
setCharacterAnimationPanel((currentPanel) =>
|
|
currentPanel
|
|
? {
|
|
...currentPanel,
|
|
promptText: characterAnimationPanel.promptText.trim(),
|
|
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,
|
|
],
|
|
);
|
|
}
|