Your Name 670a50c4d0
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add enhance pro canvas workflow and presets
2026-04-04 22:40:50 +08:00

354 lines
11 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";
const TRAILING_SLASHES_REGEX = /\/+$/;
export type EnhanceProKreaModel =
| "Redefine"
| "Recovery"
| "Recovery V2"
| "Reimagine";
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;
defaultModel: EnhanceProKreaModel;
defaultOutputFormat: EnhanceProOutputFormat;
allowBatchSize: boolean;
maxBatchSize: 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}`;
}
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"
>,
): 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:
model === "Recovery" ||
model === "Recovery V2" ||
model === "Reimagine" ||
model === "Redefine"
? model
: defaults.model || configDefaults.defaultModel,
upscalingActivated:
typeof raw.upscalingActivated === "boolean"
? raw.upscalingActivated
: defaults.upscalingActivated,
imageScalingFactor: clampFloat(
raw.imageScalingFactor,
1,
32,
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: "",
defaultModel: ENHANCE_PRO_KREA_DEFAULT_MODEL,
defaultOutputFormat: ENHANCE_PRO_KREA_DEFAULT_OUTPUT_FORMAT,
allowBatchSize: true,
maxBatchSize: 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 defaultModel = String(raw.defaultModel || "").trim();
const defaultOutputFormat = String(raw.defaultOutputFormat || "").trim();
const normalizedPresets = Array.isArray(raw.presets)
? raw.presets.map((item, index) =>
normalizeEnhanceProPreset(item, index, {
defaultModel: defaults.defaultModel,
defaultOutputFormat: defaults.defaultOutputFormat,
maxBatchSize: clampInteger(raw.maxBatchSize, 1, 4, defaults.maxBatchSize),
}),
)
: defaultEnhanceProPresets();
const presetWithDefault = normalizedPresets.some((preset) => preset.isDefault)
? normalizedPresets
: normalizedPresets.map((preset, index) => ({
...preset,
isDefault: index === 0,
}));
return {
enabled: raw.enabled === true,
providerName:
String(raw.providerName || "").trim() || defaults.providerName,
baseUrl:
String(raw.baseUrl || "")
.trim()
.replace(TRAILING_SLASHES_REGEX, "") || defaults.baseUrl,
apiKey: String(raw.apiKey || "").trim() || apiKeyFallback,
defaultModel:
defaultModel === "Recovery" ||
defaultModel === "Recovery V2" ||
defaultModel === "Reimagine"
? defaultModel
: defaults.defaultModel,
defaultOutputFormat:
defaultOutputFormat === "png" ||
defaultOutputFormat === "webp" ||
defaultOutputFormat === "jpg"
? defaultOutputFormat
: defaults.defaultOutputFormat,
allowBatchSize: raw.allowBatchSize !== false,
maxBatchSize: clampInteger(raw.maxBatchSize, 1, 4, defaults.maxBatchSize),
maxOutputEdge: clampInteger(
raw.maxOutputEdge,
512,
16_384,
defaults.maxOutputEdge,
),
enableAdvancedControls: raw.enableAdvancedControls !== false,
presets: presetWithDefault,
};
}
export function sanitizeEnhanceProKreaConfigForClient(
config: EnhanceProKreaConfig,
) {
return {
enabled: config.enabled,
providerName: config.providerName,
baseUrl: config.baseUrl,
defaultModel: config.defaultModel,
defaultOutputFormat: config.defaultOutputFormat,
allowBatchSize: config.allowBatchSize,
maxBatchSize: config.maxBatchSize,
maxOutputEdge: config.maxOutputEdge,
enableAdvancedControls: config.enableAdvancedControls,
presets: config.presets,
};
}