Your Name 66fd760b9a
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: update enhance pro configuration
2026-05-07 19:54:02 +08:00

489 lines
14 KiB
TypeScript

export const ENHANCE_PRO_KREA_SETTING_KEY = "enhance_pro_krea_config";
export const ENHANCE_PRO_KREA_DEFAULT_BASE_URL = "https://api.krea.ai";
export const ENHANCE_PRO_KREA_DEFAULT_PROVIDER_NAME = "Krea Topaz Generative";
export const ENHANCE_PRO_KREA_DEFAULT_MODEL = "Redefine";
export const ENHANCE_PRO_KREA_DEFAULT_OUTPUT_FORMAT = "jpg";
export const ENHANCE_PRO_KREA_MODELS = [
"Redefine",
"Recovery",
"Recovery V2",
"Reimagine",
] as const;
const TRAILING_SLASHES_REGEX = /\/+$/;
export type EnhanceProKreaModel = (typeof ENHANCE_PRO_KREA_MODELS)[number];
export type EnhanceProOutputFormat = "png" | "jpg" | "webp";
export type EnhanceProSubjectDetection = "All" | "Foreground" | "Background";
export type EnhanceProPreset = {
id: string;
name: string;
description: string;
isDefault: boolean;
prompt: string;
batchSize: number;
seed?: number;
outputFormat: EnhanceProOutputFormat;
subjectDetection: EnhanceProSubjectDetection;
faceEnhancement: boolean;
faceEnhancementCreativity: number;
faceEnhancementStrength: number;
cropToFill: boolean;
model: EnhanceProKreaModel;
upscalingActivated: boolean;
imageScalingFactor: number;
creativity: number;
texture: number;
sharpen: number;
denoise: number;
detail: number;
};
export type EnhanceProKreaConfig = {
enabled: boolean;
providerName: string;
baseUrl: string;
apiKey: string;
apiKeys: string[];
defaultModel: EnhanceProKreaModel;
hiddenModels: EnhanceProKreaModel[];
defaultOutputFormat: EnhanceProOutputFormat;
allowBatchSize: boolean;
maxBatchSize: number;
maxUpscaleFactor: number;
maxOutputEdge: number;
enableAdvancedControls: boolean;
presets: EnhanceProPreset[];
};
function clampInteger(
value: unknown,
min: number,
max: number,
fallback: number,
) {
const parsed = Number.parseInt(String(value ?? ""), 10);
if (!Number.isFinite(parsed)) {
return fallback;
}
return Math.min(max, Math.max(min, parsed));
}
function clampFloat(
value: unknown,
min: number,
max: number,
fallback: number,
) {
const parsed = Number.parseFloat(String(value ?? ""));
if (!Number.isFinite(parsed)) {
return fallback;
}
return Math.min(max, Math.max(min, parsed));
}
function createEnhanceProPresetId(name: string, index: number) {
const normalized = String(name || "")
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
return normalized || `enhance-pro-preset-${index + 1}`;
}
function normalizeEnhanceProApiKey(value: unknown) {
return String(value || "")
.replace(/[\u2028\u2029]/g, "")
.replace(/[\u0000-\u001F\u007F]/g, "")
.replace(/^bearer\s+/i, "")
.trim();
}
function normalizeEnhanceProApiKeys(value: unknown) {
const candidates = Array.isArray(value)
? value
: typeof value === "string"
? value.split(/\r?\n|,/)
: [];
return Array.from(
new Set(
candidates
.map((item) => normalizeEnhanceProApiKey(item))
.filter(Boolean)
)
);
}
export function isEnhanceProKreaModel(
value: unknown,
): value is EnhanceProKreaModel {
const model = String(value || "").trim();
return ENHANCE_PRO_KREA_MODELS.includes(model as EnhanceProKreaModel);
}
function normalizeEnhanceProKreaModel(
value: unknown,
fallback: EnhanceProKreaModel,
) {
const model = String(value || "").trim();
return isEnhanceProKreaModel(model) ? model : fallback;
}
function normalizeEnhanceProHiddenModels(
value: unknown,
defaultModel: EnhanceProKreaModel,
) {
const candidates = Array.isArray(value)
? value
: typeof value === "string"
? value.split(/\r?\n|,/)
: [];
const hiddenModels = Array.from(
new Set(
candidates
.map((item) => String(item || "").trim())
.filter((item): item is EnhanceProKreaModel =>
isEnhanceProKreaModel(item),
),
),
);
if (hiddenModels.length >= ENHANCE_PRO_KREA_MODELS.length) {
return hiddenModels.filter((model) => model !== defaultModel);
}
return hiddenModels;
}
function ensureEnhanceProPresetDefault(presets: EnhanceProPreset[]) {
if (presets.length === 0) {
return presets;
}
if (presets.some((preset) => preset.isDefault)) {
return presets;
}
return presets.map((preset, index) => ({
...preset,
isDefault: index === 0,
}));
}
export function getVisibleEnhanceProKreaModels(
config: Pick<EnhanceProKreaConfig, "hiddenModels">,
) {
const hiddenModels = new Set(config.hiddenModels);
const visibleModels = ENHANCE_PRO_KREA_MODELS.filter(
(model) => !hiddenModels.has(model),
);
return visibleModels.length ? visibleModels : [...ENHANCE_PRO_KREA_MODELS];
}
export function isEnhanceProKreaModelHidden(
config: Pick<EnhanceProKreaConfig, "hiddenModels">,
model: EnhanceProKreaModel,
) {
return !getVisibleEnhanceProKreaModels(config).includes(model);
}
export function defaultEnhanceProPresets(): EnhanceProPreset[] {
return [
{
id: "fidelity-recovery",
name: "保真修复",
description: "优先保留原图结构和内容,适合老图修复、压缩损伤恢复、细节补全。",
isDefault: true,
prompt:
"保持原图主体、构图和光影关系基本不变,忠实修复细节,减少压缩噪点和模糊感,让整体画质更干净自然。",
batchSize: 1,
seed: 1337,
outputFormat: "jpg",
subjectDetection: "All",
faceEnhancement: false,
faceEnhancementCreativity: 0.35,
faceEnhancementStrength: 0.45,
cropToFill: false,
model: "Recovery V2",
upscalingActivated: true,
imageScalingFactor: 2,
creativity: 1,
texture: 2,
sharpen: 0.35,
denoise: 0.45,
detail: 0.65,
},
{
id: "portrait-repair",
name: "人像修复",
description: "针对人物和面部细节优化,适合证件照、半身照、商品模特图。",
isDefault: false,
prompt:
"重点增强人物面部和发丝细节,保持肤色自然真实,避免过度磨皮和塑料感,让五官更清晰但不过度重绘。",
batchSize: 1,
seed: 1337,
outputFormat: "jpg",
subjectDetection: "Foreground",
faceEnhancement: true,
faceEnhancementCreativity: 0.35,
faceEnhancementStrength: 0.75,
cropToFill: false,
model: "Recovery V2",
upscalingActivated: true,
imageScalingFactor: 2,
creativity: 2,
texture: 3,
sharpen: 0.4,
denoise: 0.4,
detail: 0.7,
},
{
id: "creative-reimagine",
name: "创意重建",
description: "允许模型更积极地重建高频细节,适合模糊图、概念图、二次质感提升。",
isDefault: false,
prompt:
"在保持主体可识别的基础上大胆重建高频细节与纹理,提升画面层次、材质感和视觉冲击力,让结果更有高级感。",
batchSize: 1,
seed: 1337,
outputFormat: "jpg",
subjectDetection: "All",
faceEnhancement: false,
faceEnhancementCreativity: 0.6,
faceEnhancementStrength: 0.55,
cropToFill: false,
model: "Reimagine",
upscalingActivated: true,
imageScalingFactor: 2,
creativity: 5,
texture: 4,
sharpen: 0.55,
denoise: 0.25,
detail: 0.8,
},
];
}
function normalizeEnhanceProPreset(
value: unknown,
index: number,
configDefaults: Pick<
EnhanceProKreaConfig,
| "defaultModel"
| "defaultOutputFormat"
| "maxBatchSize"
| "maxUpscaleFactor"
>,
): EnhanceProPreset {
const defaults = defaultEnhanceProPresets()[index] || defaultEnhanceProPresets()[0];
const raw =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
const model = String(raw.model || "").trim();
const outputFormat = String(
raw.outputFormat || raw.output_format || "",
).trim();
const subjectDetection = String(
raw.subjectDetection || raw.subject_detection || "",
).trim();
return {
id:
String(raw.id || "").trim() ||
createEnhanceProPresetId(String(raw.name || defaults.name), index),
name: String(raw.name || "").trim() || defaults.name,
description: String(raw.description || "").trim() || defaults.description,
isDefault: raw.isDefault === true,
prompt: String(raw.prompt || "").trim() || defaults.prompt,
batchSize: clampInteger(
raw.batchSize,
1,
configDefaults.maxBatchSize,
defaults.batchSize,
),
seed: Number.isFinite(Number(raw.seed))
? Math.round(Number(raw.seed))
: defaults.seed,
outputFormat:
outputFormat === "png" ||
outputFormat === "webp" ||
outputFormat === "jpg"
? outputFormat
: defaults.outputFormat || configDefaults.defaultOutputFormat,
subjectDetection:
subjectDetection === "Foreground" || subjectDetection === "Background"
? subjectDetection
: defaults.subjectDetection,
faceEnhancement:
typeof raw.faceEnhancement === "boolean"
? raw.faceEnhancement
: defaults.faceEnhancement,
faceEnhancementCreativity: clampFloat(
raw.faceEnhancementCreativity,
0,
1,
defaults.faceEnhancementCreativity,
),
faceEnhancementStrength: clampFloat(
raw.faceEnhancementStrength,
0,
1,
defaults.faceEnhancementStrength,
),
cropToFill:
typeof raw.cropToFill === "boolean" ? raw.cropToFill : defaults.cropToFill,
model: normalizeEnhanceProKreaModel(
model,
defaults.model || configDefaults.defaultModel,
),
upscalingActivated:
typeof raw.upscalingActivated === "boolean"
? raw.upscalingActivated
: defaults.upscalingActivated,
imageScalingFactor: clampFloat(
raw.imageScalingFactor,
1,
configDefaults.maxUpscaleFactor,
defaults.imageScalingFactor,
),
creativity: clampInteger(raw.creativity, 1, 6, defaults.creativity),
texture: clampInteger(raw.texture, 1, 5, defaults.texture),
sharpen: clampFloat(raw.sharpen, 0, 1, defaults.sharpen),
denoise: clampFloat(raw.denoise, 0, 1, defaults.denoise),
detail: clampFloat(raw.detail, 0, 1, defaults.detail),
};
}
export function defaultEnhanceProKreaConfig(): EnhanceProKreaConfig {
return {
enabled: false,
providerName: ENHANCE_PRO_KREA_DEFAULT_PROVIDER_NAME,
baseUrl: ENHANCE_PRO_KREA_DEFAULT_BASE_URL,
apiKey: "",
apiKeys: [],
defaultModel: ENHANCE_PRO_KREA_DEFAULT_MODEL,
hiddenModels: [],
defaultOutputFormat: ENHANCE_PRO_KREA_DEFAULT_OUTPUT_FORMAT,
allowBatchSize: true,
maxBatchSize: 4,
maxUpscaleFactor: 4,
maxOutputEdge: 4096,
enableAdvancedControls: true,
presets: defaultEnhanceProPresets(),
};
}
export function normalizeEnhanceProKreaConfig(
value: unknown,
apiKeyFallback = ""
): EnhanceProKreaConfig {
const defaults = defaultEnhanceProKreaConfig();
const raw =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
const rawDefaultModel = normalizeEnhanceProKreaModel(
raw.defaultModel,
defaults.defaultModel,
);
const hiddenModels = normalizeEnhanceProHiddenModels(
raw.hiddenModels || raw.disabledModels,
rawDefaultModel,
);
const visibleModels = ENHANCE_PRO_KREA_MODELS.filter(
(model) => !hiddenModels.includes(model),
);
const defaultModel = hiddenModels.includes(rawDefaultModel)
? visibleModels[0] || defaults.defaultModel
: rawDefaultModel;
const defaultOutputFormat = String(raw.defaultOutputFormat || "").trim();
const maxUpscaleFactor = clampFloat(
raw.maxUpscaleFactor,
1,
32,
defaults.maxUpscaleFactor,
);
const normalizedPresets = (
Array.isArray(raw.presets) ? raw.presets : defaultEnhanceProPresets()
).map((item, index) =>
normalizeEnhanceProPreset(item, index, {
defaultModel,
defaultOutputFormat: defaults.defaultOutputFormat,
maxBatchSize: clampInteger(raw.maxBatchSize, 1, 4, defaults.maxBatchSize),
maxUpscaleFactor,
}),
);
const presetWithDefault = ensureEnhanceProPresetDefault(normalizedPresets);
return {
enabled: raw.enabled === true,
providerName:
String(raw.providerName || "").trim() || defaults.providerName,
baseUrl:
String(raw.baseUrl || "")
.trim()
.replace(TRAILING_SLASHES_REGEX, "") || defaults.baseUrl,
apiKey:
normalizeEnhanceProApiKey(raw.apiKey) ||
normalizeEnhanceProApiKey(apiKeyFallback),
apiKeys: normalizeEnhanceProApiKeys(raw.apiKeys || raw.apiKeyPool),
defaultModel,
hiddenModels,
defaultOutputFormat:
defaultOutputFormat === "png" ||
defaultOutputFormat === "webp" ||
defaultOutputFormat === "jpg"
? defaultOutputFormat
: defaults.defaultOutputFormat,
allowBatchSize: raw.allowBatchSize !== false,
maxBatchSize: clampInteger(raw.maxBatchSize, 1, 4, defaults.maxBatchSize),
maxUpscaleFactor,
maxOutputEdge: clampInteger(
raw.maxOutputEdge,
512,
16_384,
defaults.maxOutputEdge,
),
enableAdvancedControls: raw.enableAdvancedControls !== false,
presets: presetWithDefault,
};
}
export function sanitizeEnhanceProKreaConfigForClient(
config: EnhanceProKreaConfig,
) {
const availableModels = getVisibleEnhanceProKreaModels(config);
const defaultModel = availableModels.includes(config.defaultModel)
? config.defaultModel
: availableModels[0] || ENHANCE_PRO_KREA_DEFAULT_MODEL;
const visiblePresets = config.presets.filter((preset) =>
availableModels.includes(preset.model),
);
const fallbackPresets = defaultEnhanceProPresets().filter((preset) =>
availableModels.includes(preset.model),
);
const presets = ensureEnhanceProPresetDefault(
visiblePresets.length ? visiblePresets : fallbackPresets,
);
return {
enabled: config.enabled,
providerName: config.providerName,
baseUrl: config.baseUrl,
keyPoolSize: config.apiKeys.length,
defaultModel,
availableModels,
defaultOutputFormat: config.defaultOutputFormat,
allowBatchSize: config.allowBatchSize,
maxBatchSize: config.maxBatchSize,
maxUpscaleFactor: config.maxUpscaleFactor,
maxOutputEdge: config.maxOutputEdge,
enableAdvancedControls: config.enableAdvancedControls,
presets,
};
}