05f0e2db06
合并master当前工程、后端、前端和文档更新 按已确认方案解决Spine序列帧多模态、帧数和快速编辑冲突 保留当前分支底部工具栏宽度与隐藏滚动条样式
1252 lines
37 KiB
TypeScript
1252 lines
37 KiB
TypeScript
import { EDITOR_SOUND_EFFECT_MODEL } from '../../../packages/shared/src/contracts/editorAudio';
|
|
import type { EditorSceneGenerationInputs } from '../../../packages/shared/src/contracts/editorScene';
|
|
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,
|
|
CharacterReferenceImage,
|
|
GenerateDialogState,
|
|
} from './ImageCanvasEditorTypes';
|
|
import {
|
|
buildBackgroundMusicGenerationInputs,
|
|
buildCharacterGenerationInputs,
|
|
buildCharacterSheetGenerationInputs,
|
|
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_IMAGE_MODEL,
|
|
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,
|
|
EDITOR_IMAGE_DIMENSION_OPTIONS,
|
|
EDITOR_IMAGE_MODEL_OPTIONS,
|
|
IMAGE_MODEL_GPT_IMAGE_2,
|
|
inferEditorImageAspectRatio,
|
|
inferEditorImageSizeLabel,
|
|
normalizeEditorImageModel,
|
|
resolveCharacterAnimationSourceImageSrc,
|
|
resolveEditorImageGenerationPixelSize,
|
|
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;
|
|
|
|
/**
|
|
* 场景生成接口的历史契约只接受字符串参数;画布配方则保留原生
|
|
* string | number | boolean 值,以便在素材元数据中无损回放。
|
|
*/
|
|
export function toEditorSceneGenerationInputs(
|
|
generationInputs: CanvasGenerationInputs,
|
|
): EditorSceneGenerationInputs {
|
|
return {
|
|
fields: generationInputs.fields.map(({ title, value }) => ({
|
|
title,
|
|
value: String(value),
|
|
})),
|
|
references: generationInputs.references.map(
|
|
({ title, label, refType, refId }) => ({
|
|
title,
|
|
label: label ?? '',
|
|
refType,
|
|
refId,
|
|
}),
|
|
),
|
|
};
|
|
}
|
|
|
|
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;
|
|
},
|
|
referenceLabel: '原图' | '参考图' = '原图',
|
|
) {
|
|
const resourceId = reference.resourceId?.trim();
|
|
if (
|
|
resourceId &&
|
|
!resourceId.startsWith('local-resource-') &&
|
|
!resourceId.startsWith('generation-dialog:')
|
|
) {
|
|
return resourceId;
|
|
}
|
|
const assetId = reference.sourceAssetId?.trim();
|
|
if (assetId && !assetId.startsWith('upload-')) {
|
|
return assetId;
|
|
}
|
|
throw new Error(
|
|
`${referenceLabel}尚未登记为项目资源或素材,请重新选择或上传后再试`,
|
|
);
|
|
}
|
|
|
|
function buildSeedanceVideoReferenceInput(
|
|
references: NonNullable<GenerateDialogState['generationReferences']>,
|
|
) {
|
|
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);
|
|
}
|
|
|
|
function resolveOptionalFieldWithFallback<T extends string>(
|
|
preferredValue: string | undefined | null,
|
|
supportedValues: readonly string[],
|
|
fallbackValues: readonly T[],
|
|
defaultValue: T,
|
|
) {
|
|
const normalizedPreferred = preferredValue?.trim() ?? '';
|
|
if (supportedValues.includes(normalizedPreferred)) {
|
|
return normalizedPreferred as T;
|
|
}
|
|
return (
|
|
fallbackValues.find((fallbackValue) =>
|
|
supportedValues.includes(fallbackValue),
|
|
) ?? defaultValue
|
|
);
|
|
}
|
|
|
|
function resolveSupportedFieldValue({
|
|
preferredValues,
|
|
supportedValues,
|
|
fallbackValues,
|
|
defaultValue,
|
|
}: {
|
|
preferredValues: Array<string | null | undefined>;
|
|
supportedValues: readonly string[];
|
|
fallbackValues: readonly string[];
|
|
defaultValue: string;
|
|
}) {
|
|
for (const value of preferredValues) {
|
|
const normalized = value?.trim();
|
|
if (normalized && supportedValues.includes(normalized)) {
|
|
return normalized;
|
|
}
|
|
}
|
|
return (
|
|
fallbackValues.find((fallbackValue) =>
|
|
supportedValues.includes(fallbackValue),
|
|
) ?? defaultValue
|
|
);
|
|
}
|
|
|
|
function resolveImageEditFieldValueFromSourceLayer(
|
|
sourceLayer: CanvasLayer,
|
|
fieldId: string,
|
|
) {
|
|
const rawValue = sourceLayer.generationInputs?.fields.find(
|
|
(field) => field.id === fieldId,
|
|
)?.value;
|
|
return typeof rawValue === 'string' ? rawValue : null;
|
|
}
|
|
|
|
function resolveImageEditModel({
|
|
sourceLayer,
|
|
dialogModel,
|
|
}: {
|
|
sourceLayer: CanvasLayer;
|
|
dialogModel?: string | null;
|
|
}) {
|
|
const normalizedDialogModel = dialogModel
|
|
? normalizeEditorImageModel(dialogModel)
|
|
: null;
|
|
const normalizedSourceLayerModel = sourceLayer.model
|
|
? normalizeEditorImageModel(sourceLayer.model)
|
|
: null;
|
|
const resolvedModelFromLayer = resolveImageEditFieldValueFromSourceLayer(
|
|
sourceLayer,
|
|
'model',
|
|
);
|
|
const normalizedLayerPersistedModel = resolvedModelFromLayer
|
|
? normalizeEditorImageModel(resolvedModelFromLayer)
|
|
: null;
|
|
return [
|
|
normalizedDialogModel,
|
|
normalizedLayerPersistedModel,
|
|
normalizedSourceLayerModel,
|
|
].find((candidateModel) =>
|
|
EDITOR_IMAGE_MODEL_OPTIONS.some(
|
|
(option) => option.value === candidateModel,
|
|
),
|
|
);
|
|
}
|
|
|
|
function resolveImageEditGeometry({
|
|
sourceLayer,
|
|
dialogImageModel,
|
|
dialogAspectRatio,
|
|
dialogImageSize,
|
|
}: {
|
|
sourceLayer: CanvasLayer;
|
|
dialogImageModel?: string | null;
|
|
dialogAspectRatio?: string | null;
|
|
dialogImageSize?: string | null;
|
|
}) {
|
|
const model = resolveImageEditModel({
|
|
sourceLayer,
|
|
dialogModel: dialogImageModel,
|
|
});
|
|
const selectedModel = model ?? DEFAULT_IMAGE_MODEL;
|
|
const dimensionOptions =
|
|
EDITOR_IMAGE_DIMENSION_OPTIONS[
|
|
selectedModel as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
|
|
] ??
|
|
EDITOR_IMAGE_DIMENSION_OPTIONS[
|
|
DEFAULT_IMAGE_MODEL as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
|
|
];
|
|
const supportedAspectRatios =
|
|
dimensionOptions.aspectRatios as readonly string[];
|
|
const supportedImageSizes = dimensionOptions.imageSizes as readonly string[];
|
|
const persistedAspectRatio = resolveImageEditFieldValueFromSourceLayer(
|
|
sourceLayer,
|
|
'aspectRatio',
|
|
);
|
|
const persistedImageSize = resolveImageEditFieldValueFromSourceLayer(
|
|
sourceLayer,
|
|
'imageSize',
|
|
);
|
|
const inferredAspectRatio = inferEditorImageAspectRatio(
|
|
sourceLayer.originalWidth,
|
|
sourceLayer.originalHeight,
|
|
);
|
|
const inferredImageSize = inferEditorImageSizeLabel(
|
|
sourceLayer.originalWidth,
|
|
sourceLayer.originalHeight,
|
|
);
|
|
const defaultImageSize = supportedImageSizes.includes('1K')
|
|
? '1K'
|
|
: (supportedImageSizes[0] ?? '1K');
|
|
const aspectRatio = resolveSupportedFieldValue({
|
|
preferredValues: [dialogAspectRatio, persistedAspectRatio],
|
|
supportedValues: supportedAspectRatios,
|
|
fallbackValues: [inferredAspectRatio, ...dimensionOptions.aspectRatios],
|
|
defaultValue: '1:1',
|
|
});
|
|
const imageSize = resolveSupportedFieldValue({
|
|
preferredValues: [dialogImageSize, persistedImageSize],
|
|
supportedValues: supportedImageSizes,
|
|
fallbackValues: [inferredImageSize, ...dimensionOptions.imageSizes],
|
|
defaultValue: defaultImageSize,
|
|
});
|
|
const outputSize = resolveEditorImageGenerationPixelSize({
|
|
model: selectedModel,
|
|
aspectRatio,
|
|
imageSize,
|
|
});
|
|
return {
|
|
model: selectedModel,
|
|
aspectRatio,
|
|
imageSize,
|
|
outputSize,
|
|
};
|
|
}
|
|
|
|
export type ImageGenerationSubmissionPlan =
|
|
| {
|
|
kind: 'edit';
|
|
normalizedPrompt: string;
|
|
sourceLayer: CanvasLayer;
|
|
resultTitle: string;
|
|
editInput: Pick<
|
|
EditorImageEditInput,
|
|
'size' | 'model' | 'aspectRatio' | 'imageSize'
|
|
>;
|
|
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<
|
|
EditorImageEditInput,
|
|
'size' | 'model' | 'aspectRatio' | 'imageSize' | 'referenceImageSrcs'
|
|
>;
|
|
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('未找到要修改的图片');
|
|
}
|
|
const geometry = resolveImageEditGeometry({
|
|
sourceLayer,
|
|
dialogImageModel: dialog.imageModel,
|
|
dialogAspectRatio: dialog.aspectRatio,
|
|
dialogImageSize: dialog.imageSize,
|
|
});
|
|
return {
|
|
kind: 'edit',
|
|
normalizedPrompt,
|
|
sourceLayer,
|
|
resultTitle: resolveGenerationAssetLabel(
|
|
dialog.assetLabel,
|
|
`${sourceLayer.title} 修改结果`,
|
|
),
|
|
editInput: {
|
|
size: `${geometry.outputSize.width}x${geometry.outputSize.height}`,
|
|
model: geometry.model,
|
|
aspectRatio: geometry.aspectRatio,
|
|
imageSize: geometry.imageSize,
|
|
},
|
|
generationInputs: buildEditGenerationInputs(
|
|
'修改要求',
|
|
normalizedPrompt,
|
|
sourceLayer,
|
|
{
|
|
model: geometry.model,
|
|
aspectRatio: geometry.aspectRatio,
|
|
imageSize: geometry.imageSize,
|
|
},
|
|
),
|
|
};
|
|
}
|
|
|
|
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 requestedImageModel = normalizeEditorImageModel(
|
|
dialog.imageModel ?? IMAGE_MODEL_GPT_IMAGE_2,
|
|
);
|
|
const imageModel = EDITOR_IMAGE_MODEL_OPTIONS.some(
|
|
(option) => option.value === requestedImageModel,
|
|
)
|
|
? requestedImageModel
|
|
: IMAGE_MODEL_GPT_IMAGE_2;
|
|
const dimensionOptions =
|
|
EDITOR_IMAGE_DIMENSION_OPTIONS[
|
|
imageModel as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
|
|
] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[IMAGE_MODEL_GPT_IMAGE_2];
|
|
const supportedAspectRatios =
|
|
dimensionOptions.aspectRatios as readonly string[];
|
|
const supportedImageSizes =
|
|
dimensionOptions.imageSizes as readonly string[];
|
|
const inferredAspectRatio = inferEditorImageAspectRatio(
|
|
sourceLayer.originalWidth,
|
|
sourceLayer.originalHeight,
|
|
);
|
|
const inferredImageSize = inferEditorImageSizeLabel(
|
|
sourceLayer.originalWidth,
|
|
sourceLayer.originalHeight,
|
|
);
|
|
const aspectRatio = supportedAspectRatios.includes(dialog.aspectRatio ?? '')
|
|
? dialog.aspectRatio!.trim()
|
|
: resolveOptionalFieldWithFallback(
|
|
inferredAspectRatio,
|
|
supportedAspectRatios,
|
|
dimensionOptions.aspectRatios,
|
|
'1:1',
|
|
);
|
|
const imageSize = supportedImageSizes.includes(dialog.imageSize ?? '')
|
|
? dialog.imageSize!
|
|
: resolveOptionalFieldWithFallback(
|
|
inferredImageSize,
|
|
supportedImageSizes,
|
|
dimensionOptions.imageSizes,
|
|
'1K',
|
|
);
|
|
const outputSize = resolveEditorImageGenerationPixelSize({
|
|
model: imageModel,
|
|
aspectRatio,
|
|
imageSize,
|
|
});
|
|
const references = dialog.generationReferences ?? [];
|
|
return {
|
|
kind: 'quick-edit',
|
|
normalizedPrompt: normalizedQuickEditPrompt,
|
|
sourceLayer,
|
|
editInput: {
|
|
size: `${outputSize.width}x${outputSize.height}`,
|
|
model: imageModel,
|
|
aspectRatio,
|
|
imageSize,
|
|
...(references.length
|
|
? {
|
|
referenceImageSrcs: references.map(
|
|
resolveImageReferenceSubmissionSource,
|
|
),
|
|
}
|
|
: {}),
|
|
},
|
|
result: {
|
|
title: resolveGenerationAssetLabel(
|
|
dialog.assetLabel,
|
|
`${sourceLayer.title} 快速编辑`,
|
|
),
|
|
assetKind: sourceLayer.assetKind,
|
|
generationInputs: buildQuickEditGenerationInputs(
|
|
'快速编辑提示词',
|
|
normalizedQuickEditPrompt,
|
|
sourceLayer,
|
|
references,
|
|
{
|
|
model: imageModel,
|
|
aspectRatio,
|
|
imageSize,
|
|
},
|
|
),
|
|
},
|
|
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,
|
|
{
|
|
model: imageModel,
|
|
aspectRatio: dialog.aspectRatio ?? '16:9',
|
|
imageSize: dialog.imageSize ?? '1K',
|
|
},
|
|
);
|
|
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: toEditorSceneGenerationInputs(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;
|
|
const sheetEnabled = dialog.characterSheetEnabled === true;
|
|
// 中文注释:多方向角色图走后端 kind=character-sheet(绿色间隔底,便于后续拆分),
|
|
// 产物按 icon-spritesheet 落库,复用既有「拆分图集」能力;不做像素化、不去背景。
|
|
return {
|
|
kind: 'image',
|
|
normalizedPrompt,
|
|
input: {
|
|
prompt: normalizedPrompt,
|
|
kind: sheetEnabled ? 'character-sheet' : 'character',
|
|
model: imageModel,
|
|
...(sheetEnabled ? {} : { screenColor, segModel }),
|
|
style: sheetEnabled
|
|
? 'none'
|
|
: dialog.style === 'pixelArt'
|
|
? 'pixelArt'
|
|
: 'none',
|
|
aspectRatio: dialog.aspectRatio ?? '1:1',
|
|
imageSize: dialog.imageSize ?? '1K',
|
|
...(sheetEnabled
|
|
? {
|
|
assetKind: 'icon-spritesheet',
|
|
}
|
|
: {}),
|
|
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
|
|
},
|
|
result: {
|
|
assetKind: sheetEnabled ? 'icon-spritesheet' : 'character',
|
|
title: resolveGenerationAssetLabel(
|
|
dialog.assetLabel,
|
|
sheetEnabled
|
|
? `角色多方向图 ${nextGeneratedIndex}`
|
|
: `角色形象 ${nextGeneratedIndex}`,
|
|
),
|
|
generationInputs: sheetEnabled
|
|
? buildCharacterSheetGenerationInputs(
|
|
normalizedPrompt,
|
|
dialog.characterSpecReference,
|
|
dialog.characterReferences,
|
|
{
|
|
model: imageModel,
|
|
aspectRatio: dialog.aspectRatio ?? '1:1',
|
|
imageSize: dialog.imageSize ?? '1K',
|
|
},
|
|
)
|
|
: buildCharacterGenerationInputs(
|
|
normalizedPrompt,
|
|
dialog.characterSpecReference,
|
|
dialog.characterReferences,
|
|
{
|
|
model: imageModel,
|
|
style: dialog.style === 'pixelArt' ? 'pixelArt' : 'none',
|
|
aspectRatio: dialog.aspectRatio ?? '1:1',
|
|
imageSize: dialog.imageSize ?? '1K',
|
|
},
|
|
),
|
|
},
|
|
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,
|
|
{
|
|
model: imageModel,
|
|
aspectRatio: dialog.aspectRatio ?? '16:9',
|
|
imageSize: normalizeGptImageSize(dialog.imageSize),
|
|
},
|
|
),
|
|
},
|
|
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,
|
|
workflow.id,
|
|
),
|
|
},
|
|
};
|
|
}
|
|
|
|
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,
|
|
dialog.generationReferences,
|
|
{
|
|
model,
|
|
aspectRatio,
|
|
resolution,
|
|
durationSeconds,
|
|
sound,
|
|
webSearchEnabled,
|
|
},
|
|
),
|
|
},
|
|
};
|
|
}
|
|
|
|
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,
|
|
{
|
|
model: imageModel,
|
|
style: dialog.style === 'pixelArt' ? 'pixelArt' : 'none',
|
|
aspectRatio: dialog.aspectRatio ?? '1:1',
|
|
imageSize: dialog.imageSize ?? '1K',
|
|
},
|
|
),
|
|
},
|
|
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,
|
|
{
|
|
model: rememberImageModel,
|
|
style: dialog.style === 'pixelArt' ? 'pixelArt' : 'none',
|
|
aspectRatio: dialog.aspectRatio ?? '1:1',
|
|
imageSize: dialog.imageSize ?? '1K',
|
|
},
|
|
),
|
|
rememberImageModel,
|
|
resultTitle: resolveGenerationAssetLabel(
|
|
dialog.assetLabel,
|
|
`图标素材图集 ${nextGeneratedIndex}`,
|
|
),
|
|
};
|
|
}
|
|
|
|
export function buildCharacterAnimationSubmissionPlan({
|
|
panel,
|
|
sourceLayer,
|
|
reference = null,
|
|
}: {
|
|
panel: CharacterAnimationPanelState;
|
|
sourceLayer: CanvasLayer | null;
|
|
reference?: CharacterReferenceImage | null;
|
|
}): CharacterAnimationSubmissionPlan {
|
|
const promptText = panel.promptText.trim();
|
|
const resultTitle = resolveGenerationAssetLabel(panel.assetLabel, '角色动作');
|
|
if (!promptText && !reference && !sourceLayer) {
|
|
throw new Error('动画描述和参考素材不能同时为空。');
|
|
}
|
|
const inputMode =
|
|
reference?.mediaType === 'video'
|
|
? ('video' as const)
|
|
: reference || sourceLayer
|
|
? ('image' as const)
|
|
: ('text' as const);
|
|
const referenceSrc = reference
|
|
? reference.objectKey?.trim() || reference.src
|
|
: sourceLayer
|
|
? resolveCharacterAnimationSourceImageSrc(sourceLayer)
|
|
: undefined;
|
|
const referenceWidth =
|
|
typeof reference?.width === 'number' &&
|
|
Number.isFinite(reference.width) &&
|
|
reference.width > 0
|
|
? Math.round(reference.width)
|
|
: sourceLayer?.originalWidth;
|
|
const referenceHeight =
|
|
typeof reference?.height === 'number' &&
|
|
Number.isFinite(reference.height) &&
|
|
reference.height > 0
|
|
? Math.round(reference.height)
|
|
: sourceLayer?.originalHeight;
|
|
return {
|
|
promptText,
|
|
resultTitle,
|
|
input: {
|
|
inputMode,
|
|
...(sourceLayer ? { sourceLayerId: sourceLayer.id } : {}),
|
|
...(inputMode === 'image' && referenceSrc
|
|
? { sourceImageSrc: referenceSrc }
|
|
: {}),
|
|
...(referenceWidth && referenceHeight
|
|
? {
|
|
sourceWidth: referenceWidth,
|
|
sourceHeight: referenceHeight,
|
|
}
|
|
: {}),
|
|
...(referenceSrc
|
|
? {
|
|
referenceMediaType: inputMode === 'video' ? 'video' : 'image',
|
|
referenceSrc,
|
|
}
|
|
: {}),
|
|
promptText,
|
|
...(panel.backgroundColor
|
|
? { backgroundColor: panel.backgroundColor }
|
|
: {}),
|
|
resolution: panel.resolution,
|
|
ratio: panel.ratio,
|
|
frameCount: panel.frameCount,
|
|
durationSeconds: panel.durationSeconds,
|
|
model: CHARACTER_ANIMATION_MODEL,
|
|
assetLabel: resultTitle,
|
|
},
|
|
};
|
|
}
|