feat(*): 创作与运维 - 新增图片尺寸预设和系统性能面板并优化图片组动画
This commit is contained in:
@@ -59,11 +59,13 @@ function parseImageResolutionOption(value: string): ImageResolutionOption | null
|
||||
if (!match) return null;
|
||||
const width = Number(match[1]);
|
||||
const height = Number(match[2]);
|
||||
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width <= 0 || height <= 0) return null;
|
||||
const pixels = width * height;
|
||||
const tier: ImageResolutionTier | null = pixels <= 2_000_000 ? "1k" : pixels <= 4_300_000 ? "2k" : pixels <= 8_294_400 ? "4k" : null;
|
||||
if (!tier) return null;
|
||||
const ratio = closestRatio(width / height);
|
||||
if (!ratio) return null;
|
||||
let a = width, b = height;
|
||||
while (b) [a, b] = [b, a % b];
|
||||
const ratio = closestRatio(width / height) || `${width / a}:${height / a}`;
|
||||
return { size: `${width}x${height}`, tier, ratio, width, height };
|
||||
}
|
||||
|
||||
|
||||
95
web/src/lib/image-size-presets.ts
Normal file
95
web/src/lib/image-size-presets.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { buildImageResolutionOptions, type ImageResolutionOption, type ImageResolutionTier } from "./image-resolution-tiers";
|
||||
import type { ImageCapabilityConfig } from "./model-capabilities";
|
||||
|
||||
export const IMAGE_RESOLUTIONS: ImageResolutionTier[] = ["1k", "2k", "4k"];
|
||||
export const IMAGE_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "21:9"];
|
||||
const standardSizes: Record<string, string[]> = {
|
||||
"1:1": ["1024x1024", "2048x2048", "2880x2880"],
|
||||
"16:9": ["1824x1024", "2752x1536", "3840x2160"],
|
||||
"9:16": ["1024x1824", "1536x2752", "2160x3840"],
|
||||
"4:3": ["1360x1024", "2304x1728", "3264x2448"],
|
||||
"3:4": ["1024x1360", "1728x2304", "2448x3264"],
|
||||
"3:2": ["1536x1024", "2496x1664", "3504x2336"],
|
||||
"2:3": ["1024x1536", "1664x2496", "2336x3504"],
|
||||
"21:9": ["2048x878", "3136x1344", "3808x1632"],
|
||||
};
|
||||
|
||||
export function imagePresetForRatio(tier: ImageResolutionTier, input: string): ImageResolutionOption {
|
||||
const match = input.trim().match(/^(\d{1,5})\s*[::]\s*(\d{1,5})$/);
|
||||
if (!match) throw new Error("请输入宽高比,例如 16:9");
|
||||
let w = Number(match[1]),
|
||||
h = Number(match[2]);
|
||||
if (!w || !h || Math.max(w, h) / Math.min(w, h) > 3) throw new Error("宽高比必须为正数,且不超过 3:1");
|
||||
let a = w,
|
||||
b = h;
|
||||
while (b) [a, b] = [b, a % b];
|
||||
w /= a;
|
||||
h /= a;
|
||||
const ratio =
|
||||
Object.keys(standardSizes).find((value) => {
|
||||
const [width, height] = value.split(":").map(Number);
|
||||
return width * h === height * w;
|
||||
}) || `${w}:${h}`;
|
||||
const index = IMAGE_RESOLUTIONS.indexOf(tier);
|
||||
if (index < 0) throw new Error("分辨率仅支持 1K、2K、4K");
|
||||
const standard = standardSizes[ratio]?.[index];
|
||||
const pixels = [1_048_576, 4_194_304, 8_294_400][index];
|
||||
let width = Math.round(Math.sqrt((pixels * w) / h) / 16) * 16;
|
||||
let height = Math.round((width * h) / w / 16) * 16;
|
||||
while (width * height > pixels || Math.max(width, height) > 3840) {
|
||||
if (width >= height) {
|
||||
width -= 16;
|
||||
height = Math.round((width * h) / w / 16) * 16;
|
||||
} else {
|
||||
height -= 16;
|
||||
width = Math.round((height * w) / h / 16) * 16;
|
||||
}
|
||||
}
|
||||
if (standard) [width, height] = standard.split("x").map(Number);
|
||||
return { tier, ratio, width, height, size: `${width}x${height}` };
|
||||
}
|
||||
|
||||
export function imageQualityForTier(profile: ImageCapabilityConfig, tier: ImageResolutionTier) {
|
||||
const aliases = { "1k": ["1k", "low"], "2k": ["2k", "medium"], "4k": ["4k", "high"] }[tier];
|
||||
return profile.quality.supported ? profile.quality.values.find((value) => aliases.includes(value.toLowerCase())) : undefined;
|
||||
}
|
||||
|
||||
export function imageResolutionUsesQuality(profile: ImageCapabilityConfig) {
|
||||
return profile.size.parameter === "aspect_ratio" && IMAGE_RESOLUTIONS.some((tier) => imageQualityForTier(profile, tier));
|
||||
}
|
||||
|
||||
export function imageTierAvailable(profile: ImageCapabilityConfig, tier: ImageResolutionTier) {
|
||||
// 比例协议必须能把档位传给上游;允许自定义比例不等于支持任意分辨率。
|
||||
if (profile.size.parameter !== "aspect_ratio") return profile.size.parameter !== "none";
|
||||
return imageResolutionUsesQuality(profile) ? Boolean(imageQualityForTier(profile, tier)) : tier === "1k";
|
||||
}
|
||||
|
||||
export function imageSizePresets(profile: ImageCapabilityConfig): ImageResolutionOption[] {
|
||||
if (profile.size.parameter === "none") return [];
|
||||
if (profile.size.presets) return profile.size.presets;
|
||||
const pixels = buildImageResolutionOptions(profile.size.values);
|
||||
const tiers = profile.size.parameter === "aspect_ratio" ? IMAGE_RESOLUTIONS.filter((tier) => imageQualityForTier(profile, tier)) : ["1k" as const];
|
||||
const ratios = (tiers.length ? tiers : ["1k" as const]).flatMap((tier) =>
|
||||
profile.size.values.flatMap((ratio) => {
|
||||
try {
|
||||
const preset = imagePresetForRatio(tier, ratio);
|
||||
return pixels.some((pixel) => pixel.ratio === preset.ratio) ? [] : [preset];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
);
|
||||
return [...pixels, ...ratios];
|
||||
}
|
||||
|
||||
export function imagePresetValue(profile: ImageCapabilityConfig, preset: ImageResolutionOption) {
|
||||
if (!profile.size.presets && !profile.size.allowCustom && preset.tier === "1k" && profile.size.values.includes(preset.ratio) && !profile.size.values.includes(preset.size)) return preset.ratio;
|
||||
return profile.size.parameter === "aspect_ratio" ? preset.ratio : preset.size;
|
||||
}
|
||||
|
||||
export function imageSizeConfigWithPresets(profile: ImageCapabilityConfig, presets: ImageResolutionOption[]): ImageCapabilityConfig["size"] {
|
||||
const values = [...new Set(presets.map((preset) => (profile.size.parameter === "aspect_ratio" ? preset.ratio : preset.size)))];
|
||||
if (profile.size.values.includes("auto")) values.unshift("auto");
|
||||
if (!values.length) values.push("auto");
|
||||
return { ...profile.size, presets, values, default: values.includes(profile.size.default) ? profile.size.default : values[0] || "auto" };
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ModelProtocol, ModelProtocolWorkflow } from "@/lib/model-protocols";
|
||||
import type { ImageResolutionOption } from "@/lib/image-resolution-tiers";
|
||||
|
||||
export type ModelCapabilityConfig = {
|
||||
version: number;
|
||||
@@ -31,6 +32,7 @@ export type ImageCapabilityConfig = {
|
||||
values: string[];
|
||||
default: string;
|
||||
allowCustom: boolean;
|
||||
presets?: ImageResolutionOption[];
|
||||
};
|
||||
quality: {
|
||||
supported: boolean;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defaultImageCapabilityConfig, modelCapabilityConfigFor, normalizeImageValue, normalizeVideoValue, STANDARD_IMAGE_SIZE_VALUES, videoDurationAllowed, type ImageCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import { videoResolutionComparisonKey } from "@/lib/video-generation-options";
|
||||
import { imageSizePresets } from "@/lib/image-size-presets";
|
||||
import { modelOptionName, resolveModelChannel, selectableModelsByCapability, type AiConfig, type ModelCapability } from "@/stores/use-config-store";
|
||||
|
||||
export type ModelInputSummary = {
|
||||
@@ -229,7 +230,8 @@ export function mergedImageCapabilityConfig(config: AiConfig, selected: string):
|
||||
const allowCustom = profiles.some((profile) => profile.size.allowCustom || profile.size.values.includes("*"));
|
||||
const values = concreteValues.length ? concreteValues : allowCustom ? [...STANDARD_IMAGE_SIZE_VALUES] : [];
|
||||
const base = selectedProfile || profiles[0];
|
||||
return { ...base, size: { ...base.size, values, allowCustom } };
|
||||
const presets = profiles.some((profile) => profile.size.presets) ? [...new Map(profiles.flatMap((profile) => imageSizePresets(profile)).map((preset) => [`${preset.tier}:${preset.ratio}:${preset.size}`, preset])).values()] : undefined;
|
||||
return { ...base, size: { ...base.size, values, allowCustom, presets } };
|
||||
}
|
||||
|
||||
// 切换模型后初始化图片参数为该模型能力默认值,避免旧参数在目标模型族不兼容导致无法切换。
|
||||
|
||||
Reference in New Issue
Block a user