import { EDITOR_SOUND_EFFECT_MODEL } from '../../../packages/shared/src/contracts/editorAudio'; import { CUSTOM_EDITOR_SCENE_STYLE_PRESET, DEFAULT_EDITOR_SCENE_STYLE_PRESET, } from '../../../packages/shared/src/contracts/editorScene'; import type { EditorBackgroundMusicGenerationInput, EditorCharacterAnimationGenerationInput, EditorIconSpecGenerationInput, EditorIconSpritesheetGenerationInput, EditorImageEditInput, EditorImageGenerationInput, EditorSceneGenerationInput, EditorSoundEffectGenerationInput, EditorVideoGenerationInput, } from '../../services/image-editor/editorProjectClient'; import { EDITOR_ICON_DESCRIPTION_MAX_CHARS } from '../../services/image-editor/editorProjectClient'; import type { CanvasGenerationInputs, CanvasLayer, CharacterAnimationPanelState, GenerateDialogState, } from './ImageCanvasEditorTypes'; import { buildBackgroundMusicGenerationInputs, buildCharacterGenerationInputs, buildEditGenerationInputs, buildIconGenerationInputs, buildImageGenerationInputs, buildPublicationMaterialsGenerationInputs, buildPublicationMaterialsGenerationPrompt, buildPublicationMaterialsPrompt, buildQuickEditGenerationInputs, buildSceneGenerationInputs, buildSoundEffectGenerationInputs, buildSpecGenerationInputs, buildSpecPrompt, buildUiDesignGenerationInputs, buildVideoGenerationInputs, CHARACTER_ANIMATION_MODEL, DEFAULT_EDITOR_BGFILTER_SEG_MODEL, DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR, DEFAULT_SOUND_EFFECT_DURATION_SECONDS, DEFAULT_SPEC_FORM_VALUES, DEFAULT_VIDEO_ASPECT_RATIO, DEFAULT_VIDEO_DURATION_SECONDS, DEFAULT_VIDEO_MODEL, DEFAULT_VIDEO_SOUND, DEFAULT_VIDEO_WEB_SEARCH_ENABLED, IMAGE_MODEL_GPT_IMAGE_2, normalizeEditorImageModel, resolveCharacterAnimationSourceImageSrc, SEEDANCE_VIDEO_REFERENCE_LIMITS, SPEC_GENERATION_ASPECT_RATIO, SPEC_GENERATION_IMAGE_SIZE, SPEC_GENERATION_MODEL, SPEC_GENERATION_SIZE, SPEC_TYPE_LABEL, } from './ImageCanvasGenerationModel'; import { getPublicationMaterialsWorkflow } from './ImageCanvasPublicationMaterialsModel'; import { validateSoundEffectPrompt } from './ImageCanvasSoundEffectPromptModel'; type ImageGenerationSubmissionOptions = { dialog: GenerateDialogState; layers: CanvasLayer[]; nextGeneratedIndex: number; canonicalBackgroundMusicPrompt?: string; }; export const EDITOR_SCENE_CONTENT_REQUIRED_ERROR = '请填写画面内容'; export const EDITOR_SCENE_CUSTOM_STYLE_REQUIRED_ERROR = '请填写自定义画风'; export const EDITOR_GENERATED_ASSET_LABEL_MAX_CHARS = 80; export function resolveGenerationAssetLabel( assetLabel: string | null | undefined, fallback: string, ) { const normalized = assetLabel?.trim(); return normalized ? Array.from(normalized) .slice(0, EDITOR_GENERATED_ASSET_LABEL_MAX_CHARS) .join('') : fallback; } function isSeedanceVideoModel(model: string) { return model === 'seedance2.0' || model === 'seedance2.0-fast'; } export function isSeedanceEditorVideoModel(model: string) { return isSeedanceVideoModel(model); } function normalizeGptImageSize(imageSize: string | null | undefined) { return imageSize?.trim().toUpperCase() === '2K' ? '2K' : '1K'; } export function resolveImageReferenceSubmissionSource(reference: { src: string; objectKey?: string | null; }) { return reference.objectKey?.trim() || reference.src; } export function resolveRegisteredEditorReferenceId(reference: { resourceId?: string | null; sourceAssetId?: string | null; }) { const resourceId = reference.resourceId?.trim(); if ( resourceId && !resourceId.startsWith('local-resource-') && !resourceId.startsWith('generation-dialog:') ) { return resourceId; } const assetId = reference.sourceAssetId?.trim(); if (assetId) { return assetId; } throw new Error('参考图尚未登记为项目资源或素材,请重新选择或上传后再试'); } function buildSeedanceVideoReferenceInput( references: NonNullable, ) { const resolveReferenceSource = (reference: { src: string; objectKey?: string | null; }) => reference.objectKey?.trim() || reference.src; const imageReferences = references.filter( (reference) => (reference.mediaType ?? 'image') === 'image', ); const videoReferences = references.filter( (reference) => reference.mediaType === 'video', ); const audioReferences = references.filter( (reference) => reference.mediaType === 'audio', ); if (imageReferences.length > SEEDANCE_VIDEO_REFERENCE_LIMITS.image) { throw new Error('参考图片最多 9 张'); } if (videoReferences.length > SEEDANCE_VIDEO_REFERENCE_LIMITS.video) { throw new Error('参考视频最多 3 个'); } if (audioReferences.length > SEEDANCE_VIDEO_REFERENCE_LIMITS.audio) { throw new Error('参考音频最多 3 个'); } if ( audioReferences.length && !imageReferences.length && !videoReferences.length ) { throw new Error('参考音频必须搭配参考图片或参考视频'); } return { referenceImageSrcs: imageReferences.map(resolveReferenceSource), referenceVideoSrcs: videoReferences.map(resolveReferenceSource), referenceAudioSrcs: audioReferences.map(resolveReferenceSource), }; } // 使用 map 避免嵌套 if else const DEFAULT_GENERATION_PROMPTS = new Map< GenerateDialogState['mode'], string >([ ['edit', '修改当前图片'], ['audio-sound-effect', '游戏音效'], ['audio-background-music', '游戏背景音乐'], ]); const REQUIRED_GENERATION_PROMPT_MODES = new Set< GenerateDialogState['mode'] >(['scene']); function getDialogDefaultPrompt(mode: GenerateDialogState['mode']) { return DEFAULT_GENERATION_PROMPTS.get(mode) ?? 'AI 生成图片'; } export function resolveImageGenerationDialogPrompt( dialog: GenerateDialogState, ) { const prompt = dialog.prompt.trim(); if (prompt || REQUIRED_GENERATION_PROMPT_MODES.has(dialog.mode)) { return prompt; } return getDialogDefaultPrompt(dialog.mode); } export type ImageGenerationSubmissionPlan = | { kind: 'edit'; normalizedPrompt: string; sourceLayer: CanvasLayer; resultTitle: string; generationInputs: CanvasGenerationInputs; } | { kind: 'image'; normalizedPrompt: string; input: EditorImageGenerationInput; result: { assetKind?: CanvasLayer['assetKind']; title?: string; generationInputs: CanvasGenerationInputs; }; rememberImageModel?: string; } | { kind: 'scene'; normalizedPrompt: string; input: EditorSceneGenerationInput; result: { assetKind: 'scene'; title: string; generationInputs: CanvasGenerationInputs; }; rememberImageModel?: string; } | { kind: 'icon-spec'; normalizedPrompt: string; input: EditorIconSpecGenerationInput; result: { assetKind: 'icon-spec'; title: string; generationInputs: CanvasGenerationInputs; }; } | { kind: 'quick-edit'; normalizedPrompt: string; sourceLayer: CanvasLayer; editInput: Pick; result: { title: string; assetKind?: CanvasLayer['assetKind']; generationInputs: CanvasGenerationInputs; }; rememberImageModel?: string; } | { kind: 'video'; normalizedPrompt: string; input: EditorVideoGenerationInput; result: { title: string; generationInputs: CanvasGenerationInputs; }; } | { kind: 'audio'; audioKind: 'sound-effect'; normalizedPrompt: string; input: EditorSoundEffectGenerationInput; result: { title: string; generationInputs: CanvasGenerationInputs; }; } | { kind: 'audio'; audioKind: 'background-music'; normalizedPrompt: string; input: EditorBackgroundMusicGenerationInput; result: { title: string; generationInputs: CanvasGenerationInputs; }; }; export type IconSpritesheetGenerationSubmissionPlan = | { ok: false; errorMessage: string; } | { ok: true; iconDescriptions: string[]; input: EditorIconSpritesheetGenerationInput; generationInputs: CanvasGenerationInputs; rememberImageModel: string; resultTitle: string; }; export type CharacterAnimationSubmissionPlan = { promptText: string; resultTitle: string; input: EditorCharacterAnimationGenerationInput; }; export function buildImageGenerationSubmissionPlan({ dialog, layers, nextGeneratedIndex, canonicalBackgroundMusicPrompt, }: ImageGenerationSubmissionOptions): ImageGenerationSubmissionPlan { if (dialog.mode === 'audio-background-music') { if (canonicalBackgroundMusicPrompt === undefined) { throw new Error('背景音乐提交缺少已确认的提示词'); } return { kind: 'audio', audioKind: 'background-music', normalizedPrompt: canonicalBackgroundMusicPrompt, input: { gptDescriptionPrompt: canonicalBackgroundMusicPrompt, makeInstrumental: true, }, result: { title: resolveGenerationAssetLabel( dialog.assetLabel, `游戏背景音乐 ${nextGeneratedIndex}`, ), generationInputs: buildBackgroundMusicGenerationInputs( canonicalBackgroundMusicPrompt, ), }, }; } const soundEffectPrompt = dialog.mode === 'audio-sound-effect' ? validateSoundEffectPrompt(dialog.prompt) : null; if (soundEffectPrompt && !soundEffectPrompt.ok) { throw new Error( soundEffectPrompt.reason === 'empty' ? '音效描述不能为空' : '音效描述不能超过 2048 个字符', ); } // SFX 走自己的 canonicalization 与 2048 边界;其余模式复用统一解析(含 scene 必填语义)。 const normalizedPrompt = soundEffectPrompt ? soundEffectPrompt.prompt : resolveImageGenerationDialogPrompt(dialog); if (dialog.mode === 'edit') { const sourceLayer = layers.find( (layer) => layer.id === dialog.sourceLayerId, ); if (!sourceLayer) { throw new Error('未找到要修改的图片'); } return { kind: 'edit', normalizedPrompt, sourceLayer, resultTitle: resolveGenerationAssetLabel( dialog.assetLabel, `${sourceLayer.title} 修改结果`, ), generationInputs: buildEditGenerationInputs( '修改要求', normalizedPrompt, sourceLayer, ), }; } if (dialog.mode === 'quick-edit' && dialog.sourceLayerId) { const sourceLayer = layers.find( (layer) => layer.id === dialog.sourceLayerId, ); if (!sourceLayer) { throw new Error('未找到要改造的图片'); } const basePrompt = dialog.prompt.trim() || '快速编辑图片'; const normalizedQuickEditPrompt = basePrompt; const imageModel = IMAGE_MODEL_GPT_IMAGE_2; return { kind: 'quick-edit', normalizedPrompt: normalizedQuickEditPrompt, sourceLayer, editInput: { size: `${sourceLayer.originalWidth}x${sourceLayer.originalHeight}`, model: imageModel, }, result: { title: resolveGenerationAssetLabel( dialog.assetLabel, `${sourceLayer.title} 快速编辑`, ), assetKind: sourceLayer.assetKind, generationInputs: buildQuickEditGenerationInputs( '快速编辑提示词', normalizedQuickEditPrompt, sourceLayer, [], ), }, rememberImageModel: imageModel, }; } if (dialog.mode === 'scene') { const sceneContent = dialog.prompt.trim(); if (!sceneContent) { throw new Error(EDITOR_SCENE_CONTENT_REQUIRED_ERROR); } const stylePreset = dialog.sceneStylePreset ?? DEFAULT_EDITOR_SCENE_STYLE_PRESET; const customStyle = dialog.sceneCustomStyle?.trim() ?? ''; if (stylePreset === CUSTOM_EDITOR_SCENE_STYLE_PRESET && !customStyle) { throw new Error(EDITOR_SCENE_CUSTOM_STYLE_REQUIRED_ERROR); } const references = dialog.generationReferences ?? []; const imageModel = normalizeEditorImageModel(dialog.imageModel); const generationInputs = buildSceneGenerationInputs( sceneContent, stylePreset, customStyle, references, ); return { kind: 'scene', normalizedPrompt: sceneContent, input: { sceneContent, stylePreset, ...(stylePreset === CUSTOM_EDITOR_SCENE_STYLE_PRESET ? { customStyle } : {}), model: imageModel, aspectRatio: dialog.aspectRatio ?? '16:9', imageSize: dialog.imageSize ?? '1K', ...(references.length ? { referenceImageSrcs: references.map((reference) => resolveImageReferenceSubmissionSource(reference), ), } : {}), generationInputs, }, result: { assetKind: 'scene', title: resolveGenerationAssetLabel( dialog.assetLabel, `游戏场景 ${nextGeneratedIndex}`, ), generationInputs, }, rememberImageModel: imageModel, }; } if (dialog.mode === 'spec') { const specType = dialog.specType ?? 'custom'; const specValues = dialog.specValues ?? DEFAULT_SPEC_FORM_VALUES[specType]; if (specType === 'icon') { const generationInputs = buildSpecGenerationInputs( specType, specValues, dialog.specReference, ); return { kind: 'icon-spec', normalizedPrompt, input: { playSetting: specValues.playSetting.trim(), artStyle: specValues.artStyle.trim(), ...(dialog.specReference?.src ? { referenceId: resolveRegisteredEditorReferenceId( dialog.specReference, ), } : {}), generationInputs, }, result: { assetKind: 'icon-spec', title: resolveGenerationAssetLabel( dialog.assetLabel, `${SPEC_TYPE_LABEL[specType]} ${nextGeneratedIndex}`, ), generationInputs, }, }; } return { kind: 'image', normalizedPrompt, input: { prompt: buildSpecPrompt( specType, specValues, Boolean(dialog.specReference?.src), ), size: SPEC_GENERATION_SIZE, model: SPEC_GENERATION_MODEL, aspectRatio: SPEC_GENERATION_ASPECT_RATIO, imageSize: SPEC_GENERATION_IMAGE_SIZE, kind: 'spec', ...(dialog.specReference?.src ? { referenceImageSrcs: [ resolveImageReferenceSubmissionSource(dialog.specReference), ], } : {}), }, result: { assetKind: 'spec', title: resolveGenerationAssetLabel( dialog.assetLabel, `${SPEC_TYPE_LABEL[specType]} ${nextGeneratedIndex}`, ), generationInputs: buildSpecGenerationInputs( specType, specValues, dialog.specReference, ), }, }; } if (dialog.mode === 'character') { const referenceImageSrcs = [ dialog.characterSpecReference ? resolveImageReferenceSubmissionSource(dialog.characterSpecReference) : null, ...(dialog.characterReferences ?? []).map((reference) => resolveImageReferenceSubmissionSource(reference), ), ].filter((src): src is string => Boolean(src)); const imageModel = normalizeEditorImageModel(dialog.imageModel); const screenColor = DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR; const segModel = DEFAULT_EDITOR_BGFILTER_SEG_MODEL; return { kind: 'image', normalizedPrompt, input: { prompt: normalizedPrompt, kind: 'character', model: imageModel, screenColor, segModel, style: dialog.style === 'pixelArt' ? 'pixelArt' : 'none', aspectRatio: dialog.aspectRatio ?? '1:1', imageSize: dialog.imageSize ?? '1K', ...(referenceImageSrcs.length ? { referenceImageSrcs } : {}), }, result: { assetKind: 'character', title: resolveGenerationAssetLabel( dialog.assetLabel, `角色形象 ${nextGeneratedIndex}`, ), generationInputs: buildCharacterGenerationInputs( normalizedPrompt, dialog.characterSpecReference, dialog.characterReferences, ), }, rememberImageModel: imageModel, }; } if (dialog.mode === 'ui-design') { const imageModel = IMAGE_MODEL_GPT_IMAGE_2; const referenceImageSrcs = [ dialog.uiDesignSpecReference ? resolveImageReferenceSubmissionSource(dialog.uiDesignSpecReference) : null, ...(dialog.generationReferences ?? []).map((reference) => resolveImageReferenceSubmissionSource(reference), ), ].filter((src): src is string => Boolean(src)); return { kind: 'image', normalizedPrompt, input: { prompt: normalizedPrompt, kind: 'ui-design', model: imageModel, aspectRatio: dialog.aspectRatio ?? '16:9', imageSize: normalizeGptImageSize(dialog.imageSize), ...(referenceImageSrcs.length ? { referenceImageSrcs } : {}), }, result: { assetKind: 'ui-design', title: resolveGenerationAssetLabel( dialog.assetLabel, `UI设计图 ${nextGeneratedIndex}`, ), generationInputs: buildUiDesignGenerationInputs( normalizedPrompt, dialog.uiDesignSpecReference, dialog.generationReferences, ), }, rememberImageModel: imageModel, }; } if (dialog.mode === 'publication') { const workflow = getPublicationMaterialsWorkflow( dialog.publicationWorkflowId ?? 'publication-cover-image', ); const imageModel = IMAGE_MODEL_GPT_IMAGE_2; const publicationPrompt = buildPublicationMaterialsPrompt(dialog.publicationGameInfo) || normalizedPrompt; const generationPrompt = buildPublicationMaterialsGenerationPrompt({ gameInfo: dialog.publicationGameInfo, workflow, }); return { kind: 'image', normalizedPrompt: publicationPrompt, input: { prompt: generationPrompt, size: workflow.outputSize, kind: 'publication-material', model: imageModel, aspectRatio: workflow.aspectRatio, imageSize: workflow.imageSize, ...(dialog.publicationReferences?.length ? { referenceImageSrcs: dialog.publicationReferences.map( (reference) => resolveImageReferenceSubmissionSource(reference), ), } : {}), }, result: { assetKind: 'publication-material', title: resolveGenerationAssetLabel( dialog.assetLabel, `${nextGeneratedIndex} 宣发素材`, ), generationInputs: buildPublicationMaterialsGenerationInputs( dialog.publicationGameInfo, dialog.publicationReferences, ), }, }; } if (dialog.mode === 'video') { const resolution = dialog.videoResolution ?? '480p'; const durationSeconds = typeof dialog.videoDurationSeconds === 'number' ? Math.min(15, Math.max(4, Math.round(dialog.videoDurationSeconds))) : DEFAULT_VIDEO_DURATION_SECONDS; const model = dialog.videoModel ?? DEFAULT_VIDEO_MODEL; const aspectRatio = dialog.videoAspectRatio ?? DEFAULT_VIDEO_ASPECT_RATIO; const sound = dialog.videoSound ?? DEFAULT_VIDEO_SOUND; const webSearchEnabled = dialog.videoWebSearchEnabled ?? DEFAULT_VIDEO_WEB_SEARCH_ENABLED; const seedanceReferences = isSeedanceVideoModel(model) ? buildSeedanceVideoReferenceInput(dialog.generationReferences ?? []) : { referenceImageSrcs: [], referenceVideoSrcs: [], referenceAudioSrcs: [], }; return { kind: 'video', normalizedPrompt, input: { prompt: normalizedPrompt, model, aspectRatio, durationSeconds, resolution, mode: 'std', sound, webSearchEnabled, ...(seedanceReferences.referenceImageSrcs.length ? { referenceImageSrcs: seedanceReferences.referenceImageSrcs } : {}), ...(seedanceReferences.referenceVideoSrcs.length ? { referenceVideoSrcs: seedanceReferences.referenceVideoSrcs } : {}), ...(seedanceReferences.referenceAudioSrcs.length ? { referenceAudioSrcs: seedanceReferences.referenceAudioSrcs } : {}), }, result: { title: resolveGenerationAssetLabel( dialog.assetLabel, `生成视频 ${nextGeneratedIndex}`, ), generationInputs: buildVideoGenerationInputs( normalizedPrompt, durationSeconds, dialog.generationReferences, ), }, }; } if (dialog.mode === 'audio-sound-effect') { const soundModel = EDITOR_SOUND_EFFECT_MODEL; const durationSeconds = dialog.soundDurationMode === 'auto' ? null : typeof dialog.soundDurationSeconds === 'number' ? dialog.soundDurationSeconds : DEFAULT_SOUND_EFFECT_DURATION_SECONDS; const loop = dialog.soundLoop === true; return { kind: 'audio', audioKind: 'sound-effect', normalizedPrompt, input: { prompt: normalizedPrompt, model: soundModel, duration: durationSeconds, loop, }, result: { title: resolveGenerationAssetLabel( dialog.assetLabel, `游戏音效 ${nextGeneratedIndex}`, ), generationInputs: buildSoundEffectGenerationInputs( normalizedPrompt, soundModel, durationSeconds, loop, ), }, }; } const imageModel = normalizeEditorImageModel(dialog.imageModel); return { kind: 'image', normalizedPrompt, input: { prompt: normalizedPrompt, model: imageModel, ...(dialog.mode === 'generate' ? { style: dialog.style === 'pixelArt' ? 'pixelArt' : 'none' } : {}), aspectRatio: dialog.aspectRatio ?? '1:1', imageSize: dialog.imageSize ?? '1K', ...(dialog.generationReferences?.length ? { referenceImageSrcs: dialog.generationReferences.map((reference) => resolveImageReferenceSubmissionSource(reference), ), } : {}), }, result: { title: resolveGenerationAssetLabel( dialog.assetLabel, `生成图片 ${nextGeneratedIndex}`, ), generationInputs: buildImageGenerationInputs( normalizedPrompt, dialog.generationReferences, ), }, rememberImageModel: imageModel, }; } export function buildIconSpritesheetGenerationSubmissionPlan( dialog: GenerateDialogState, nextGeneratedIndex = 1, ): IconSpritesheetGenerationSubmissionPlan { const normalizedPrompt = dialog.prompt.trim() || (dialog.iconDescriptions ?? []) .map((description) => description.trim()) .filter(Boolean) .join('\n'); const iconDescriptions = normalizedPrompt ? [normalizedPrompt] : []; if (!dialog.iconSpecReference) { return { ok: false, errorMessage: '请选择图标规范', }; } if (!iconDescriptions.length) { return { ok: false, errorMessage: '请填写素材描述', }; } if (Array.from(normalizedPrompt).length > EDITOR_ICON_DESCRIPTION_MAX_CHARS) { return { ok: false, errorMessage: `素材描述不能超过 ${EDITOR_ICON_DESCRIPTION_MAX_CHARS} 个字符`, }; } let referenceId: string; try { referenceId = resolveRegisteredEditorReferenceId(dialog.iconSpecReference); } catch (error) { return { ok: false, errorMessage: error instanceof Error && error.message.trim() ? error.message : '图标规范尚未完成资源登记,请重新选择或上传后再试', }; } const rememberImageModel = normalizeEditorImageModel(dialog.imageModel); const screenColor = DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR; const segModel = DEFAULT_EDITOR_BGFILTER_SEG_MODEL; return { ok: true, iconDescriptions, input: { referenceId, ...(dialog.generationReferences?.length ? { referenceImageSrcs: dialog.generationReferences.map((reference) => resolveImageReferenceSubmissionSource(reference), ), } : {}), iconDescriptions, model: rememberImageModel, screenColor, segModel, style: dialog.style === 'pixelArt' ? 'pixelArt' : 'none', aspectRatio: dialog.aspectRatio ?? '1:1', imageSize: dialog.imageSize ?? '1K', }, generationInputs: buildIconGenerationInputs( iconDescriptions, dialog.iconSpecReference, dialog.generationReferences, ), rememberImageModel, resultTitle: resolveGenerationAssetLabel( dialog.assetLabel, `图标素材图集 ${nextGeneratedIndex}`, ), }; } export function buildCharacterAnimationSubmissionPlan({ panel, sourceLayer, }: { panel: CharacterAnimationPanelState; sourceLayer: CanvasLayer; }): CharacterAnimationSubmissionPlan { const promptText = panel.promptText.trim(); const resultTitle = resolveGenerationAssetLabel(panel.assetLabel, '角色动作'); return { promptText, resultTitle, input: { sourceLayerId: sourceLayer.id, sourceImageSrc: resolveCharacterAnimationSourceImageSrc(sourceLayer), sourceWidth: sourceLayer.originalWidth, sourceHeight: sourceLayer.originalHeight, promptText, // 与生图入口一致:默认 auto,由后端做背景色自动决策。 screenColor: DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR, resolution: panel.resolution, ratio: panel.ratio, frameCount: panel.frameCount, durationSeconds: panel.durationSeconds, model: CHARACTER_ANIMATION_MODEL, assetLabel: resultTitle, }, }; }