ea29887d9c
- 快速编辑支持最多8张额外参考图,并将原图自动作为最后一张参考图提交 - 快速编辑尺寸和模型沿用原图配置,按禁用态参数胶囊展示 - 快速编辑提交前同步改写原图引用为实际图序号 - 快速编辑生成改为创建独立画布生成占位,并支持键盘删除生成中对象 - 对齐生成类面板参考图图标、规范类固定参数和相关文档测试
719 lines
22 KiB
TypeScript
719 lines
22 KiB
TypeScript
import { ApiClientError } from '../../services/apiClient';
|
|
import type {
|
|
EditorCharacterAnimationRatio,
|
|
EditorCharacterAnimationResolution,
|
|
} from '../../services/image-editor/editorProjectClient';
|
|
import { isGeneratedLayer } from './ImageCanvasEditorModel';
|
|
import type {
|
|
CanvasGenerationDialogState,
|
|
CanvasGenerationInputField,
|
|
CanvasGenerationInputs,
|
|
CanvasLayer,
|
|
CharacterReferenceImage,
|
|
GenerateDialogState,
|
|
SpecFormValues,
|
|
SpecGenerationType,
|
|
} from './ImageCanvasEditorTypes';
|
|
|
|
// 中文注释:与 api-server/src/editor_generation_config.rs 保持同名语义,当前作为前端展示兜底。
|
|
export const EDITOR_GENERATION_MUD_POINT_CONFIG = {
|
|
image: 12,
|
|
spec: 5,
|
|
character: 12,
|
|
icon: 12,
|
|
uiDesign: 12,
|
|
videoPerSecond: {
|
|
'480p': 10,
|
|
'720p': 20,
|
|
},
|
|
characterAnimationPerSecond: {
|
|
'480p': 10,
|
|
'720p': 20,
|
|
},
|
|
soundEffect: 10,
|
|
backgroundMusic: 5,
|
|
} as const;
|
|
export const SPEC_GENERATION_COST = EDITOR_GENERATION_MUD_POINT_CONFIG.spec;
|
|
export const SPEC_GENERATION_SIZE = '2048x1152';
|
|
export const SPEC_FRAME_ORIGINAL_SIZE = { width: 2048, height: 1152 };
|
|
export const SPEC_FRAME_DISPLAY_SIZE = { width: 560, height: 315 };
|
|
export const CHARACTER_FRAME_ORIGINAL_SIZE = { width: 2048, height: 2048 };
|
|
export const CHARACTER_FRAME_DISPLAY_SIZE = { width: 420, height: 420 };
|
|
export const CHARACTER_ANIMATION_FRAME_ORIGINAL_SIZE = {
|
|
width: 1024,
|
|
height: 1024,
|
|
};
|
|
export const CHARACTER_ANIMATION_FRAME_DISPLAY_SIZE = {
|
|
width: 420,
|
|
height: 420,
|
|
};
|
|
export const ICON_FRAME_ORIGINAL_SIZE = { width: 512, height: 512 };
|
|
export const ICON_FRAME_DISPLAY_SIZE = { width: 360, height: 360 };
|
|
export const UI_DESIGN_FRAME_ORIGINAL_SIZE = { width: 2048, height: 1152 };
|
|
export const UI_DESIGN_FRAME_DISPLAY_SIZE = { width: 560, height: 315 };
|
|
export const IMAGE_MODEL_GPT_IMAGE_2 = 'gpt-image-2';
|
|
export const IMAGE_MODEL_NANOBANANA2 = 'gemini-3.1-flash-image-preview';
|
|
export const DEFAULT_IMAGE_MODEL = IMAGE_MODEL_NANOBANANA2;
|
|
export const SPEC_GENERATION_MODEL = IMAGE_MODEL_GPT_IMAGE_2;
|
|
export const SPEC_GENERATION_ASPECT_RATIO = '16:9';
|
|
export const SPEC_GENERATION_IMAGE_SIZE = '2K';
|
|
export const ICON_DESCRIPTION_LIMIT = 100;
|
|
export const DEFAULT_ICON_DESCRIPTIONS = [
|
|
'返回按钮',
|
|
'设置按钮',
|
|
'下一关按钮',
|
|
'提示按钮',
|
|
'原图按钮',
|
|
'冻结按钮',
|
|
];
|
|
export const QUICK_EDIT_SIZE_PRESETS = [
|
|
'1024x1024',
|
|
'1536x1024',
|
|
'2048x1152',
|
|
'1024x1536',
|
|
] as const;
|
|
export const QUICK_EDIT_MODEL_OPTIONS = [
|
|
{ label: 'nanobanana2', value: IMAGE_MODEL_NANOBANANA2 },
|
|
{ label: 'gpt-image-2', value: IMAGE_MODEL_GPT_IMAGE_2 },
|
|
] as const;
|
|
export const EDITOR_IMAGE_MODEL_OPTIONS = QUICK_EDIT_MODEL_OPTIONS;
|
|
export const EDITOR_IMAGE_DIMENSION_OPTIONS = {
|
|
[IMAGE_MODEL_NANOBANANA2]: {
|
|
aspectRatios: ['1:1', '2:3', '3:2', '9:16', '16:9'],
|
|
imageSizes: ['0.5K', '1K', '2K'],
|
|
},
|
|
[IMAGE_MODEL_GPT_IMAGE_2]: {
|
|
aspectRatios: ['1:1', '2:3', '3:2', '9:16', '16:9'],
|
|
imageSizes: ['1K', '2K'],
|
|
},
|
|
} as const;
|
|
export const VIDEO_MODEL_SEEDANCE_2 = 'seedance2.0';
|
|
export const VIDEO_MODEL_SEEDANCE_2_FAST = 'seedance2.0-fast';
|
|
export const DEFAULT_VIDEO_MODEL = VIDEO_MODEL_SEEDANCE_2_FAST;
|
|
export const CHARACTER_ANIMATION_MODEL = VIDEO_MODEL_SEEDANCE_2_FAST;
|
|
export const SEEDANCE_VIDEO_REFERENCE_LIMITS = {
|
|
image: 9,
|
|
video: 3,
|
|
audio: 3,
|
|
requestBytes: 64 * 1024 * 1024,
|
|
} as const;
|
|
export const QUICK_EDIT_REFERENCE_LIMIT = 8;
|
|
export const CHARACTER_ANIMATION_ACTION_PROMPTS = [
|
|
{ label: '待机', text: '待机动作,轻微呼吸起伏。' },
|
|
{ label: '行走', text: '循环行走动作,步伐稳定。' },
|
|
{ label: '奔跑', text: '循环奔跑动作,动作清晰有力。' },
|
|
{ label: '跳跃', text: '起跳、滞空、落地动作。' },
|
|
{ label: '攻击', text: '攻击动作,前摇、出手、收招清晰。' },
|
|
{ label: '受击', text: '受击后短暂后仰并恢复站姿。' },
|
|
{ label: '倒下', text: '倒下动作,重心下落自然。' },
|
|
] as const;
|
|
export const CHARACTER_ANIMATION_RATIO_OPTIONS: Array<{
|
|
label: string;
|
|
value: EditorCharacterAnimationRatio;
|
|
}> = [
|
|
{ label: '与角色图片保持同尺寸', value: 'same' },
|
|
{ label: '1:1', value: '1:1' },
|
|
{ label: '4:3', value: '4:3' },
|
|
{ label: '16:9', value: '16:9' },
|
|
{ label: '9:16', value: '9:16' },
|
|
{ label: '3:4', value: '3:4' },
|
|
];
|
|
export const CHARACTER_ANIMATION_DURATION_OPTIONS = [
|
|
{ label: '32帧·4秒', frameCount: 32, durationSeconds: 4 },
|
|
{ label: '40帧·5秒', frameCount: 40, durationSeconds: 5 },
|
|
{ label: '48帧·6秒', frameCount: 48, durationSeconds: 6 },
|
|
] as const;
|
|
export const VIDEO_FRAME_ORIGINAL_SIZE = { width: 1280, height: 720 };
|
|
export const VIDEO_FRAME_DISPLAY_SIZE = { width: 560, height: 315 };
|
|
export const AUDIO_FRAME_ORIGINAL_SIZE = { width: 420, height: 120 };
|
|
export const AUDIO_FRAME_DISPLAY_SIZE = { width: 420, height: 120 };
|
|
export const EDITOR_VIDEO_MODEL_OPTIONS = [
|
|
{ label: 'Seedance 2.0 Fast', value: VIDEO_MODEL_SEEDANCE_2_FAST },
|
|
{ label: 'Seedance 2.0', value: VIDEO_MODEL_SEEDANCE_2 },
|
|
{ label: 'Kling 3.0', value: 'kling3.0' },
|
|
{ label: 'Kling 3.0 Omni', value: 'kling3.0-omni' },
|
|
] as const;
|
|
export const EDITOR_VIDEO_DURATION_OPTIONS = [
|
|
{ label: '4秒', value: '4' },
|
|
{ label: '5秒', value: '5' },
|
|
] as const;
|
|
|
|
export const DEFAULT_SPEC_FORM_VALUES: Record<
|
|
SpecGenerationType,
|
|
SpecFormValues
|
|
> = {
|
|
character: {
|
|
playSetting: '战棋类RPG玩法',
|
|
artStyle: '像素风',
|
|
bodyRatio: '3',
|
|
characterView:
|
|
'右向斜侧身站姿,保留少量正面信息,能读到面部轮廓与胸肩结构,禁止生成完全 90 度纯右视图,也禁止生成正面立绘。',
|
|
customPrompt: '',
|
|
},
|
|
ui: {
|
|
playSetting: '抓娃娃题材的抓大鹅玩法',
|
|
artStyle: '毛茸茸',
|
|
bodyRatio: '3',
|
|
characterView:
|
|
'右向斜侧身站姿,保留少量正面信息,能读到面部轮廓与胸肩结构,禁止生成完全 90 度纯右视图,也禁止生成正面立绘。',
|
|
customPrompt: '',
|
|
},
|
|
icon: {
|
|
playSetting: '休闲小游戏',
|
|
artStyle: '清爽卡通',
|
|
bodyRatio: '3',
|
|
characterView:
|
|
'右向斜侧身站姿,保留少量正面信息,能读到面部轮廓与胸肩结构,禁止生成完全 90 度纯右视图,也禁止生成正面立绘。',
|
|
customPrompt: '',
|
|
},
|
|
custom: {
|
|
playSetting: '',
|
|
artStyle: '',
|
|
bodyRatio: '3',
|
|
characterView:
|
|
'右向斜侧身站姿,保留少量正面信息,能读到面部轮廓与胸肩结构,禁止生成完全 90 度纯右视图,也禁止生成正面立绘。',
|
|
customPrompt: '',
|
|
},
|
|
};
|
|
|
|
export const SPEC_TYPE_LABEL: Record<SpecGenerationType, string> = {
|
|
character: '角色规范',
|
|
ui: '图标规范',
|
|
icon: '图标规范',
|
|
custom: '自定义规范',
|
|
};
|
|
|
|
export const CHARACTER_SPEC_VIEW_OPTIONS = [
|
|
DEFAULT_SPEC_FORM_VALUES.character.characterView,
|
|
'左向三分之二侧身站姿',
|
|
'左向三分之二侧身站姿,保留少量正面信息,能读到面部轮廓与胸肩结构,禁止生成完全 90 度纯左视图,也禁止生成正面立绘。',
|
|
'右向三分之二侧身站姿,保留少量正面信息,强调面部轮廓、胸肩结构与主要装备层次。',
|
|
'背向斜侧身站姿,保留少量侧脸信息,突出背部服饰层次、武器挂载与轮廓识别。',
|
|
];
|
|
|
|
export function buildQuickEditSizeOptions(currentSize: string) {
|
|
return Array.from(new Set([currentSize, ...QUICK_EDIT_SIZE_PRESETS]));
|
|
}
|
|
|
|
export function buildQuickEditModelOptions(currentModel: string) {
|
|
const options = [...QUICK_EDIT_MODEL_OPTIONS];
|
|
return options.some((option) => option.value === currentModel)
|
|
? options
|
|
: [{ label: currentModel, value: currentModel }, ...options];
|
|
}
|
|
|
|
export function buildCharacterSpecPrompt(values: SpecFormValues) {
|
|
return [
|
|
'生成2D 角色美术视觉规范设定图,纯白底板,整齐排布全身标准立绘;固定统一头身比例、勾线粗细恒定;展示待机行走攻击基础动作帧样例,重心对齐不变位,服饰配饰分层结构示意,搭配专属角色色卡标注色号,无多余杂物,精准尺寸标注,高清矢量规范稿',
|
|
'禁止模糊、笔触杂乱、光影方向混乱、比例畸形、3D 渲染、实景照片、水印、花纹堆砌、画面抖动错位效果、噪点,',
|
|
`玩法设计:${values.playSetting.trim() || DEFAULT_SPEC_FORM_VALUES.character.playSetting}`,
|
|
`美术风格:${values.artStyle.trim() || DEFAULT_SPEC_FORM_VALUES.character.artStyle}`,
|
|
`头身比:${values.bodyRatio.trim() || DEFAULT_SPEC_FORM_VALUES.character.bodyRatio}`,
|
|
`视角要求:${values.characterView.trim() || DEFAULT_SPEC_FORM_VALUES.character.characterView}`,
|
|
].join('\n');
|
|
}
|
|
|
|
export function buildUiSpecPrompt(values: SpecFormValues) {
|
|
return [
|
|
'生成一张完整游戏UI规范汇总设定展板,纯白色干净背景,Figma专业设计稿质感,矢量锐利线条,页面划分九大区域:色彩规范、字体规范、图标规范、按钮规范、组件规范、布局规范、特效规范、IP规范、主视觉。主视觉居中较大显示,其他八个区域环绕主视觉',
|
|
'',
|
|
`玩法设定:${values.playSetting.trim() || DEFAULT_SPEC_FORM_VALUES.ui.playSetting}`,
|
|
`美术风格:${values.artStyle.trim() || DEFAULT_SPEC_FORM_VALUES.ui.artStyle}`,
|
|
].join('\n');
|
|
}
|
|
|
|
export function buildIconSpecPrompt(values: SpecFormValues) {
|
|
return [
|
|
'生成一张游戏图标素材视觉规范展板,纯白色干净背景,展示按钮图标的统一视角、线条粗细、填充风格、描边、阴影、圆角、材质、状态层级和色彩规范,图标样例需要成组排列且风格高度统一。',
|
|
'',
|
|
`玩法设定:${values.playSetting.trim() || DEFAULT_SPEC_FORM_VALUES.icon.playSetting}`,
|
|
`美术风格:${values.artStyle.trim() || DEFAULT_SPEC_FORM_VALUES.icon.artStyle}`,
|
|
].join('\n');
|
|
}
|
|
|
|
export function buildSpecPrompt(
|
|
type: SpecGenerationType,
|
|
values: SpecFormValues,
|
|
hasReferenceImage = false,
|
|
) {
|
|
const prompt =
|
|
type === 'character'
|
|
? buildCharacterSpecPrompt(values)
|
|
: type === 'ui'
|
|
? buildUiSpecPrompt(values)
|
|
: type === 'icon'
|
|
? buildIconSpecPrompt(values)
|
|
: values.customPrompt.trim();
|
|
if (!hasReferenceImage) {
|
|
return prompt;
|
|
}
|
|
return [
|
|
'参考图生成规范:严格参考图1的构图、风格、材质、色彩、形状语言和视觉层级生成本次规范图;参考图只作为美术方向和规范语义依据,不要直接复制参考图中的文字、水印或无关背景。',
|
|
prompt,
|
|
].join('\n');
|
|
}
|
|
|
|
export function getLayerKindLabel(layer: CanvasLayer) {
|
|
if (layer.assetKind === 'spec') {
|
|
return '规范';
|
|
}
|
|
if (layer.assetKind === 'character') {
|
|
return '角色';
|
|
}
|
|
if (layer.assetKind === 'character-animation') {
|
|
return '动作';
|
|
}
|
|
if (layer.assetKind === 'icon') {
|
|
return '图标';
|
|
}
|
|
if (layer.assetKind === 'icon-spec') {
|
|
return '图标规范';
|
|
}
|
|
if (layer.assetKind === 'ui-design') {
|
|
return 'UI设计';
|
|
}
|
|
if (layer.assetKind === 'video' || layer.mediaType === 'video') {
|
|
return '视频';
|
|
}
|
|
if (layer.assetKind === 'sound-effect') {
|
|
return '音效';
|
|
}
|
|
if (layer.assetKind === 'background-music') {
|
|
return '背景音乐';
|
|
}
|
|
if (layer.mediaType === 'audio') {
|
|
return '音频';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function buildCharacterAnimationGenerationInputs(
|
|
promptText: string,
|
|
sourceLayer: CanvasLayer,
|
|
): CanvasGenerationInputs {
|
|
return {
|
|
fields: createGenerationInputField('动作描述', promptText),
|
|
references: [
|
|
{
|
|
title: '角色图片',
|
|
label: sourceLayer.title,
|
|
src: sourceLayer.objectKey?.trim() || sourceLayer.src,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
export function formatLayerImageType(layer: CanvasLayer) {
|
|
if (layer.assetKind === 'spec') {
|
|
return '规范图片';
|
|
}
|
|
if (layer.assetKind === 'character') {
|
|
return '角色图片';
|
|
}
|
|
if (layer.assetKind === 'character-animation') {
|
|
return '角色动作图片';
|
|
}
|
|
if (layer.assetKind === 'icon') {
|
|
return '图标素材图片';
|
|
}
|
|
if (layer.assetKind === 'icon-spec') {
|
|
return '图标规范图片';
|
|
}
|
|
if (layer.assetKind === 'ui-design') {
|
|
return 'UI设计图';
|
|
}
|
|
if (layer.assetKind === 'video' || layer.mediaType === 'video') {
|
|
return '生成视频';
|
|
}
|
|
if (layer.assetKind === 'sound-effect') {
|
|
return '游戏音效';
|
|
}
|
|
if (layer.assetKind === 'background-music') {
|
|
return '游戏背景音乐';
|
|
}
|
|
if (layer.mediaType === 'audio') {
|
|
return '生成音频';
|
|
}
|
|
return isGeneratedLayer(layer) ? '生成图片' : '上传图片';
|
|
}
|
|
|
|
export function calculateCharacterAnimationPrice(
|
|
resolution: EditorCharacterAnimationResolution,
|
|
durationSeconds: number,
|
|
) {
|
|
return (
|
|
EDITOR_GENERATION_MUD_POINT_CONFIG.characterAnimationPerSecond[resolution] *
|
|
durationSeconds
|
|
);
|
|
}
|
|
|
|
export function calculateEditorVideoPrice(
|
|
resolution: '480p' | '720p',
|
|
durationSeconds: number,
|
|
) {
|
|
return (
|
|
EDITOR_GENERATION_MUD_POINT_CONFIG.videoPerSecond[resolution] *
|
|
durationSeconds
|
|
);
|
|
}
|
|
|
|
export function resolveCharacterAnimationSourceImageSrc(layer: CanvasLayer) {
|
|
// 中文注释:角色图已持久化到 OSS 时优先传 objectKey,避免把大 Data URL 塞进 JSON 请求体触发 body limit。
|
|
return layer.objectKey?.trim() || layer.src;
|
|
}
|
|
|
|
export function createCanvasLayerReference(
|
|
layer: CanvasLayer,
|
|
): CharacterReferenceImage {
|
|
const mediaType = layer.mediaType === 'video' || layer.mediaType === 'audio'
|
|
? layer.mediaType
|
|
: undefined;
|
|
return {
|
|
id: `canvas-${layer.id}`,
|
|
label: layer.title,
|
|
src: layer.objectKey?.trim() || layer.src,
|
|
...(mediaType ? { mediaType } : {}),
|
|
objectKey: layer.objectKey ?? undefined,
|
|
assetObjectId: layer.assetObjectId ?? undefined,
|
|
};
|
|
}
|
|
|
|
export function appendLimitedQuickEditReferences(
|
|
references: CharacterReferenceImage[] | undefined,
|
|
nextReferences: CharacterReferenceImage[],
|
|
) {
|
|
const imageReferences = nextReferences.filter(
|
|
(reference) => (reference.mediaType ?? 'image') === 'image',
|
|
);
|
|
return [
|
|
...(references ?? []),
|
|
...imageReferences,
|
|
].slice(0, QUICK_EDIT_REFERENCE_LIMIT);
|
|
}
|
|
|
|
export function normalizeQuickEditPromptSourceReference(
|
|
prompt: string,
|
|
sourceReferenceIndex: number,
|
|
) {
|
|
const normalizedIndex = Math.max(1, sourceReferenceIndex);
|
|
const sourceReferenceLabel = `图${normalizedIndex}`;
|
|
return prompt
|
|
.replace(/原图|当前图片|当前图/gu, sourceReferenceLabel)
|
|
.replace(/图\s*1(?!\d)/gu, sourceReferenceLabel);
|
|
}
|
|
|
|
export function createGenerationInputField(
|
|
title: string,
|
|
value: string | null | undefined,
|
|
): CanvasGenerationInputField[] {
|
|
const normalizedValue = value?.trim();
|
|
return normalizedValue ? [{ title, value: normalizedValue }] : [];
|
|
}
|
|
|
|
export function buildImageGenerationInputs(
|
|
prompt: string,
|
|
references?: CharacterReferenceImage[],
|
|
): CanvasGenerationInputs {
|
|
return {
|
|
fields: createGenerationInputField('生成提示词', prompt),
|
|
references: (references ?? []).map((reference, index) => ({
|
|
title: `参考图 ${index + 1}`,
|
|
label: reference.label,
|
|
src: reference.src,
|
|
})),
|
|
};
|
|
}
|
|
|
|
export function buildVideoGenerationInputs(
|
|
prompt: string,
|
|
references?: CharacterReferenceImage[],
|
|
): CanvasGenerationInputs {
|
|
const mediaIndexes = {
|
|
image: 0,
|
|
video: 0,
|
|
audio: 0,
|
|
};
|
|
return {
|
|
fields: createGenerationInputField('视频描述', prompt),
|
|
references: (references ?? []).map((reference) => {
|
|
const mediaType = reference.mediaType ?? 'image';
|
|
mediaIndexes[mediaType] += 1;
|
|
const title =
|
|
mediaType === 'video'
|
|
? `参考视频 ${mediaIndexes.video}`
|
|
: mediaType === 'audio'
|
|
? `参考音频 ${mediaIndexes.audio}`
|
|
: `参考图 ${mediaIndexes.image}`;
|
|
return {
|
|
title,
|
|
label: reference.label,
|
|
src: reference.src,
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
|
|
export function buildSoundEffectGenerationInputs(
|
|
sound: string,
|
|
soundType: string,
|
|
tempo: number | null | undefined,
|
|
): CanvasGenerationInputs {
|
|
return {
|
|
fields: [
|
|
...createGenerationInputField('sound', sound),
|
|
...createGenerationInputField('type', soundType),
|
|
...(tempo
|
|
? createGenerationInputField('tempo', `${tempo}BPM`)
|
|
: []),
|
|
],
|
|
references: [],
|
|
};
|
|
}
|
|
|
|
export function buildBackgroundMusicGenerationInputs(
|
|
gptDescriptionPrompt: string,
|
|
): CanvasGenerationInputs {
|
|
return {
|
|
fields: createGenerationInputField(
|
|
'gpt_description_prompt',
|
|
gptDescriptionPrompt,
|
|
),
|
|
references: [],
|
|
};
|
|
}
|
|
|
|
export function buildSpecGenerationInputs(
|
|
specType: SpecGenerationType,
|
|
values: SpecFormValues,
|
|
reference?: CharacterReferenceImage | null,
|
|
): CanvasGenerationInputs {
|
|
const references = reference
|
|
? [{ title: '参考图', label: reference.label, src: reference.src }]
|
|
: [];
|
|
if (specType === 'custom') {
|
|
return {
|
|
fields: createGenerationInputField('自定义规范提示词', values.customPrompt),
|
|
references,
|
|
};
|
|
}
|
|
|
|
const baseFields = [
|
|
...createGenerationInputField('玩法设定', values.playSetting),
|
|
...createGenerationInputField('美术风格', values.artStyle),
|
|
];
|
|
if (specType === 'character') {
|
|
baseFields.push(
|
|
...createGenerationInputField('头身比', values.bodyRatio),
|
|
...createGenerationInputField('角色视角', values.characterView),
|
|
);
|
|
}
|
|
return {
|
|
fields: baseFields,
|
|
references,
|
|
};
|
|
}
|
|
|
|
export function buildCharacterGenerationInputs(
|
|
prompt: string,
|
|
specReference: CharacterReferenceImage | null | undefined,
|
|
references: CharacterReferenceImage[] | undefined,
|
|
): CanvasGenerationInputs {
|
|
return {
|
|
fields: createGenerationInputField('角色设定', prompt),
|
|
references: [
|
|
...(specReference
|
|
? [
|
|
{
|
|
title: '角色规范',
|
|
label: specReference.label,
|
|
src: specReference.src,
|
|
},
|
|
]
|
|
: []),
|
|
...(references ?? []).map((reference, index) => ({
|
|
title: `常规参考图 ${index + 1}`,
|
|
label: reference.label,
|
|
src: reference.src,
|
|
})),
|
|
],
|
|
};
|
|
}
|
|
|
|
export function buildUiDesignGenerationInputs(
|
|
prompt: string,
|
|
specReference: CharacterReferenceImage | null | undefined,
|
|
): CanvasGenerationInputs {
|
|
return {
|
|
fields: createGenerationInputField('用户输入', prompt),
|
|
references: specReference
|
|
? [
|
|
{
|
|
title: '图标规范',
|
|
label: specReference.label,
|
|
src: specReference.src,
|
|
},
|
|
]
|
|
: [],
|
|
};
|
|
}
|
|
|
|
export function buildIconGenerationInputs(
|
|
iconDescriptions: string[],
|
|
specReference: CharacterReferenceImage,
|
|
): CanvasGenerationInputs {
|
|
return {
|
|
fields: [
|
|
{
|
|
title: '素材描述',
|
|
value: iconDescriptions.join('\n'),
|
|
},
|
|
],
|
|
references: [
|
|
{
|
|
title: '图标规范',
|
|
label: specReference.label,
|
|
src: specReference.src,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
export function buildEditGenerationInputs(
|
|
title: '修改要求' | '快速编辑提示词' | '重绘提示词',
|
|
prompt: string,
|
|
sourceLayer: CanvasLayer,
|
|
): CanvasGenerationInputs {
|
|
return {
|
|
fields: createGenerationInputField(title, prompt),
|
|
references: [
|
|
{
|
|
title: '参考图',
|
|
label: sourceLayer.title,
|
|
src: sourceLayer.src,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
export function buildQuickEditGenerationInputs(
|
|
title: '快速编辑提示词' | '重绘提示词',
|
|
prompt: string,
|
|
sourceLayer: CanvasLayer,
|
|
references: CharacterReferenceImage[] | undefined,
|
|
): CanvasGenerationInputs {
|
|
const limitedReferences = (references ?? []).slice(
|
|
0,
|
|
QUICK_EDIT_REFERENCE_LIMIT,
|
|
);
|
|
return {
|
|
fields: createGenerationInputField(title, prompt),
|
|
references: [
|
|
...limitedReferences.map((reference, index) => ({
|
|
title: `参考图 ${index + 1}`,
|
|
label: reference.label,
|
|
src: reference.src,
|
|
})),
|
|
{
|
|
title: `参考图 ${limitedReferences.length + 1}`,
|
|
label: sourceLayer.title,
|
|
src: sourceLayer.objectKey?.trim() || sourceLayer.src,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
export function isCanvasGenerationDialog(
|
|
dialog: GenerateDialogState | null,
|
|
): dialog is CanvasGenerationDialogState {
|
|
return Boolean(
|
|
dialog?.id &&
|
|
(dialog.mode === 'generate' ||
|
|
dialog.mode === 'spec' ||
|
|
dialog.mode === 'character' ||
|
|
dialog.mode === 'icon' ||
|
|
dialog.mode === 'ui-design' ||
|
|
dialog.mode === 'quick-edit' ||
|
|
dialog.mode === 'character-animation' ||
|
|
dialog.mode === 'video' ||
|
|
dialog.mode === 'audio-sound-effect' ||
|
|
dialog.mode === 'audio-background-music'),
|
|
);
|
|
}
|
|
|
|
export function getGenerationFrameAriaLabel(
|
|
dialog: CanvasGenerationDialogState,
|
|
) {
|
|
if (dialog.mode === 'character') {
|
|
return '角色生成占位图';
|
|
}
|
|
if (dialog.mode === 'spec') {
|
|
return '规范生成占位图';
|
|
}
|
|
if (dialog.mode === 'icon') {
|
|
return '图标素材生成占位图';
|
|
}
|
|
if (dialog.mode === 'ui-design') {
|
|
return 'UI设计图生成占位图';
|
|
}
|
|
if (dialog.mode === 'quick-edit') {
|
|
return '快速编辑生成占位图';
|
|
}
|
|
if (dialog.mode === 'video') {
|
|
return '视频生成占位图';
|
|
}
|
|
if (dialog.mode === 'character-animation') {
|
|
return '角色动作生成占位图';
|
|
}
|
|
if (dialog.mode === 'audio-sound-effect') {
|
|
return '音效生成占位图';
|
|
}
|
|
if (dialog.mode === 'audio-background-music') {
|
|
return '背景音乐生成占位图';
|
|
}
|
|
return '图像生成占位图';
|
|
}
|
|
|
|
export function getGenerationFrameLabel(dialog: CanvasGenerationDialogState) {
|
|
if (dialog.mode === 'character') {
|
|
return 'Character Generator';
|
|
}
|
|
if (dialog.mode === 'spec') {
|
|
return 'Spec Generator';
|
|
}
|
|
if (dialog.mode === 'icon') {
|
|
return 'Icon Generator';
|
|
}
|
|
if (dialog.mode === 'ui-design') {
|
|
return 'UI Design Generator';
|
|
}
|
|
if (dialog.mode === 'quick-edit') {
|
|
return 'Quick Edit Generator';
|
|
}
|
|
if (dialog.mode === 'video') {
|
|
return 'Video Generator';
|
|
}
|
|
if (dialog.mode === 'character-animation') {
|
|
return 'Action Generator';
|
|
}
|
|
if (dialog.mode === 'audio-sound-effect') {
|
|
return 'Sound Generator';
|
|
}
|
|
if (dialog.mode === 'audio-background-music') {
|
|
return 'Music Generator';
|
|
}
|
|
return 'Image Generator';
|
|
}
|
|
|
|
export function resolveImageGenerationErrorMessage(error: unknown) {
|
|
if (
|
|
error instanceof ApiClientError &&
|
|
(error.status === 401 || error.status === 403)
|
|
) {
|
|
return '请先登录后再生成图片';
|
|
}
|
|
|
|
return error instanceof Error && error.message.trim()
|
|
? error.message
|
|
: '生成图片失败';
|
|
}
|