314 lines
9.4 KiB
TypeScript
314 lines
9.4 KiB
TypeScript
export const AI_WASH_CONFIG_KEY = "ai_wash_config";
|
||
export const AI_WASH_DEFAULT_MODE_LABEL = "AI洗图";
|
||
export const AI_WASH_DEFAULT_IMAGE_SIZE = "2K";
|
||
export const AI_WASH_DEFAULT_HISTORY_LIMIT = 8;
|
||
export const AI_WASH_CONTROL_ORDER = [
|
||
"creativity",
|
||
"detail",
|
||
"lighting",
|
||
] as const;
|
||
|
||
export type AiWashControlId = (typeof AI_WASH_CONTROL_ORDER)[number];
|
||
export type AiWashControlKind = "model" | "prompt";
|
||
|
||
export type AiWashControlLevel = {
|
||
id: string;
|
||
label: string;
|
||
modelId: string;
|
||
promptFragment: string;
|
||
};
|
||
|
||
export type AiWashControl = {
|
||
id: AiWashControlId;
|
||
label: string;
|
||
helperText: string;
|
||
kind: AiWashControlKind;
|
||
levels: AiWashControlLevel[];
|
||
defaultLevel: number;
|
||
};
|
||
|
||
export type AiWashPreset = {
|
||
id: string;
|
||
title: string;
|
||
description: string;
|
||
prompt: string;
|
||
modelId: string;
|
||
enabled: boolean;
|
||
isDefault: boolean;
|
||
controls: AiWashControl[];
|
||
};
|
||
|
||
export type AiWashConfig = {
|
||
enabled: boolean;
|
||
modeLabel: string;
|
||
modeIconUrl: string;
|
||
hideModelSelector: boolean;
|
||
imageSize: string;
|
||
promptExtend: boolean;
|
||
historyLimit: number;
|
||
presets: AiWashPreset[];
|
||
};
|
||
|
||
function normalizeText(value: unknown, maxLength: number) {
|
||
return String(value || "").trim().slice(0, maxLength);
|
||
}
|
||
|
||
function createPresetId(title: string, index: number) {
|
||
const normalized = String(title || "")
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-")
|
||
.replace(/^-+|-+$/g, "");
|
||
|
||
return normalized || `ai-wash-preset-${index + 1}`;
|
||
}
|
||
|
||
function createLevelId(controlId: AiWashControlId, index: number) {
|
||
return `${controlId}-level-${index + 1}`;
|
||
}
|
||
|
||
function createDefaultControl(
|
||
controlId: AiWashControlId,
|
||
overrides?: Partial<AiWashControl>
|
||
): AiWashControl {
|
||
const defaults: Record<
|
||
AiWashControlId,
|
||
Pick<AiWashControl, "label" | "helperText" | "kind">
|
||
> = {
|
||
creativity: {
|
||
label: "创造力",
|
||
helperText: "每一档都可以单独指定生图模型,也可以补充少量提示词片段。",
|
||
kind: "model",
|
||
},
|
||
detail: {
|
||
label: "细节",
|
||
helperText: "每一档都可以补充默认提示词,控制画面的细节强弱与精修程度。",
|
||
kind: "prompt",
|
||
},
|
||
lighting: {
|
||
label: "光塑",
|
||
helperText: "每一档都可以补充默认提示词,控制光影层次、氛围和空间塑造。",
|
||
kind: "prompt",
|
||
},
|
||
};
|
||
|
||
return {
|
||
id: controlId,
|
||
label: overrides?.label || defaults[controlId].label,
|
||
helperText: overrides?.helperText || defaults[controlId].helperText,
|
||
kind: overrides?.kind || defaults[controlId].kind,
|
||
defaultLevel:
|
||
typeof overrides?.defaultLevel === "number"
|
||
? Math.max(0, Math.min(23, Math.floor(overrides.defaultLevel)))
|
||
: 1,
|
||
levels:
|
||
overrides?.levels && overrides.levels.length > 0
|
||
? overrides.levels
|
||
: Array.from({ length: 3 }, (_, index) => ({
|
||
id: createLevelId(controlId, index),
|
||
label: String(index + 1),
|
||
modelId: "",
|
||
promptFragment: "",
|
||
})),
|
||
};
|
||
}
|
||
|
||
function normalizeControlLevel(
|
||
controlId: AiWashControlId,
|
||
value: unknown,
|
||
index: number
|
||
): AiWashControlLevel {
|
||
const raw =
|
||
value && typeof value === "object" && !Array.isArray(value)
|
||
? (value as Record<string, unknown>)
|
||
: {};
|
||
|
||
return {
|
||
id: normalizeText(raw.id, 64) || createLevelId(controlId, index),
|
||
label: normalizeText(raw.label, 24) || String(index + 1),
|
||
modelId: normalizeText(raw.modelId, 128),
|
||
promptFragment: normalizeText(raw.promptFragment, 1000),
|
||
};
|
||
}
|
||
|
||
function normalizeControl(
|
||
controlId: AiWashControlId,
|
||
value: unknown,
|
||
fallback?: AiWashControl
|
||
): AiWashControl {
|
||
const base = fallback || createDefaultControl(controlId);
|
||
const raw =
|
||
value && typeof value === "object" && !Array.isArray(value)
|
||
? (value as Record<string, unknown>)
|
||
: {};
|
||
const levelsInput = Array.isArray(raw.levels) ? raw.levels : base.levels;
|
||
const levels = levelsInput
|
||
.map((level, index) => normalizeControlLevel(controlId, level, index))
|
||
.slice(0, 24);
|
||
|
||
const defaultLevel = Number.isFinite(Number(raw.defaultLevel))
|
||
? Number(raw.defaultLevel)
|
||
: base.defaultLevel;
|
||
|
||
return {
|
||
id: controlId,
|
||
label: normalizeText(raw.label, 24) || base.label,
|
||
helperText: normalizeText(raw.helperText, 200) || base.helperText,
|
||
kind:
|
||
raw.kind === "model" || raw.kind === "prompt" ? raw.kind : base.kind,
|
||
levels: levels.length > 0 ? levels : base.levels,
|
||
defaultLevel: Math.max(0, Math.min(levels.length - 1, Math.floor(defaultLevel))),
|
||
};
|
||
}
|
||
|
||
function normalizeControls(value: unknown): AiWashControl[] {
|
||
const rawList = Array.isArray(value) ? value : [];
|
||
|
||
return AI_WASH_CONTROL_ORDER.map((controlId) => {
|
||
const matched =
|
||
rawList.find((item) => {
|
||
const raw =
|
||
item && typeof item === "object" && !Array.isArray(item)
|
||
? (item as Record<string, unknown>)
|
||
: {};
|
||
return raw.id === controlId;
|
||
}) || null;
|
||
|
||
return normalizeControl(controlId, matched, createDefaultControl(controlId));
|
||
});
|
||
}
|
||
|
||
function normalizePreset(
|
||
value: unknown,
|
||
index: number,
|
||
fallback: AiWashPreset
|
||
): AiWashPreset {
|
||
const raw =
|
||
value && typeof value === "object" && !Array.isArray(value)
|
||
? (value as Record<string, unknown>)
|
||
: {};
|
||
const title = normalizeText(raw.title, 48) || fallback.title;
|
||
const description =
|
||
normalizeText(raw.description, 200) || fallback.description;
|
||
const prompt = normalizeText(raw.prompt, 4000) || fallback.prompt;
|
||
|
||
return {
|
||
id: normalizeText(raw.id, 64) || createPresetId(title, index),
|
||
title,
|
||
description,
|
||
prompt,
|
||
modelId: normalizeText(raw.modelId, 128),
|
||
enabled: raw.enabled !== false,
|
||
isDefault: raw.isDefault === true,
|
||
controls: normalizeControls(raw.controls),
|
||
};
|
||
}
|
||
|
||
function ensureDefaultPreset(presets: AiWashPreset[]) {
|
||
if (presets.length === 0) return presets;
|
||
if (presets.some((preset) => preset.isDefault && preset.enabled)) {
|
||
return presets;
|
||
}
|
||
return presets.map((preset, index) => ({
|
||
...preset,
|
||
isDefault: index === 0,
|
||
}));
|
||
}
|
||
|
||
function defaultControlsForPreset(): AiWashControl[] {
|
||
return AI_WASH_CONTROL_ORDER.map((controlId) => createDefaultControl(controlId));
|
||
}
|
||
|
||
export function defaultAiWashConfig(): AiWashConfig {
|
||
return {
|
||
enabled: false,
|
||
modeLabel: AI_WASH_DEFAULT_MODE_LABEL,
|
||
modeIconUrl: "",
|
||
hideModelSelector: false,
|
||
imageSize: AI_WASH_DEFAULT_IMAGE_SIZE,
|
||
promptExtend: false,
|
||
historyLimit: AI_WASH_DEFAULT_HISTORY_LIMIT,
|
||
presets: [
|
||
{
|
||
id: "texture-precise",
|
||
title: "质感提升(细节更精准)",
|
||
description: "保留原图结构,强化细节精度、材质质感和光影层级。",
|
||
prompt:
|
||
"保持原图主体、构图、色彩关系和空间透视基本不变,重点提升材质纹理、细节锐度、光影层次和整体清晰度,让画面更精致、更干净、更高级。",
|
||
modelId: "",
|
||
enabled: true,
|
||
isDefault: true,
|
||
controls: defaultControlsForPreset(),
|
||
},
|
||
{
|
||
id: "texture-expressive",
|
||
title: "质感提升(细节更发散)",
|
||
description: "允许模型更积极地重建纹理和氛围,适合需要更强视觉表现的场景。",
|
||
prompt:
|
||
"在保留主体识别和原始构图的基础上,允许更积极地重建纹理、材质和环境氛围,提升视觉张力与高级感,让结果更具设计感和表现力。",
|
||
modelId: "",
|
||
enabled: true,
|
||
isDefault: false,
|
||
controls: defaultControlsForPreset(),
|
||
},
|
||
{
|
||
id: "ai-render",
|
||
title: "AI模型渲染(类似渲染器)",
|
||
description: "转成更接近商业渲染图的质感,适合家居、空间、产品展示。",
|
||
prompt:
|
||
"将原图重塑为更接近专业商业渲染图的视觉效果,提升光影控制、材质真实度、环境反射和空间氛围,保持主体结构与镜头视角稳定。",
|
||
modelId: "",
|
||
enabled: true,
|
||
isDefault: false,
|
||
controls: defaultControlsForPreset(),
|
||
},
|
||
],
|
||
};
|
||
}
|
||
|
||
export function normalizeAiWashConfig(value: unknown): AiWashConfig {
|
||
const fallback = defaultAiWashConfig();
|
||
const raw =
|
||
value && typeof value === "object" && !Array.isArray(value)
|
||
? (value as Record<string, unknown>)
|
||
: {};
|
||
const presetsInput = Array.isArray(raw.presets) ? raw.presets : fallback.presets;
|
||
const presets = ensureDefaultPreset(
|
||
presetsInput
|
||
.map((preset, index) =>
|
||
normalizePreset(
|
||
preset,
|
||
index,
|
||
fallback.presets[index] || fallback.presets[0]
|
||
)
|
||
)
|
||
.slice(0, 6)
|
||
);
|
||
|
||
return {
|
||
enabled: raw.enabled === true,
|
||
modeLabel: normalizeText(raw.modeLabel, 32) || AI_WASH_DEFAULT_MODE_LABEL,
|
||
modeIconUrl: normalizeText(raw.modeIconUrl, 2000),
|
||
hideModelSelector: raw.hideModelSelector === true,
|
||
imageSize: normalizeText(raw.imageSize, 16) || AI_WASH_DEFAULT_IMAGE_SIZE,
|
||
promptExtend: raw.promptExtend === true,
|
||
historyLimit: Math.max(
|
||
1,
|
||
Math.min(24, Number(raw.historyLimit) || AI_WASH_DEFAULT_HISTORY_LIMIT)
|
||
),
|
||
presets,
|
||
};
|
||
}
|
||
|
||
export function sanitizeAiWashConfigForClient(
|
||
config: AiWashConfig
|
||
): AiWashConfig {
|
||
return normalizeAiWashConfig(config);
|
||
}
|
||
|
||
export function getEnabledAiWashPresets(config: AiWashConfig) {
|
||
return config.presets.filter(
|
||
(preset) => preset.enabled && preset.prompt.trim().length > 0
|
||
);
|
||
}
|