343 lines
10 KiB
TypeScript
343 lines
10 KiB
TypeScript
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 =
|
|
string;
|
|
export type SceneFusionImageSizeValue =
|
|
string;
|
|
|
|
export type SceneFusionImageGenConfig = {
|
|
supportsSize?: boolean;
|
|
supports1K?: boolean;
|
|
supports2K?: boolean;
|
|
supports4K?: boolean;
|
|
supportedAspectRatios?: string[];
|
|
supportedImageSizes?: string[];
|
|
};
|
|
|
|
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 "snapgen-image":
|
|
case "openai-image":
|
|
case "bananaimage":
|
|
case "visionary":
|
|
case "atlascloud":
|
|
case "vip98k":
|
|
case "gemini":
|
|
case "volcano-vision":
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function orderedIntersect<T extends string>(base: readonly T[], groups: T[][]): T[] {
|
|
if (groups.length === 0) return [...base];
|
|
return base.filter((item) => groups.every((group) => group.includes(item)));
|
|
}
|
|
|
|
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":
|
|
return ["1:1", "4:3", "3:4", "16:9", "9:16"];
|
|
case "alibaba-image":
|
|
case "fal-image":
|
|
case "openai-image":
|
|
case "bananaimage":
|
|
case "visionary":
|
|
case "atlascloud":
|
|
case "vip98k":
|
|
case "gemini":
|
|
default:
|
|
return [...ALL_ASPECT_RATIO_VALUES];
|
|
}
|
|
}
|
|
|
|
function getSupportedImageSizesByConfig(
|
|
imageGenConfig?: SceneFusionImageGenConfig | null
|
|
): SceneFusionImageSizeValue[] {
|
|
if (imageGenConfig?.supportsSize === false) {
|
|
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;
|
|
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,
|
|
candidate.model.imageGenConfig
|
|
);
|
|
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 aspectRatioGroups = sourceCandidates.map(
|
|
(candidate) => getSceneFusionCandidateCapability(candidate).aspectRatio.options
|
|
);
|
|
const imageSizeGroups = sourceCandidates.map(
|
|
(candidate) => getSceneFusionCandidateCapability(candidate).imageSize.options
|
|
);
|
|
const aspectRatioOptions = orderedIntersect(
|
|
uniqueOrdered([...ALL_ASPECT_RATIO_VALUES, ...aspectRatioGroups.flat()]),
|
|
aspectRatioGroups
|
|
);
|
|
const imageSizeOptions = orderedIntersect(
|
|
uniqueOrdered([...ALL_IMAGE_SIZE_VALUES, ...imageSizeGroups.flat()]),
|
|
imageSizeGroups
|
|
);
|
|
|
|
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,
|
|
};
|
|
}
|