de9eaef28f
生成输入参考图只保存 refType/refId 行引用 生成器布局快照只保存参考图行指针并在加载时恢复临时源 上传参考图优先创建项目资源行并回写 resourceId 元数据弹窗改为展示参考图行引用信息 更新图片画布技术文档和项目决策记录
1144 lines
37 KiB
TypeScript
1144 lines
37 KiB
TypeScript
import { ApiClientError } from '../../services/apiClient';
|
||
import type {
|
||
EditorCharacterAnimationRatio,
|
||
EditorCharacterAnimationResolution,
|
||
} from '../../services/image-editor/editorProjectClient';
|
||
import { isGeneratedLayer } from './ImageCanvasEditorModel';
|
||
import type {
|
||
CanvasGenerationDialogState,
|
||
CanvasGenerationInputField,
|
||
CanvasGenerationInputReference,
|
||
CanvasGenerationInputs,
|
||
CanvasLayer,
|
||
CharacterReferenceImage,
|
||
GenerateDialogState,
|
||
PublicationMaterialsGameInfo,
|
||
PublicationMaterialsWorkflowId,
|
||
SpecFormValues,
|
||
SpecGenerationType,
|
||
} from './ImageCanvasEditorTypes';
|
||
import {
|
||
getPublicationMaterialsWorkflow,
|
||
type PublicationMaterialsWorkflow,
|
||
} from './ImageCanvasPublicationMaterialsModel';
|
||
|
||
// 中文注释:与 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_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 PUBLICATION_FRAME_ORIGINAL_SIZE: Record<
|
||
PublicationMaterialsWorkflowId,
|
||
{ width: number; height: number }
|
||
> = {
|
||
'publication-cover-image': { width: 720, height: 540 },
|
||
'publication-detail-gallery': { width: 720, height: 1280 },
|
||
'publication-promo-poster': { width: 1280, height: 720 },
|
||
};
|
||
export const PUBLICATION_FRAME_DISPLAY_SIZE: Record<
|
||
PublicationMaterialsWorkflowId,
|
||
{ width: number; height: number }
|
||
> = {
|
||
'publication-cover-image': { width: 480, height: 360 },
|
||
'publication-detail-gallery': { width: 360, height: 640 },
|
||
'publication-promo-poster': { 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;
|
||
const IMAGE_MODEL_NANOBANANA_ALIASES = new Set([
|
||
IMAGE_MODEL_NANOBANANA2,
|
||
'nanobanana2',
|
||
'nano-banana',
|
||
]);
|
||
export const SPEC_GENERATION_MODEL = IMAGE_MODEL_GPT_IMAGE_2;
|
||
export const EDITOR_IMAGE_MODEL_MUD_POINT_CONFIG = {
|
||
[IMAGE_MODEL_NANOBANANA2]: EDITOR_GENERATION_MUD_POINT_CONFIG.image,
|
||
// 中文注释:gpt-image-2 作为更高成本图片模型独立计费,避免切换模型后按钮价格不变。
|
||
[IMAGE_MODEL_GPT_IMAGE_2]: 20,
|
||
} as const;
|
||
export const EDITOR_SPEC_MODEL_MUD_POINT_CONFIG = {
|
||
[IMAGE_MODEL_GPT_IMAGE_2]: EDITOR_GENERATION_MUD_POINT_CONFIG.spec,
|
||
} as const;
|
||
export const SPEC_GENERATION_COST =
|
||
EDITOR_SPEC_MODEL_MUD_POINT_CONFIG[SPEC_GENERATION_MODEL];
|
||
export const SPEC_GENERATION_ASPECT_RATIO = '16:9';
|
||
export const SPEC_GENERATION_IMAGE_SIZE = '2K';
|
||
export const ICON_DESCRIPTION_LIMIT = 100;
|
||
export const DEFAULT_PUBLICATION_GAME_INFO: PublicationMaterialsGameInfo = {
|
||
gameName: '',
|
||
gameCategories: '',
|
||
gameDescription: '',
|
||
};
|
||
export const DEFAULT_ICON_DESCRIPTIONS = [
|
||
'返回按钮',
|
||
'设置按钮',
|
||
'下一关按钮',
|
||
'提示按钮',
|
||
'原图按钮',
|
||
'冻结按钮',
|
||
];
|
||
export const UI_DESIGN_ASSET_EXTRACTION_PROMPT =
|
||
'提取画面中的所有独立并整理成spritesheet';
|
||
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;
|
||
|
||
type EditorImageFrameSize = {
|
||
width: number;
|
||
height: number;
|
||
};
|
||
|
||
function getSafeImageSizeValue(imageSize: string | null | undefined) {
|
||
return (imageSize ?? '1K').trim().toUpperCase();
|
||
}
|
||
|
||
function getImageSizeLongEdge(imageSize: string | null | undefined) {
|
||
const normalizedImageSize = getSafeImageSizeValue(imageSize);
|
||
if (normalizedImageSize === '0.5K') {
|
||
return 512;
|
||
}
|
||
if (normalizedImageSize === '2K') {
|
||
return 2048;
|
||
}
|
||
return 1024;
|
||
}
|
||
|
||
function resolveAspectRatioNumbers(aspectRatio: string | null | undefined) {
|
||
const [rawWidth = 1, rawHeight = 1] = (aspectRatio ?? '1:1')
|
||
.split(':')
|
||
.map((value) => Number(value));
|
||
const width = Number.isFinite(rawWidth) && rawWidth > 0 ? rawWidth : 1;
|
||
const height = Number.isFinite(rawHeight) && rawHeight > 0 ? rawHeight : 1;
|
||
return { width, height };
|
||
}
|
||
|
||
function resolveImageFrameSizeFromRatio({
|
||
aspectRatio,
|
||
imageSize,
|
||
}: {
|
||
aspectRatio: string | null | undefined;
|
||
imageSize: string | null | undefined;
|
||
}): EditorImageFrameSize {
|
||
const longEdge = getImageSizeLongEdge(imageSize);
|
||
const ratio = resolveAspectRatioNumbers(aspectRatio);
|
||
if (ratio.width >= ratio.height) {
|
||
return {
|
||
width: longEdge,
|
||
height: Math.max(1, Math.round((longEdge * ratio.height) / ratio.width)),
|
||
};
|
||
}
|
||
return {
|
||
width: Math.max(1, Math.round((longEdge * ratio.width) / ratio.height)),
|
||
height: longEdge,
|
||
};
|
||
}
|
||
|
||
export function resolveEditorImageGenerationPixelSize({
|
||
aspectRatio,
|
||
imageSize,
|
||
}: {
|
||
model: string | null | undefined;
|
||
aspectRatio: string | null | undefined;
|
||
imageSize: string | null | undefined;
|
||
}): EditorImageFrameSize {
|
||
return resolveImageFrameSizeFromRatio({ aspectRatio, imageSize });
|
||
}
|
||
|
||
export function resizeGenerationPlaceholderToImageSelection(
|
||
dialog: GenerateDialogState,
|
||
): GenerateDialogState {
|
||
if (!dialog.placeholder) {
|
||
return dialog;
|
||
}
|
||
const size = resolveEditorImageGenerationPixelSize({
|
||
model: dialog.imageModel,
|
||
aspectRatio: dialog.aspectRatio,
|
||
imageSize: dialog.imageSize,
|
||
});
|
||
const centerX = dialog.placeholder.x + dialog.placeholder.width / 2;
|
||
const centerY = dialog.placeholder.y + dialog.placeholder.height / 2;
|
||
return {
|
||
...dialog,
|
||
placeholder: {
|
||
...dialog.placeholder,
|
||
x: centerX - size.width / 2,
|
||
y: centerY - size.height / 2,
|
||
width: size.width,
|
||
height: size.height,
|
||
originalWidth: size.width,
|
||
originalHeight: size.height,
|
||
},
|
||
};
|
||
}
|
||
|
||
function resolveEvenVideoWidth(height: number, aspectRatio: string | null | undefined) {
|
||
const ratio = resolveAspectRatioNumbers(aspectRatio ?? '16:9');
|
||
const rawWidth = Math.max(1, Math.round((height * ratio.width) / ratio.height));
|
||
return rawWidth % 2 === 0 ? rawWidth : rawWidth + 1;
|
||
}
|
||
|
||
export function resolveEditorVideoGenerationPixelSize({
|
||
aspectRatio,
|
||
resolution,
|
||
}: {
|
||
aspectRatio: string | null | undefined;
|
||
resolution: '480p' | '720p' | string | null | undefined;
|
||
}): EditorImageFrameSize {
|
||
const height = resolution === '720p' ? 720 : 480;
|
||
return {
|
||
width: resolveEvenVideoWidth(height, aspectRatio),
|
||
height,
|
||
};
|
||
}
|
||
|
||
export function resizeGenerationPlaceholderToVideoSelection(
|
||
dialog: GenerateDialogState,
|
||
): GenerateDialogState {
|
||
if (!dialog.placeholder) {
|
||
return dialog;
|
||
}
|
||
const size = resolveEditorVideoGenerationPixelSize({
|
||
aspectRatio: dialog.videoAspectRatio,
|
||
resolution: dialog.videoResolution,
|
||
});
|
||
const centerX = dialog.placeholder.x + dialog.placeholder.width / 2;
|
||
const centerY = dialog.placeholder.y + dialog.placeholder.height / 2;
|
||
return {
|
||
...dialog,
|
||
placeholder: {
|
||
...dialog.placeholder,
|
||
x: centerX - size.width / 2,
|
||
y: centerY - size.height / 2,
|
||
width: size.width,
|
||
height: size.height,
|
||
originalWidth: size.width,
|
||
originalHeight: size.height,
|
||
},
|
||
};
|
||
}
|
||
export const VIDEO_MODEL_SEEDANCE_2 = 'seedance2.0';
|
||
export const VIDEO_MODEL_SEEDANCE_2_FAST = 'seedance2.0-fast';
|
||
export const VIDEO_MODEL_KLING_3 = 'kling3.0';
|
||
export const VIDEO_MODEL_KLING_3_OMNI = 'kling3.0-omni';
|
||
export const VIDEO_MODEL_VEO_3_1 = 'veo3.1';
|
||
export const VIDEO_MODEL_VEO_3_1_FAST = 'veo3.1-fast';
|
||
export const DEFAULT_VIDEO_MODEL = VIDEO_MODEL_SEEDANCE_2_FAST;
|
||
export const CHARACTER_ANIMATION_MODEL = VIDEO_MODEL_SEEDANCE_2_FAST;
|
||
export const SOUND_EFFECT_MODEL_VIDU = 'audio1.0';
|
||
export const DEFAULT_SOUND_EFFECT_MODEL = SOUND_EFFECT_MODEL_VIDU;
|
||
export const DEFAULT_SOUND_EFFECT_DURATION_SECONDS = 5;
|
||
export const SOUND_EFFECT_DURATION_OPTIONS = Array.from(
|
||
{ length: 9 },
|
||
(_, index) => index + 2,
|
||
);
|
||
export const BACKGROUND_MUSIC_MODEL_SUNO = 'chirp-v5';
|
||
export const DEFAULT_BACKGROUND_MUSIC_MODEL = BACKGROUND_MUSIC_MODEL_SUNO;
|
||
const EDITOR_VIDEO_RESOLUTION_RATE_CONFIG = {
|
||
'480p': EDITOR_GENERATION_MUD_POINT_CONFIG.videoPerSecond['480p'],
|
||
'720p': EDITOR_GENERATION_MUD_POINT_CONFIG.videoPerSecond['720p'],
|
||
} as const;
|
||
const EDITOR_VIDEO_STANDARD_RESOLUTION_RATE_CONFIG = {
|
||
'480p': 12,
|
||
'720p': 24,
|
||
} as const;
|
||
const EDITOR_VIDEO_KLING_RESOLUTION_RATE_CONFIG = {
|
||
'480p': 15,
|
||
'720p': 30,
|
||
} as const;
|
||
const EDITOR_VIDEO_KLING_OMNI_RESOLUTION_RATE_CONFIG = {
|
||
'480p': 20,
|
||
'720p': 40,
|
||
} as const;
|
||
export const EDITOR_VIDEO_MODEL_MUD_POINT_CONFIG = {
|
||
[VIDEO_MODEL_SEEDANCE_2_FAST]: EDITOR_VIDEO_RESOLUTION_RATE_CONFIG,
|
||
[VIDEO_MODEL_SEEDANCE_2]: EDITOR_VIDEO_STANDARD_RESOLUTION_RATE_CONFIG,
|
||
[VIDEO_MODEL_KLING_3]: EDITOR_VIDEO_KLING_RESOLUTION_RATE_CONFIG,
|
||
[VIDEO_MODEL_KLING_3_OMNI]: EDITOR_VIDEO_KLING_OMNI_RESOLUTION_RATE_CONFIG,
|
||
// 中文注释:前端不展示 Veo 入口,但类型与后端兼容层仍可接收旧布局回放,定价表必须同步覆盖。
|
||
[VIDEO_MODEL_VEO_3_1]: EDITOR_VIDEO_RESOLUTION_RATE_CONFIG,
|
||
[VIDEO_MODEL_VEO_3_1_FAST]: EDITOR_VIDEO_RESOLUTION_RATE_CONFIG,
|
||
} as const;
|
||
export const EDITOR_CHARACTER_ANIMATION_MODEL_MUD_POINT_CONFIG = {
|
||
[CHARACTER_ANIMATION_MODEL]: {
|
||
'480p': EDITOR_GENERATION_MUD_POINT_CONFIG.characterAnimationPerSecond['480p'],
|
||
'720p': EDITOR_GENERATION_MUD_POINT_CONFIG.characterAnimationPerSecond['720p'],
|
||
},
|
||
} as const;
|
||
export const EDITOR_SOUND_EFFECT_MODEL_MUD_POINT_CONFIG = {
|
||
[SOUND_EFFECT_MODEL_VIDU]: EDITOR_GENERATION_MUD_POINT_CONFIG.soundEffect,
|
||
} as const;
|
||
export const EDITOR_BACKGROUND_MUSIC_MODEL_MUD_POINT_CONFIG = {
|
||
[BACKGROUND_MUSIC_MODEL_SUNO]: EDITOR_GENERATION_MUD_POINT_CONFIG.backgroundMusic,
|
||
} as const;
|
||
export const EDITOR_SOUND_EFFECT_MODEL_OPTIONS = [
|
||
{ label: 'Vidu', value: SOUND_EFFECT_MODEL_VIDU },
|
||
] as const;
|
||
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: 854, height: 480 };
|
||
export const VIDEO_FRAME_DISPLAY_SIZE = { width: 854, height: 480 };
|
||
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: VIDEO_MODEL_KLING_3 },
|
||
{ label: 'Kling 3.0 Omni', value: VIDEO_MODEL_KLING_3_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 normalizeEditorImageModel(
|
||
model: string | null | undefined,
|
||
) {
|
||
const normalizedModel = model?.trim();
|
||
if (!normalizedModel) {
|
||
return DEFAULT_IMAGE_MODEL;
|
||
}
|
||
if (IMAGE_MODEL_NANOBANANA_ALIASES.has(normalizedModel)) {
|
||
return IMAGE_MODEL_NANOBANANA2;
|
||
}
|
||
return normalizedModel;
|
||
}
|
||
|
||
export function buildQuickEditModelOptions(currentModel: string) {
|
||
const normalizedCurrentModel = normalizeEditorImageModel(currentModel);
|
||
const options = [...QUICK_EDIT_MODEL_OPTIONS];
|
||
return options.some((option) => option.value === normalizedCurrentModel)
|
||
? options
|
||
: [{ label: normalizedCurrentModel, value: normalizedCurrentModel }, ...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-spritesheet') {
|
||
return '图集';
|
||
}
|
||
if (layer.assetKind === 'icon-spec') {
|
||
return '图标规范';
|
||
}
|
||
if (layer.assetKind === 'publication-material') {
|
||
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: createLayerGenerationInputReference('角色图片', sourceLayer),
|
||
};
|
||
}
|
||
|
||
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-spritesheet') {
|
||
return '图标素材图集';
|
||
}
|
||
if (layer.assetKind === 'icon-spec') {
|
||
return '图标规范图片';
|
||
}
|
||
if (layer.assetKind === 'publication-material') {
|
||
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(
|
||
model: string | null | undefined,
|
||
resolution: EditorCharacterAnimationResolution,
|
||
durationSeconds: number,
|
||
) {
|
||
const normalizedModel = (model ?? CHARACTER_ANIMATION_MODEL).trim();
|
||
const modelRates =
|
||
EDITOR_CHARACTER_ANIMATION_MODEL_MUD_POINT_CONFIG[
|
||
normalizedModel as keyof typeof EDITOR_CHARACTER_ANIMATION_MODEL_MUD_POINT_CONFIG
|
||
] ?? EDITOR_CHARACTER_ANIMATION_MODEL_MUD_POINT_CONFIG[CHARACTER_ANIMATION_MODEL];
|
||
return modelRates[resolution] * durationSeconds;
|
||
}
|
||
|
||
export function calculateEditorVideoPrice(
|
||
model: string | null | undefined,
|
||
resolution: '480p' | '720p',
|
||
durationSeconds: number,
|
||
) {
|
||
const normalizedModel = (model ?? DEFAULT_VIDEO_MODEL).trim();
|
||
const modelRates =
|
||
EDITOR_VIDEO_MODEL_MUD_POINT_CONFIG[
|
||
normalizedModel as keyof typeof EDITOR_VIDEO_MODEL_MUD_POINT_CONFIG
|
||
] ?? EDITOR_VIDEO_MODEL_MUD_POINT_CONFIG[DEFAULT_VIDEO_MODEL];
|
||
return modelRates[resolution] * durationSeconds;
|
||
}
|
||
|
||
export function calculateEditorImageModelPrice(
|
||
model: string | null | undefined,
|
||
) {
|
||
const normalizedModel = normalizeEditorImageModel(model);
|
||
return (
|
||
EDITOR_IMAGE_MODEL_MUD_POINT_CONFIG[
|
||
normalizedModel as keyof typeof EDITOR_IMAGE_MODEL_MUD_POINT_CONFIG
|
||
] ?? EDITOR_GENERATION_MUD_POINT_CONFIG.image
|
||
);
|
||
}
|
||
|
||
export function calculateEditorSpecGenerationPrice(
|
||
model: string | null | undefined = SPEC_GENERATION_MODEL,
|
||
) {
|
||
const normalizedModel = (model ?? SPEC_GENERATION_MODEL).trim();
|
||
return (
|
||
EDITOR_SPEC_MODEL_MUD_POINT_CONFIG[
|
||
normalizedModel as keyof typeof EDITOR_SPEC_MODEL_MUD_POINT_CONFIG
|
||
] ?? EDITOR_SPEC_MODEL_MUD_POINT_CONFIG[SPEC_GENERATION_MODEL]
|
||
);
|
||
}
|
||
|
||
export function calculateEditorImageGenerationPrice({
|
||
kind,
|
||
model,
|
||
}: {
|
||
kind?: string | null;
|
||
model?: string | null;
|
||
} = {}) {
|
||
const normalizedKind = kind?.trim();
|
||
if (normalizedKind === 'spec') {
|
||
return calculateEditorSpecGenerationPrice(model);
|
||
}
|
||
return calculateEditorImageModelPrice(model);
|
||
}
|
||
|
||
export function calculateEditorIconSpritesheetPrice(
|
||
model: string | null | undefined,
|
||
) {
|
||
return calculateEditorImageModelPrice(model);
|
||
}
|
||
|
||
export function calculateEditorUiDesignPrice(
|
||
model: string | null | undefined,
|
||
) {
|
||
return calculateEditorImageModelPrice(model);
|
||
}
|
||
|
||
export function calculateEditorSoundEffectPrice(
|
||
model: string | null | undefined,
|
||
) {
|
||
const normalizedModel = (model ?? DEFAULT_SOUND_EFFECT_MODEL).trim();
|
||
return (
|
||
EDITOR_SOUND_EFFECT_MODEL_MUD_POINT_CONFIG[
|
||
normalizedModel as keyof typeof EDITOR_SOUND_EFFECT_MODEL_MUD_POINT_CONFIG
|
||
] ?? EDITOR_SOUND_EFFECT_MODEL_MUD_POINT_CONFIG[DEFAULT_SOUND_EFFECT_MODEL]
|
||
);
|
||
}
|
||
|
||
export function calculateEditorBackgroundMusicPrice(
|
||
model: string | null | undefined = DEFAULT_BACKGROUND_MUSIC_MODEL,
|
||
) {
|
||
const normalizedModel = (model ?? DEFAULT_BACKGROUND_MUSIC_MODEL).trim();
|
||
return (
|
||
EDITOR_BACKGROUND_MUSIC_MODEL_MUD_POINT_CONFIG[
|
||
normalizedModel as keyof typeof EDITOR_BACKGROUND_MUSIC_MODEL_MUD_POINT_CONFIG
|
||
] ??
|
||
EDITOR_BACKGROUND_MUSIC_MODEL_MUD_POINT_CONFIG[
|
||
DEFAULT_BACKGROUND_MUSIC_MODEL
|
||
]
|
||
);
|
||
}
|
||
|
||
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,
|
||
resourceId: layer.resourceId,
|
||
sourceAssetId: layer.sourceAssetId ?? undefined,
|
||
};
|
||
}
|
||
|
||
export function createGenerationInputReference(
|
||
title: string,
|
||
reference: Pick<
|
||
CharacterReferenceImage,
|
||
'label' | 'resourceId' | 'sourceAssetId'
|
||
>,
|
||
): CanvasGenerationInputReference[] {
|
||
const resourceId = reference.resourceId?.trim();
|
||
if (
|
||
resourceId &&
|
||
!resourceId.startsWith('local-') &&
|
||
!resourceId.startsWith('generation-dialog:')
|
||
) {
|
||
return [
|
||
{
|
||
title,
|
||
label: reference.label,
|
||
refType: 'project-resource',
|
||
refId: resourceId,
|
||
},
|
||
];
|
||
}
|
||
const sourceAssetId = reference.sourceAssetId?.trim();
|
||
return sourceAssetId && !sourceAssetId.startsWith('upload-')
|
||
? [
|
||
{
|
||
title,
|
||
label: reference.label,
|
||
refType: 'asset',
|
||
refId: sourceAssetId,
|
||
},
|
||
]
|
||
: [];
|
||
}
|
||
|
||
export function createLayerGenerationInputReference(
|
||
title: string,
|
||
layer: CanvasLayer,
|
||
): CanvasGenerationInputReference[] {
|
||
return createGenerationInputReference(title, {
|
||
label: layer.title,
|
||
resourceId: layer.resourceId,
|
||
sourceAssetId: layer.sourceAssetId,
|
||
});
|
||
}
|
||
|
||
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 ?? []).flatMap((reference, index) =>
|
||
createGenerationInputReference(`参考图 ${index + 1}`, reference),
|
||
),
|
||
};
|
||
}
|
||
|
||
export function buildVideoGenerationInputs(
|
||
prompt: string,
|
||
references?: CharacterReferenceImage[],
|
||
): CanvasGenerationInputs {
|
||
const mediaIndexes = {
|
||
image: 0,
|
||
video: 0,
|
||
audio: 0,
|
||
};
|
||
return {
|
||
fields: createGenerationInputField('视频描述', prompt),
|
||
references: (references ?? []).flatMap((reference) => {
|
||
const mediaType = reference.mediaType ?? 'image';
|
||
mediaIndexes[mediaType] += 1;
|
||
const title =
|
||
mediaType === 'video'
|
||
? `参考视频 ${mediaIndexes.video}`
|
||
: mediaType === 'audio'
|
||
? `参考音频 ${mediaIndexes.audio}`
|
||
: `参考图 ${mediaIndexes.image}`;
|
||
return createGenerationInputReference(title, reference);
|
||
}),
|
||
};
|
||
}
|
||
|
||
export function buildSoundEffectGenerationInputs(
|
||
prompt: string,
|
||
model: string,
|
||
duration: number,
|
||
): CanvasGenerationInputs {
|
||
return {
|
||
fields: [
|
||
...createGenerationInputField('prompt', prompt),
|
||
...createGenerationInputField('model', model),
|
||
...createGenerationInputField('duration', `${duration}秒`),
|
||
],
|
||
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
|
||
? createGenerationInputReference('参考图', reference)
|
||
: [];
|
||
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
|
||
? createGenerationInputReference('角色规范', specReference)
|
||
: []),
|
||
...(references ?? []).flatMap((reference, index) =>
|
||
createGenerationInputReference(`常规参考图 ${index + 1}`, reference),
|
||
),
|
||
],
|
||
};
|
||
}
|
||
|
||
export function buildUiDesignGenerationInputs(
|
||
prompt: string,
|
||
specReference: CharacterReferenceImage | null | undefined,
|
||
): CanvasGenerationInputs {
|
||
return {
|
||
fields: createGenerationInputField('用户输入', prompt),
|
||
references: specReference
|
||
? createGenerationInputReference('图标规范', specReference)
|
||
: [],
|
||
};
|
||
}
|
||
|
||
function normalizePublicationGameInfo(
|
||
gameInfo: PublicationMaterialsGameInfo | null | undefined,
|
||
): PublicationMaterialsGameInfo {
|
||
return {
|
||
gameName: gameInfo?.gameName?.trim() ?? '',
|
||
gameCategories: gameInfo?.gameCategories?.trim() ?? '',
|
||
gameDescription: gameInfo?.gameDescription?.trim() ?? '',
|
||
};
|
||
}
|
||
|
||
export function buildPublicationMaterialsPrompt(
|
||
gameInfo: PublicationMaterialsGameInfo | null | undefined,
|
||
) {
|
||
const normalizedGameInfo = normalizePublicationGameInfo(gameInfo);
|
||
return [
|
||
...createGenerationInputField('游戏名', normalizedGameInfo.gameName),
|
||
...createGenerationInputField('游戏分类', normalizedGameInfo.gameCategories),
|
||
...createGenerationInputField(
|
||
'一句话描述游戏',
|
||
normalizedGameInfo.gameDescription,
|
||
),
|
||
]
|
||
.map((field) => `${field.title}:${field.value}`)
|
||
.join('\n');
|
||
}
|
||
|
||
export function buildPublicationMaterialsGenerationPrompt({
|
||
gameInfo,
|
||
workflow,
|
||
}: {
|
||
gameInfo: PublicationMaterialsGameInfo | null | undefined;
|
||
workflow: PublicationMaterialsWorkflow;
|
||
}) {
|
||
const inputPrompt = buildPublicationMaterialsPrompt(gameInfo);
|
||
return [
|
||
`【宣发素材类型】${workflow.promptLabel ?? workflow.label}`,
|
||
inputPrompt ? `【游戏输入】\n${inputPrompt}` : '【游戏输入】\n请根据参考图生成宣发素材。',
|
||
`【生图约束】\n${workflow.promptConstraints.join('\n')}`,
|
||
`【参考图约束】\n${workflow.referenceConstraints.join('\n')}`,
|
||
`【输出检查】\n${workflow.postProcessConstraints.join('\n')}`,
|
||
].join('\n\n');
|
||
}
|
||
|
||
export function buildPublicationMaterialsGenerationInputs(
|
||
gameInfo: PublicationMaterialsGameInfo | null | undefined,
|
||
references: CharacterReferenceImage[] | undefined,
|
||
): CanvasGenerationInputs {
|
||
const normalizedGameInfo = normalizePublicationGameInfo(gameInfo);
|
||
return {
|
||
fields: [
|
||
...createGenerationInputField('游戏名', normalizedGameInfo.gameName),
|
||
...createGenerationInputField('游戏分类', normalizedGameInfo.gameCategories),
|
||
...createGenerationInputField(
|
||
'一句话描述游戏',
|
||
normalizedGameInfo.gameDescription,
|
||
),
|
||
],
|
||
references: (references ?? []).flatMap((reference, index) =>
|
||
createGenerationInputReference(`宣发参考图 ${index + 1}`, reference),
|
||
),
|
||
};
|
||
}
|
||
|
||
export function buildIconGenerationInputs(
|
||
iconDescriptions: string[],
|
||
specReference: CharacterReferenceImage,
|
||
): CanvasGenerationInputs {
|
||
return {
|
||
fields: [
|
||
{
|
||
title: '素材描述',
|
||
value: iconDescriptions.join('\n'),
|
||
},
|
||
],
|
||
references: createGenerationInputReference('图标规范', specReference),
|
||
};
|
||
}
|
||
|
||
export function buildUiDesignAssetExtractionGenerationInputs(
|
||
sourceLayer: CanvasLayer,
|
||
): CanvasGenerationInputs {
|
||
return {
|
||
fields: createGenerationInputField(
|
||
'提取提示词',
|
||
UI_DESIGN_ASSET_EXTRACTION_PROMPT,
|
||
),
|
||
references: createLayerGenerationInputReference('UI设计图', sourceLayer),
|
||
};
|
||
}
|
||
|
||
export function buildEditGenerationInputs(
|
||
title: '修改要求' | '快速编辑提示词' | '重绘提示词',
|
||
prompt: string,
|
||
sourceLayer: CanvasLayer,
|
||
): CanvasGenerationInputs {
|
||
return {
|
||
fields: createGenerationInputField(title, prompt),
|
||
references: createLayerGenerationInputReference('参考图', sourceLayer),
|
||
};
|
||
}
|
||
|
||
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.flatMap((reference, index) =>
|
||
createGenerationInputReference(`参考图 ${index + 1}`, reference),
|
||
),
|
||
...createLayerGenerationInputReference(
|
||
`参考图 ${limitedReferences.length + 1}`,
|
||
sourceLayer,
|
||
),
|
||
],
|
||
};
|
||
}
|
||
|
||
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 === 'publication' ||
|
||
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 === 'publication') {
|
||
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 === 'publication') {
|
||
return `${getPublicationMaterialsWorkflow(
|
||
dialog.publicationWorkflowId ?? 'publication-cover-image',
|
||
).englishName} 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
|
||
: '生成图片失败';
|
||
}
|