export const SCENE_FUSION_ASPECT_RATIO_OPTIONS = [ { label: "自动", value: "auto" }, { label: "1:1", value: "1:1" }, { label: "4:3", value: "4:3" }, { label: "3:4", value: "3:4" }, { label: "16:9", value: "16:9" }, { label: "9:16", value: "9:16" }, ] as const; export const SCENE_FUSION_IMAGE_SIZE_OPTIONS = [ { label: "标准 (1K)", value: "1K" }, { label: "高清 (2K)", value: "2K" }, { label: "超清 (4K)", value: "4K" }, ] as const; export type SceneFusionAspectRatioValue = (typeof SCENE_FUSION_ASPECT_RATIO_OPTIONS)[number]["value"]; export type SceneFusionImageSizeValue = (typeof SCENE_FUSION_IMAGE_SIZE_OPTIONS)[number]["value"]; export type SceneFusionImageGenConfig = { supportsSize?: boolean; supports1K?: boolean; supports2K?: boolean; supports4K?: boolean; }; export type SceneFusionCapabilityCandidate = { model: { id: string; displayName: string; imageGenConfig?: SceneFusionImageGenConfig | null; }; provider: { type: string; }; }; export type SceneFusionCapabilitySummary = { strategy: "preferred-model" | "candidate-intersection" | "fallback-default"; strategyLabel: string; preferredModelId: string | null; preferredModelName: string | null; candidateCount: number; note: string | null; aspectRatio: { enabled: boolean; options: SceneFusionAspectRatioValue[]; defaultValue: SceneFusionAspectRatioValue; }; imageSize: { enabled: boolean; options: SceneFusionImageSizeValue[]; defaultValue: SceneFusionImageSizeValue; }; }; const DEFAULT_ASPECT_RATIO: SceneFusionAspectRatioValue = "auto"; const DEFAULT_IMAGE_SIZE: SceneFusionImageSizeValue = "2K"; const ALL_ASPECT_RATIO_VALUES = SCENE_FUSION_ASPECT_RATIO_OPTIONS.map( (option) => option.value ); const ALL_IMAGE_SIZE_VALUES = SCENE_FUSION_IMAGE_SIZE_OPTIONS.map((option) => option.value); export function normalizeSceneFusionProviderType(providerType: string): string { return providerType === "gemini-image" ? "gemini" : providerType; } export function canProcessSceneFusionProviderType(providerType: string): boolean { const normalized = normalizeSceneFusionProviderType(providerType); switch (normalized) { case "alibaba-image": case "ruxa-image": case "fal-image": case "openai-image": case "gemini": case "volcano-vision": return true; default: return false; } } function orderedIntersect(base: readonly T[], groups: T[][]): T[] { if (groups.length === 0) return [...base]; return base.filter((item) => groups.every((group) => group.includes(item))); } function getSupportedAspectRatiosByProvider(providerType: string): SceneFusionAspectRatioValue[] { switch (normalizeSceneFusionProviderType(providerType)) { case "volcano-vision": return []; case "ruxa-image": return ["1:1", "4:3", "3:4", "16:9", "9:16"]; case "alibaba-image": case "fal-image": case "openai-image": case "gemini": default: return [...ALL_ASPECT_RATIO_VALUES]; } } function getSupportedImageSizesByConfig( imageGenConfig?: SceneFusionImageGenConfig | null ): SceneFusionImageSizeValue[] { if (imageGenConfig?.supportsSize === false) { return []; } return ALL_IMAGE_SIZE_VALUES.filter((value) => { if (value === "1K") return imageGenConfig?.supports1K !== false; if (value === "2K") return imageGenConfig?.supports2K !== false; if (value === "4K") return imageGenConfig?.supports4K !== false; return true; }); } function pickDefaultAspectRatio( options: SceneFusionAspectRatioValue[] ): SceneFusionAspectRatioValue { return ( options.find((value) => value === DEFAULT_ASPECT_RATIO) || options.find((value) => value === "1:1") || options[0] || DEFAULT_ASPECT_RATIO ); } function pickDefaultImageSize(options: SceneFusionImageSizeValue[]): SceneFusionImageSizeValue { return ( options.find((value) => value === DEFAULT_IMAGE_SIZE) || options.find((value) => value === "1K") || options.find((value) => value === "4K") || options[0] || DEFAULT_IMAGE_SIZE ); } export function getSceneFusionCandidateCapability(candidate: SceneFusionCapabilityCandidate) { const aspectRatioOptions = getSupportedAspectRatiosByProvider(candidate.provider.type); const imageSizeOptions = getSupportedImageSizesByConfig(candidate.model.imageGenConfig); return { aspectRatio: { enabled: aspectRatioOptions.length > 0, options: aspectRatioOptions, defaultValue: pickDefaultAspectRatio(aspectRatioOptions), }, imageSize: { enabled: imageSizeOptions.length > 0, options: imageSizeOptions, defaultValue: pickDefaultImageSize(imageSizeOptions), }, }; } export function buildSceneFusionCapabilitySummary( candidates: SceneFusionCapabilityCandidate[], preferredModelId?: string | null ): SceneFusionCapabilitySummary { const supportedCandidates = candidates.filter((candidate) => canProcessSceneFusionProviderType(candidate.provider.type) ); if (supportedCandidates.length === 0) { return { strategy: "fallback-default", strategyLabel: "未检测到可用模型,使用默认参数", preferredModelId: null, preferredModelName: null, candidateCount: 0, note: "后台当前没有可用于批量处理的生图模型,以下参数仅作为默认值展示。", aspectRatio: { enabled: true, options: [...ALL_ASPECT_RATIO_VALUES], defaultValue: DEFAULT_ASPECT_RATIO, }, imageSize: { enabled: true, options: [...ALL_IMAGE_SIZE_VALUES], defaultValue: DEFAULT_IMAGE_SIZE, }, }; } const preferredCandidate = preferredModelId ? supportedCandidates.find((candidate) => candidate.model.id === preferredModelId) || null : null; const sourceCandidates = preferredCandidate ? [preferredCandidate] : supportedCandidates; const strategy = preferredCandidate ? "preferred-model" : "candidate-intersection"; const aspectRatioOptions = orderedIntersect( ALL_ASPECT_RATIO_VALUES, sourceCandidates.map((candidate) => getSceneFusionCandidateCapability(candidate).aspectRatio.options) ); const imageSizeOptions = orderedIntersect( ALL_IMAGE_SIZE_VALUES, sourceCandidates.map((candidate) => getSceneFusionCandidateCapability(candidate).imageSize.options) ); return { strategy, strategyLabel: preferredCandidate ? `按首选模型 ${preferredCandidate.model.displayName} 的能力展示` : `未指定首选模型,按 ${supportedCandidates.length} 个候选模型交集展示`, preferredModelId: preferredCandidate?.model.id || null, preferredModelName: preferredCandidate?.model.displayName || null, candidateCount: supportedCandidates.length, note: preferredCandidate ? supportedCandidates.length > 1 ? "当首选模型失败回退到其他模型时,系统会自动按目标模型能力降级不支持的参数。" : "当前仅使用一个模型,参数会严格按该模型能力生效。" : "多模型轮换时,不支持的参数已在前端按交集隐藏,后端也会继续做能力兜底。", aspectRatio: { enabled: aspectRatioOptions.length > 0, options: aspectRatioOptions, defaultValue: pickDefaultAspectRatio(aspectRatioOptions), }, imageSize: { enabled: imageSizeOptions.length > 0, options: imageSizeOptions, defaultValue: pickDefaultImageSize(imageSizeOptions), }, }; } export function normalizeSceneFusionOutputParamsForCandidate( candidate: SceneFusionCapabilityCandidate, requested: { aspectRatio?: string | null; imageSize?: string | null; } ) { const capability = getSceneFusionCandidateCapability(candidate); const requestedAspectRatio = String(requested.aspectRatio || "").trim() as SceneFusionAspectRatioValue; const requestedImageSize = String(requested.imageSize || "").trim().toUpperCase() as SceneFusionImageSizeValue; const aspectRatio = !capability.aspectRatio.enabled ? undefined : capability.aspectRatio.options.includes(requestedAspectRatio) ? requestedAspectRatio : requestedAspectRatio ? capability.aspectRatio.defaultValue : undefined; const imageSize = !capability.imageSize.enabled ? undefined : capability.imageSize.options.includes(requestedImageSize) ? requestedImageSize : requestedImageSize ? capability.imageSize.defaultValue : undefined; const changes: string[] = []; if (requestedAspectRatio && aspectRatio !== requestedAspectRatio) { changes.push( aspectRatio ? `aspectRatio:${requestedAspectRatio}->${aspectRatio}` : `aspectRatio:${requestedAspectRatio}->default` ); } if (requestedImageSize && imageSize !== requestedImageSize) { changes.push( imageSize ? `imageSize:${requestedImageSize}->${imageSize}` : `imageSize:${requestedImageSize}->default` ); } return { aspectRatio, imageSize, changed: changes.length > 0, changes, }; }