fix: drive image params from model config
This commit is contained in:
parent
7a58cdd12b
commit
9b94aae898
@ -641,6 +641,8 @@ type ImageGenConfig = {
|
||||
supportsNegativePrompt?: boolean; // 是否支持反向提示词
|
||||
supportsWatermark?: boolean; // 是否支持水印开关
|
||||
supportsSeed?: boolean; // 是否支持随机种子
|
||||
supportedAspectRatios?: string[]; // 支持的图片比例
|
||||
supportedImageSizes?: string[]; // 支持的分辨率/K 数
|
||||
creditCostPerImage?: number; // 每张图片积分消耗
|
||||
creditCostMultiplierBySize?: Record<string, number>; // 各图片尺寸积分倍率
|
||||
creditDiscountRate?: number; // 积分折扣,按“折”配置,如 5 = 5折
|
||||
|
||||
@ -48,11 +48,11 @@ interface ImageParamsBarProps {
|
||||
supportsWatermark?: boolean;
|
||||
supportsSeed?: boolean;
|
||||
supportedAspectRatios?: string[];
|
||||
supportedImageSizes?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
// 图片比例选项(根据 Nano Banana 文档)
|
||||
const ASPECT_RATIO_OPTIONS = [
|
||||
const DEFAULT_ASPECT_RATIO_OPTIONS = [
|
||||
{ value: "auto", label: "自动" },
|
||||
{ value: "1:1", label: "1:1" },
|
||||
{ value: "16:9", label: "16:9" },
|
||||
@ -65,30 +65,63 @@ const ASPECT_RATIO_OPTIONS = [
|
||||
{ value: "4:5", label: "4:5" },
|
||||
{ value: "21:9", label: "21:9" },
|
||||
] as const;
|
||||
type AspectRatioValue = (typeof ASPECT_RATIO_OPTIONS)[number]["value"];
|
||||
type AspectRatioValue = string;
|
||||
|
||||
// 图片大小选项(仅支持 nano-banana-pro)
|
||||
const IMAGE_SIZE_OPTIONS = [
|
||||
const DEFAULT_IMAGE_SIZE_OPTIONS = [
|
||||
{ value: "1K", label: "1K", disabled: false },
|
||||
{ value: "2K", label: "2K", disabled: false },
|
||||
{ value: "4K", label: "4K", disabled: false },
|
||||
] as const;
|
||||
type ImageSizeValue = (typeof IMAGE_SIZE_OPTIONS)[number]["value"];
|
||||
type ImageSizeValue = string;
|
||||
|
||||
function normalizeImageSizeValue(value?: string, fallback: ImageSizeValue = "2K"): ImageSizeValue {
|
||||
const normalized = String(value || "").trim().toUpperCase();
|
||||
if (normalized === "1K" || normalized === "2K" || normalized === "4K") {
|
||||
return normalized;
|
||||
const trimmed = String(value || "").trim();
|
||||
if (!trimmed) return fallback;
|
||||
const kMatch = trimmed.match(/^(\d+(?:\.\d+)?)\s*k$/i);
|
||||
if (kMatch?.[1]) {
|
||||
return `${Number(kMatch[1]).toString()}K`;
|
||||
}
|
||||
return fallback;
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
const IMAGE_SIZE_PIXEL_MAP: Record<ImageSizeValue, number> = {
|
||||
const IMAGE_SIZE_PIXEL_MAP: Record<string, number> = {
|
||||
"0.5K": 512,
|
||||
"1K": 1024,
|
||||
"2K": 2048,
|
||||
"4K": 4096,
|
||||
"8K": 8192,
|
||||
"10K": 10240,
|
||||
"12K": 12288,
|
||||
};
|
||||
|
||||
const DEFAULT_IMAGE_SIZE_VALUE = "2K";
|
||||
|
||||
function getImageSizePixelValue(imageSize: string) {
|
||||
const normalized = normalizeImageSizeValue(imageSize, DEFAULT_IMAGE_SIZE_VALUE);
|
||||
if (IMAGE_SIZE_PIXEL_MAP[normalized]) {
|
||||
return IMAGE_SIZE_PIXEL_MAP[normalized];
|
||||
}
|
||||
|
||||
const kMatch = normalized.match(/^(\d+(?:\.\d+)?)K$/i);
|
||||
if (kMatch?.[1]) {
|
||||
return Math.max(1, Math.round(Number(kMatch[1]) * 1024));
|
||||
}
|
||||
|
||||
return IMAGE_SIZE_PIXEL_MAP[DEFAULT_IMAGE_SIZE_VALUE];
|
||||
}
|
||||
|
||||
function uniqueValues(values: string[]) {
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
for (const value of values) {
|
||||
const normalized = String(value || "").trim();
|
||||
if (!normalized || seen.has(normalized)) continue;
|
||||
seen.add(normalized);
|
||||
result.push(normalized);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function ImageParamsBar({
|
||||
modelName = "Gemini 图片生成",
|
||||
initialParams,
|
||||
@ -156,38 +189,71 @@ export function ImageParamsBar({
|
||||
// Gemini:1~2 张
|
||||
return [1, 2];
|
||||
};
|
||||
const configuredImageSizes = uniqueValues(
|
||||
Array.isArray(imageGenConfig?.supportedImageSizes)
|
||||
? imageGenConfig.supportedImageSizes.map((size) =>
|
||||
normalizeImageSizeValue(size, "")
|
||||
)
|
||||
: []
|
||||
);
|
||||
|
||||
const buildImageSizeOptions = () => {
|
||||
if (imageGenConfig?.supportsSize === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (configuredImageSizes.length > 0) {
|
||||
return configuredImageSizes.map((value) => ({ value, label: value, disabled: false }));
|
||||
}
|
||||
|
||||
return DEFAULT_IMAGE_SIZE_OPTIONS.filter((option) => {
|
||||
if (option.disabled) return false;
|
||||
if (!imageGenConfig) return true;
|
||||
if (option.value === "1K") return imageGenConfig.supports1K !== false;
|
||||
if (option.value === "2K") return imageGenConfig.supports2K !== false;
|
||||
if (option.value === "4K") return imageGenConfig.supports4K !== false;
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
const availableSizeOptions = buildImageSizeOptions();
|
||||
|
||||
const isImageSizeAllowed = (s: ImageSizeValue) => {
|
||||
if (!imageGenConfig) return true;
|
||||
if (s === "1K") return imageGenConfig.supports1K !== false;
|
||||
if (s === "2K") return imageGenConfig.supports2K !== false;
|
||||
if (s === "4K") return imageGenConfig.supports4K !== false;
|
||||
return true;
|
||||
if (availableSizeOptions.length === 0) return true;
|
||||
const normalized = normalizeImageSizeValue(s, "");
|
||||
return availableSizeOptions.some((option) => option.value === normalized);
|
||||
};
|
||||
|
||||
const getFallbackImageSize = () =>
|
||||
(["2K", "1K", "4K"] as const).find(isImageSizeAllowed) || "2K";
|
||||
["2K", "1K", "4K", DEFAULT_IMAGE_SIZE_VALUE].find(isImageSizeAllowed) ||
|
||||
availableSizeOptions[0]?.value ||
|
||||
DEFAULT_IMAGE_SIZE_VALUE;
|
||||
|
||||
const isAspectRatioAllowed = (aspectRatio: string) => {
|
||||
const configured = imageGenConfig?.supportedAspectRatios;
|
||||
if (Array.isArray(configured) && configured.length > 0) {
|
||||
return configured.includes(aspectRatio);
|
||||
}
|
||||
|
||||
if (providerType === "snapgen-image") {
|
||||
return ["1:1", "16:9", "9:16", "4:3", "3:4"].includes(aspectRatio);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const availableAspectRatioOptions = ASPECT_RATIO_OPTIONS.filter((option) =>
|
||||
isAspectRatioAllowed(option.value)
|
||||
const configuredAspectRatios = uniqueValues(
|
||||
Array.isArray(imageGenConfig?.supportedAspectRatios)
|
||||
? imageGenConfig.supportedAspectRatios.map((ratio) => String(ratio || "").trim())
|
||||
: []
|
||||
);
|
||||
|
||||
const defaultAspectRatioLabel = new Map<string, string>(
|
||||
DEFAULT_ASPECT_RATIO_OPTIONS.map((option) => [option.value, option.label])
|
||||
);
|
||||
const availableAspectRatioOptions = (
|
||||
configuredAspectRatios.length > 0
|
||||
? configuredAspectRatios
|
||||
: DEFAULT_ASPECT_RATIO_OPTIONS.map((option) => option.value)
|
||||
).map((value) => ({
|
||||
value,
|
||||
label: defaultAspectRatioLabel.get(value) || value,
|
||||
}));
|
||||
|
||||
const isAspectRatioAllowed = (aspectRatio: string) =>
|
||||
availableAspectRatioOptions.some((option) => option.value === aspectRatio);
|
||||
|
||||
const getFallbackAspectRatio = (): AspectRatioValue =>
|
||||
(availableAspectRatioOptions.find((option) => option.value === "1:1") ||
|
||||
availableAspectRatioOptions[0] ||
|
||||
ASPECT_RATIO_OPTIONS[1])!.value;
|
||||
availableAspectRatioOptions.find((option) => option.value === "1:1")?.value ||
|
||||
availableAspectRatioOptions[0]?.value ||
|
||||
"1:1";
|
||||
|
||||
const clampParamsByProvider = (raw: ImageGenerationParams): ImageGenerationParams => {
|
||||
const options = getImageCountOptions();
|
||||
@ -198,8 +264,8 @@ export function ImageParamsBar({
|
||||
aspectRatio = getFallbackAspectRatio();
|
||||
}
|
||||
// 根据模型配置校正分辨率:如果当前选择的档位被禁用,则降级到可用的档位
|
||||
let imageSize = normalizeImageSizeValue(raw.imageSize);
|
||||
if (!isImageSizeAllowed(imageSize)) {
|
||||
let imageSize = normalizeImageSizeValue(raw.imageSize, getFallbackImageSize());
|
||||
if (availableSizeOptions.length > 0 && !isImageSizeAllowed(imageSize)) {
|
||||
imageSize = getFallbackImageSize();
|
||||
}
|
||||
return {
|
||||
@ -251,18 +317,12 @@ export function ImageParamsBar({
|
||||
const showPromptExtend = isAlibabaImage || imageGenConfig?.supportsPromptExtend;
|
||||
const showNegativePrompt = isAlibabaImage || imageGenConfig?.supportsNegativePrompt;
|
||||
|
||||
// 根据模型配置过滤可用的分辨率档位
|
||||
const availableSizeOptions = IMAGE_SIZE_OPTIONS.filter((option) => {
|
||||
if (option.disabled) return false;
|
||||
return isImageSizeAllowed(option.value);
|
||||
});
|
||||
|
||||
const getComputedOutputSize = (aspectRatio: string, imageSize: string) => {
|
||||
if (providerType === "grsai-gpt-image") {
|
||||
return getGrsaiGptImageOutputSize(aspectRatio, imageSize);
|
||||
}
|
||||
|
||||
const size = IMAGE_SIZE_PIXEL_MAP[normalizeImageSizeValue(imageSize)];
|
||||
const size = getImageSizePixelValue(imageSize);
|
||||
if (aspectRatio === "auto" || aspectRatio === "1:1") {
|
||||
return { width: size, height: size };
|
||||
}
|
||||
@ -325,7 +385,9 @@ export function ImageParamsBar({
|
||||
imageGenConfig?.supports1K,
|
||||
imageGenConfig?.supports2K,
|
||||
imageGenConfig?.supports4K,
|
||||
imageGenConfig?.supportsSize,
|
||||
imageGenConfig?.supportedAspectRatios,
|
||||
imageGenConfig?.supportedImageSizes,
|
||||
initialParams,
|
||||
onParamsChange,
|
||||
providerType,
|
||||
@ -357,23 +419,28 @@ export function ImageParamsBar({
|
||||
const outputSize = getComputedOutputSize(params.aspectRatio, params.imageSize);
|
||||
|
||||
if (isMinimalInline) {
|
||||
const currentImageSize = params.imageSize || getFallbackImageSize();
|
||||
const currentAspectRatio = isAspectRatioAllowed(params.aspectRatio)
|
||||
? params.aspectRatio
|
||||
: getFallbackAspectRatio();
|
||||
const sizeOptionKeySet = new Set<string>();
|
||||
const sizeOptions = [
|
||||
{ label: "1:1", aspectRatio: "1:1", imageSize: "1K" },
|
||||
{ label: "3:2", aspectRatio: "3:2", imageSize: "1K" },
|
||||
{ label: "2:3", aspectRatio: "2:3", imageSize: "1K" },
|
||||
{ label: "4:3", aspectRatio: "4:3", imageSize: "1K" },
|
||||
{ label: "3:4", aspectRatio: "3:4", imageSize: "1K" },
|
||||
{ label: "9:16", aspectRatio: "9:16", imageSize: "1K" },
|
||||
{ label: "1:1(2k)", aspectRatio: "1:1", imageSize: "2K" },
|
||||
{ label: "16:9(2k)", aspectRatio: "16:9", imageSize: "2K" },
|
||||
{ label: "9:16(2k)", aspectRatio: "9:16", imageSize: "2K" },
|
||||
{ label: "16:9(4K)", aspectRatio: "16:9", imageSize: "4K" },
|
||||
{ label: "9:16(4K)", aspectRatio: "9:16", imageSize: "4K" },
|
||||
{ label: "auto", aspectRatio: "auto", imageSize: params.imageSize || "1K" },
|
||||
].filter((option) =>
|
||||
isAspectRatioAllowed(option.aspectRatio) &&
|
||||
(option.aspectRatio === "auto" || isImageSizeAllowed(normalizeImageSizeValue(option.imageSize)))
|
||||
);
|
||||
...availableAspectRatioOptions.map((option) => ({
|
||||
label: option.value === "auto" ? "auto" : option.label,
|
||||
aspectRatio: option.value,
|
||||
imageSize: currentImageSize,
|
||||
})),
|
||||
...availableSizeOptions.map((option) => ({
|
||||
label: option.label,
|
||||
aspectRatio: currentAspectRatio,
|
||||
imageSize: option.value,
|
||||
})),
|
||||
].filter((option) => {
|
||||
const key = `${option.aspectRatio}:${option.imageSize}`;
|
||||
if (sizeOptionKeySet.has(key)) return false;
|
||||
sizeOptionKeySet.add(key);
|
||||
return true;
|
||||
});
|
||||
const imageCountOptions = getImageCountOptions();
|
||||
const selectedAspectLabel = params.aspectRatio === "auto" ? "auto" : params.aspectRatio;
|
||||
const selectedImageCount = params.imageCount || 1;
|
||||
@ -464,6 +531,7 @@ export function ImageParamsBar({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{sizeOptions.length > 0 && (
|
||||
<div>
|
||||
<div className="mb-2 text-[14px] font-medium leading-none">Size</div>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
@ -497,6 +565,7 @@ export function ImageParamsBar({
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="mb-2 text-[14px] font-medium leading-none">Image</div>
|
||||
@ -613,6 +682,7 @@ export function ImageParamsBar({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{availableSizeOptions.length > 0 && (
|
||||
<div>
|
||||
<p className={cn("text-[11px] font-medium", isDarkInline ? "text-white/45" : "text-[#8a94a3]")}>选择分辨率</p>
|
||||
<div
|
||||
@ -644,6 +714,7 @@ export function ImageParamsBar({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<p className={cn("text-[11px] font-medium", isDarkInline ? "text-white/45" : "text-[#8a94a3]")}>尺寸</p>
|
||||
|
||||
@ -40,6 +40,8 @@ type DynamicModel = {
|
||||
supportsNegativePrompt?: boolean;
|
||||
supportsWatermark?: boolean;
|
||||
supportsSeed?: boolean;
|
||||
supportedAspectRatios?: string[];
|
||||
supportedImageSizes?: string[];
|
||||
creditCostPerImage?: number;
|
||||
creditCostMultiplierBySize?: Record<string, number>;
|
||||
creditDiscountRate?: number;
|
||||
|
||||
@ -398,28 +398,60 @@ export function SceneFusionView({
|
||||
MAX_BATCH_ITEMS
|
||||
);
|
||||
const outputCapabilities = config?.outputCapabilities;
|
||||
const availableAspectRatioOptions = useMemo(
|
||||
const aspectRatioOptionLabel = useMemo(
|
||||
() =>
|
||||
SCENE_FUSION_ASPECT_RATIO_OPTIONS.filter((option) =>
|
||||
outputCapabilities?.aspectRatio?.enabled === false
|
||||
? false
|
||||
: !outputCapabilities?.aspectRatio?.options?.length ||
|
||||
outputCapabilities.aspectRatio.options.includes(option.value)
|
||||
new Map<string, string>(
|
||||
SCENE_FUSION_ASPECT_RATIO_OPTIONS.map((option) => [
|
||||
option.value,
|
||||
option.label,
|
||||
])
|
||||
),
|
||||
[]
|
||||
);
|
||||
const imageSizeOptionLabel = useMemo(
|
||||
() =>
|
||||
new Map<string, string>(
|
||||
SCENE_FUSION_IMAGE_SIZE_OPTIONS.map((option) => [
|
||||
option.value,
|
||||
option.label,
|
||||
])
|
||||
),
|
||||
[]
|
||||
);
|
||||
const availableAspectRatioOptions = useMemo(
|
||||
() => {
|
||||
if (outputCapabilities?.aspectRatio?.enabled === false) {
|
||||
return [];
|
||||
}
|
||||
const values = outputCapabilities?.aspectRatio?.options?.length
|
||||
? outputCapabilities.aspectRatio.options
|
||||
: SCENE_FUSION_ASPECT_RATIO_OPTIONS.map((option) => option.value);
|
||||
return values.map((value) => ({
|
||||
value,
|
||||
label: aspectRatioOptionLabel.get(value) || value,
|
||||
}));
|
||||
},
|
||||
[
|
||||
aspectRatioOptionLabel,
|
||||
outputCapabilities?.aspectRatio?.enabled,
|
||||
outputCapabilities?.aspectRatio?.options,
|
||||
]
|
||||
);
|
||||
const availableImageSizeOptions = useMemo(
|
||||
() =>
|
||||
SCENE_FUSION_IMAGE_SIZE_OPTIONS.filter((option) =>
|
||||
outputCapabilities?.imageSize?.enabled === false
|
||||
? false
|
||||
: !outputCapabilities?.imageSize?.options?.length ||
|
||||
outputCapabilities.imageSize.options.includes(option.value)
|
||||
),
|
||||
() => {
|
||||
if (outputCapabilities?.imageSize?.enabled === false) {
|
||||
return [];
|
||||
}
|
||||
const values = outputCapabilities?.imageSize?.options?.length
|
||||
? outputCapabilities.imageSize.options
|
||||
: SCENE_FUSION_IMAGE_SIZE_OPTIONS.map((option) => option.value);
|
||||
return values.map((value) => ({
|
||||
value,
|
||||
label: imageSizeOptionLabel.get(value) || value,
|
||||
}));
|
||||
},
|
||||
[
|
||||
imageSizeOptionLabel,
|
||||
outputCapabilities?.imageSize?.enabled,
|
||||
outputCapabilities?.imageSize?.options,
|
||||
]
|
||||
|
||||
@ -141,9 +141,9 @@ export const SNAPGEN_IMAGE_MODEL_PRESETS: SnapgenImageModelPreset[] = [
|
||||
},
|
||||
imageGenConfig: {
|
||||
...SNAPGEN_COMMON_IMAGE_CONFIG,
|
||||
supportsSize: false,
|
||||
maxImages: 8,
|
||||
supportedAspectRatios: SNAPGEN_STANDARD_ASPECT_RATIOS,
|
||||
supportedImageSizes: ["1K"],
|
||||
supportedQualities: ["SPEED", "QUALITY"],
|
||||
},
|
||||
sortOrder: 30,
|
||||
@ -175,9 +175,9 @@ export const SNAPGEN_IMAGE_MODEL_PRESETS: SnapgenImageModelPreset[] = [
|
||||
},
|
||||
imageGenConfig: {
|
||||
...SNAPGEN_COMMON_IMAGE_CONFIG,
|
||||
supportsSize: false,
|
||||
maxImages: 4,
|
||||
supportedAspectRatios: SNAPGEN_STANDARD_ASPECT_RATIOS,
|
||||
supportedImageSizes: ["1K"],
|
||||
},
|
||||
sortOrder: 40,
|
||||
},
|
||||
|
||||
@ -132,8 +132,8 @@ const SCALE_ADJUST_VISUAL_DIFF_THRESHOLD = 14;
|
||||
const SCALE_ADJUST_OUTSIDE_DIFF_THRESHOLD = 18;
|
||||
const SCALE_ADJUST_OUTSIDE_DIFF_MIN_CHANGED_RATIO = 0.28;
|
||||
const SCALE_ADJUST_OUTSIDE_PIXEL_DIFF_THRESHOLD = 16;
|
||||
const SCALE_ADJUST_OUTSIDE_PIXEL_CHANGED_RATIO_THRESHOLD = 0.045;
|
||||
const SCALE_ADJUST_OUTSIDE_PIXEL_MEAN_DIFF_THRESHOLD = 4.5;
|
||||
const SCALE_ADJUST_OUTSIDE_PIXEL_CHANGED_RATIO_THRESHOLD = 0.16;
|
||||
const SCALE_ADJUST_OUTSIDE_PIXEL_MEAN_DIFF_THRESHOLD = 14;
|
||||
const SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN = 0.015;
|
||||
const SCALE_ADJUST_DIFF_SAMPLE_SIZE = 112;
|
||||
|
||||
@ -1207,7 +1207,7 @@ type CanvasModelWithProvider = Awaited<
|
||||
ReturnType<typeof getEnabledAiModelsWithProvider>
|
||||
>[number];
|
||||
|
||||
type CanvasImageSizeValue = "1K" | "2K" | "4K";
|
||||
type CanvasImageSizeValue = string;
|
||||
const CANVAS_FAILOVER_SAME_NAME_THRESHOLD = 0.5;
|
||||
|
||||
interface CanvasImageGenConfig {
|
||||
@ -1215,17 +1215,20 @@ interface CanvasImageGenConfig {
|
||||
supports1K?: boolean;
|
||||
supports2K?: boolean;
|
||||
supports4K?: boolean;
|
||||
supportedImageSizes?: string[];
|
||||
}
|
||||
|
||||
function normalizeCanvasImageSizeValue(
|
||||
value?: string,
|
||||
fallback: CanvasImageSizeValue = "2K"
|
||||
): CanvasImageSizeValue {
|
||||
const normalized = String(value || "").trim().toUpperCase();
|
||||
if (normalized === "1K" || normalized === "2K" || normalized === "4K") {
|
||||
return normalized;
|
||||
const trimmed = String(value || "").trim();
|
||||
if (!trimmed) return fallback;
|
||||
const kMatch = trimmed.match(/^(\d+(?:\.\d+)?)\s*k$/i);
|
||||
if (kMatch?.[1]) {
|
||||
return `${Number(kMatch[1]).toString()}K`;
|
||||
}
|
||||
return fallback;
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function isCanvasImageSizeSupported(
|
||||
@ -1233,6 +1236,14 @@ function isCanvasImageSizeSupported(
|
||||
size: CanvasImageSizeValue
|
||||
) {
|
||||
if (!config) return true;
|
||||
const configuredSizes = Array.isArray(config.supportedImageSizes)
|
||||
? config.supportedImageSizes
|
||||
.map((item) => normalizeCanvasImageSizeValue(item, ""))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
if (configuredSizes.length > 0) {
|
||||
return configuredSizes.includes(normalizeCanvasImageSizeValue(size, ""));
|
||||
}
|
||||
if (size === "1K") return config.supports1K !== false;
|
||||
if (size === "2K") return config.supports2K !== false;
|
||||
if (size === "4K") return config.supports4K !== false;
|
||||
@ -1250,12 +1261,20 @@ function pickSupportedCanvasImageSize(
|
||||
"2K": ["1K"],
|
||||
"1K": ["2K", "4K"],
|
||||
};
|
||||
const configuredSizes = Array.isArray(config?.supportedImageSizes)
|
||||
? config.supportedImageSizes
|
||||
.map((item) => normalizeCanvasImageSizeValue(item, ""))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
|
||||
return (
|
||||
preferredFallbacks[requestedSize].find((candidate) =>
|
||||
(preferredFallbacks[requestedSize] || ["2K", "1K", "4K"]).find((candidate) =>
|
||||
isCanvasImageSizeSupported(config, candidate)
|
||||
) ||
|
||||
(["1K", "2K", "4K"] as const).find((candidate) =>
|
||||
configuredSizes.find((candidate) =>
|
||||
isCanvasImageSizeSupported(config, candidate)
|
||||
) ||
|
||||
["1K", "2K", "4K"].find((candidate) =>
|
||||
isCanvasImageSizeSupported(config, candidate)
|
||||
) ||
|
||||
"2K"
|
||||
|
||||
@ -14,15 +14,17 @@ export const SCENE_FUSION_IMAGE_SIZE_OPTIONS = [
|
||||
] as const;
|
||||
|
||||
export type SceneFusionAspectRatioValue =
|
||||
(typeof SCENE_FUSION_ASPECT_RATIO_OPTIONS)[number]["value"];
|
||||
string;
|
||||
export type SceneFusionImageSizeValue =
|
||||
(typeof SCENE_FUSION_IMAGE_SIZE_OPTIONS)[number]["value"];
|
||||
string;
|
||||
|
||||
export type SceneFusionImageGenConfig = {
|
||||
supportsSize?: boolean;
|
||||
supports1K?: boolean;
|
||||
supports2K?: boolean;
|
||||
supports4K?: boolean;
|
||||
supportedAspectRatios?: string[];
|
||||
supportedImageSizes?: string[];
|
||||
};
|
||||
|
||||
export type SceneFusionCapabilityCandidate = {
|
||||
@ -88,12 +90,43 @@ function orderedIntersect<T extends string>(base: readonly T[], groups: T[][]):
|
||||
return base.filter((item) => groups.every((group) => group.includes(item)));
|
||||
}
|
||||
|
||||
function getSupportedAspectRatiosByProvider(providerType: string): SceneFusionAspectRatioValue[] {
|
||||
function uniqueOrdered<T extends string>(values: T[]): T[] {
|
||||
const seen = new Set<T>();
|
||||
const result: T[] = [];
|
||||
for (const value of values) {
|
||||
if (!value || seen.has(value)) continue;
|
||||
seen.add(value);
|
||||
result.push(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalizeImageSizeValue(value: string) {
|
||||
const trimmed = String(value || "").trim();
|
||||
const kMatch = trimmed.match(/^(\d+(?:\.\d+)?)\s*k$/i);
|
||||
if (kMatch?.[1]) {
|
||||
return `${Number(kMatch[1]).toString()}K`;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function getSupportedAspectRatiosByProvider(
|
||||
providerType: string,
|
||||
imageGenConfig?: SceneFusionImageGenConfig | null
|
||||
): SceneFusionAspectRatioValue[] {
|
||||
if (Array.isArray(imageGenConfig?.supportedAspectRatios)) {
|
||||
const configured = uniqueOrdered(
|
||||
imageGenConfig.supportedAspectRatios
|
||||
.map((value) => String(value || "").trim())
|
||||
.filter(Boolean)
|
||||
);
|
||||
if (configured.length > 0) return configured;
|
||||
}
|
||||
|
||||
switch (normalizeSceneFusionProviderType(providerType)) {
|
||||
case "volcano-vision":
|
||||
return [];
|
||||
case "ruxa-image":
|
||||
case "snapgen-image":
|
||||
return ["1:1", "4:3", "3:4", "16:9", "9:16"];
|
||||
case "alibaba-image":
|
||||
case "fal-image":
|
||||
@ -111,6 +144,15 @@ function getSupportedImageSizesByConfig(
|
||||
return [];
|
||||
}
|
||||
|
||||
if (Array.isArray(imageGenConfig?.supportedImageSizes)) {
|
||||
const configured = uniqueOrdered(
|
||||
imageGenConfig.supportedImageSizes
|
||||
.map((value) => normalizeImageSizeValue(value))
|
||||
.filter(Boolean)
|
||||
);
|
||||
if (configured.length > 0) return configured;
|
||||
}
|
||||
|
||||
return ALL_IMAGE_SIZE_VALUES.filter((value) => {
|
||||
if (value === "1K") return imageGenConfig?.supports1K !== false;
|
||||
if (value === "2K") return imageGenConfig?.supports2K !== false;
|
||||
@ -141,7 +183,10 @@ function pickDefaultImageSize(options: SceneFusionImageSizeValue[]): SceneFusion
|
||||
}
|
||||
|
||||
export function getSceneFusionCandidateCapability(candidate: SceneFusionCapabilityCandidate) {
|
||||
const aspectRatioOptions = getSupportedAspectRatiosByProvider(candidate.provider.type);
|
||||
const aspectRatioOptions = getSupportedAspectRatiosByProvider(
|
||||
candidate.provider.type,
|
||||
candidate.model.imageGenConfig
|
||||
);
|
||||
const imageSizeOptions = getSupportedImageSizesByConfig(candidate.model.imageGenConfig);
|
||||
|
||||
return {
|
||||
@ -194,13 +239,19 @@ export function buildSceneFusionCapabilitySummary(
|
||||
|
||||
const sourceCandidates = preferredCandidate ? [preferredCandidate] : supportedCandidates;
|
||||
const strategy = preferredCandidate ? "preferred-model" : "candidate-intersection";
|
||||
const aspectRatioGroups = sourceCandidates.map(
|
||||
(candidate) => getSceneFusionCandidateCapability(candidate).aspectRatio.options
|
||||
);
|
||||
const imageSizeGroups = sourceCandidates.map(
|
||||
(candidate) => getSceneFusionCandidateCapability(candidate).imageSize.options
|
||||
);
|
||||
const aspectRatioOptions = orderedIntersect(
|
||||
ALL_ASPECT_RATIO_VALUES,
|
||||
sourceCandidates.map((candidate) => getSceneFusionCandidateCapability(candidate).aspectRatio.options)
|
||||
uniqueOrdered([...ALL_ASPECT_RATIO_VALUES, ...aspectRatioGroups.flat()]),
|
||||
aspectRatioGroups
|
||||
);
|
||||
const imageSizeOptions = orderedIntersect(
|
||||
ALL_IMAGE_SIZE_VALUES,
|
||||
sourceCandidates.map((candidate) => getSceneFusionCandidateCapability(candidate).imageSize.options)
|
||||
uniqueOrdered([...ALL_IMAGE_SIZE_VALUES, ...imageSizeGroups.flat()]),
|
||||
imageSizeGroups
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user