diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index cdc19382a..e598d881f 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -641,6 +641,8 @@ type ImageGenConfig = { supportsNegativePrompt?: boolean; // 是否支持反向提示词 supportsWatermark?: boolean; // 是否支持水印开关 supportsSeed?: boolean; // 是否支持随机种子 + supportedAspectRatios?: string[]; // 支持的图片比例 + supportedImageSizes?: string[]; // 支持的分辨率/K 数 creditCostPerImage?: number; // 每张图片积分消耗 creditCostMultiplierBySize?: Record; // 各图片尺寸积分倍率 creditDiscountRate?: number; // 积分折扣,按“折”配置,如 5 = 5折 diff --git a/components/image-params-bar.tsx b/components/image-params-bar.tsx index 40c021d35..e8eae476a 100644 --- a/components/image-params-bar.tsx +++ b/components/image-params-bar.tsx @@ -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 = { +const IMAGE_SIZE_PIXEL_MAP: Record = { + "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(); + 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( + 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(); 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({ + {sizeOptions.length > 0 && (
Size
@@ -497,6 +565,7 @@ export function ImageParamsBar({ })}
+ )}
Image
@@ -613,6 +682,7 @@ export function ImageParamsBar({
+ {availableSizeOptions.length > 0 && (

选择分辨率

+ )}

尺寸

diff --git a/components/multimodal-input.tsx b/components/multimodal-input.tsx index aa5dd8084..145daf751 100644 --- a/components/multimodal-input.tsx +++ b/components/multimodal-input.tsx @@ -40,6 +40,8 @@ type DynamicModel = { supportsNegativePrompt?: boolean; supportsWatermark?: boolean; supportsSeed?: boolean; + supportedAspectRatios?: string[]; + supportedImageSizes?: string[]; creditCostPerImage?: number; creditCostMultiplierBySize?: Record; creditDiscountRate?: number; diff --git a/components/scene-fusion-view.tsx b/components/scene-fusion-view.tsx index 23f15f3ae..cbca236f9 100644 --- a/components/scene-fusion-view.tsx +++ b/components/scene-fusion-view.tsx @@ -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( + SCENE_FUSION_ASPECT_RATIO_OPTIONS.map((option) => [ + option.value, + option.label, + ]) ), + [] + ); + const imageSizeOptionLabel = useMemo( + () => + new Map( + 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, ] diff --git a/lib/ai/snapgen-image-models.ts b/lib/ai/snapgen-image-models.ts index 73c5675e7..1f2dbcb68 100644 --- a/lib/ai/snapgen-image-models.ts +++ b/lib/ai/snapgen-image-models.ts @@ -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, }, diff --git a/lib/canvas/generate-image.ts b/lib/canvas/generate-image.ts index 2f42ed8a4..ea98c504d 100644 --- a/lib/canvas/generate-image.ts +++ b/lib/canvas/generate-image.ts @@ -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 >[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" diff --git a/lib/scene-fusion/output-capabilities.ts b/lib/scene-fusion/output-capabilities.ts index 7beac9499..871c380d9 100644 --- a/lib/scene-fusion/output-capabilities.ts +++ b/lib/scene-fusion/output-capabilities.ts @@ -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(base: readonly T[], groups: T[][]): return base.filter((item) => groups.every((group) => group.includes(item))); } -function getSupportedAspectRatiosByProvider(providerType: string): SceneFusionAspectRatioValue[] { +function uniqueOrdered(values: T[]): T[] { + const seen = new Set(); + 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 {