fix: stabilize ai wash model handling
This commit is contained in:
parent
7aca7a764d
commit
91fe35e3b6
@ -109,8 +109,10 @@ export function LocalAiImagePipelineSections({
|
||||
handleSaveLayerSplitConfig,
|
||||
}: LocalAiImagePipelineSectionsProps) {
|
||||
const availableImageModels = (modelsData?.models || []).filter(
|
||||
(model) => model.supportsImageGen && model.isEnabled && !model.isHidden
|
||||
(model) => model.supportsImageGen && model.isEnabled
|
||||
);
|
||||
const formatAiWashModelOptionLabel = (model: (typeof availableImageModels)[number]) =>
|
||||
`${model.displayName || model.modelId}${model.isHidden ? " · 前台隐藏" : ""}`;
|
||||
const getAiWashControlToneCopy = (controlId: AiWashControlId) => {
|
||||
if (controlId === "creativity") {
|
||||
return "每档可以单独指定模型;如果留空,前台才会开放模型下拉选择。";
|
||||
@ -1768,7 +1770,7 @@ export function LocalAiImagePipelineSections({
|
||||
{availableImageModels.length > 0 ? (
|
||||
availableImageModels.map((model) => (
|
||||
<SelectItem key={model.id} value={model.modelId}>
|
||||
{model.displayName || model.modelId}
|
||||
{formatAiWashModelOptionLabel(model)}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
@ -2130,7 +2132,7 @@ export function LocalAiImagePipelineSections({
|
||||
key={model.id}
|
||||
value={model.modelId}
|
||||
>
|
||||
{model.displayName || model.modelId}
|
||||
{formatAiWashModelOptionLabel(model)}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
|
||||
@ -341,6 +341,17 @@ function getControlLevelByIndex(
|
||||
return control.levels[clampedIndex] || control.levels[0] || null;
|
||||
}
|
||||
|
||||
function getPresetDefaultLevelIndexByControl(preset?: AiWashPreset | null) {
|
||||
if (!preset) return {};
|
||||
return AI_WASH_CONTROL_ORDER.reduce<Record<string, number>>(
|
||||
(accumulator, controlId) => {
|
||||
accumulator[controlId] = getPresetControl(preset, controlId)?.defaultLevel ?? 0;
|
||||
return accumulator;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
function getAiWashStatusLabel(status?: AiWashResultStatus) {
|
||||
switch (status) {
|
||||
case "submitting":
|
||||
@ -481,17 +492,6 @@ export function AiWashWorkspace({
|
||||
() => getEnabledAiWashPresets(config).filter((preset) => preset.enabled),
|
||||
[config]
|
||||
);
|
||||
const primaryPreset = useMemo(
|
||||
() => enabledPresets.find((preset) => preset.isDefault) || enabledPresets[0] || null,
|
||||
[enabledPresets]
|
||||
);
|
||||
const uiControls = useMemo(
|
||||
() =>
|
||||
AI_WASH_CONTROL_ORDER.map(
|
||||
(controlId) => getPresetControl(primaryPreset as AiWashPreset, controlId)
|
||||
).filter(Boolean) as AiWashControl[],
|
||||
[primaryPreset]
|
||||
);
|
||||
const compareSourceUrl = String(
|
||||
historySourceOverride || sourceImageUrl || ""
|
||||
).trim();
|
||||
@ -516,6 +516,22 @@ export function AiWashWorkspace({
|
||||
selectedPreviewResult?.status === "succeeded" ? selectedPreviewResult : null,
|
||||
[selectedPreviewResult]
|
||||
);
|
||||
const selectedPreset = useMemo(
|
||||
() =>
|
||||
enabledPresets.find((preset) => preset.id === selectedResultId) ||
|
||||
enabledPresets.find((preset) => preset.id === selectedPreviewResult?.presetId) ||
|
||||
enabledPresets.find((preset) => preset.isDefault) ||
|
||||
enabledPresets[0] ||
|
||||
null,
|
||||
[enabledPresets, selectedPreviewResult?.presetId, selectedResultId]
|
||||
);
|
||||
const uiControls = useMemo(
|
||||
() =>
|
||||
AI_WASH_CONTROL_ORDER.map(
|
||||
(controlId) => getPresetControl(selectedPreset, controlId)
|
||||
).filter(Boolean) as AiWashControl[],
|
||||
[selectedPreset]
|
||||
);
|
||||
const visibleHistoryBatches = useMemo(() => {
|
||||
const scoped = persistedHistoryBatches
|
||||
.filter(
|
||||
@ -595,13 +611,7 @@ export function AiWashWorkspace({
|
||||
const defaultPreset =
|
||||
enabledPresets.find((preset) => preset.isDefault) || enabledPresets[0];
|
||||
setSelectedResultId(defaultPreset?.id || "");
|
||||
setSelectedLevelIndexByControl(
|
||||
AI_WASH_CONTROL_ORDER.reduce<Record<string, number>>((accumulator, controlId) => {
|
||||
accumulator[controlId] =
|
||||
getPresetControl(defaultPreset, controlId)?.defaultLevel ?? 0;
|
||||
return accumulator;
|
||||
}, {})
|
||||
);
|
||||
setSelectedLevelIndexByControl(getPresetDefaultLevelIndexByControl(defaultPreset));
|
||||
setComparePositionById(
|
||||
enabledPresets.reduce<Record<string, number>>((accumulator, preset) => {
|
||||
accumulator[preset.id] = 50;
|
||||
@ -626,10 +636,10 @@ export function AiWashWorkspace({
|
||||
setLastErrorMessage("");
|
||||
setHistorySourceOverride("");
|
||||
setResults(createIdleResultCards(enabledPresets));
|
||||
setSelectedResultId(
|
||||
(enabledPresets.find((preset) => preset.isDefault) || enabledPresets[0])?.id ||
|
||||
""
|
||||
);
|
||||
const defaultPreset =
|
||||
enabledPresets.find((preset) => preset.isDefault) || enabledPresets[0] || null;
|
||||
setSelectedResultId(defaultPreset?.id || "");
|
||||
setSelectedLevelIndexByControl(getPresetDefaultLevelIndexByControl(defaultPreset));
|
||||
}, [enabledPresets, sourceImageUrl]);
|
||||
|
||||
const persistHistoryBatches = useCallback(
|
||||
@ -968,15 +978,18 @@ export function AiWashWorkspace({
|
||||
}, [compareSourceUrl, onApplyResult, selectedResult]);
|
||||
|
||||
const handleLoadHistoryBatch = useCallback((entry: AiWashHistoryBatch) => {
|
||||
const loadedPresetId =
|
||||
(entry.items.find((item) => item.isDefault) || entry.items[0])?.presetId || "";
|
||||
const loadedPreset =
|
||||
enabledPresets.find((preset) => preset.id === loadedPresetId) || null;
|
||||
setResults(
|
||||
entry.items.map((item) => ({
|
||||
...item,
|
||||
status: "succeeded",
|
||||
}))
|
||||
);
|
||||
setSelectedResultId(
|
||||
(entry.items.find((item) => item.isDefault) || entry.items[0])?.presetId || ""
|
||||
);
|
||||
setSelectedResultId(loadedPresetId);
|
||||
setSelectedLevelIndexByControl(getPresetDefaultLevelIndexByControl(loadedPreset));
|
||||
setHistorySourceOverride(entry.sourceImageUrl || "");
|
||||
setComparePositionById(
|
||||
entry.items.reduce<Record<string, number>>((accumulator, item) => {
|
||||
@ -986,7 +999,7 @@ export function AiWashWorkspace({
|
||||
);
|
||||
setLastErrorMessage("");
|
||||
toast.success("已加载历史 AI洗图记录");
|
||||
}, []);
|
||||
}, [enabledPresets]);
|
||||
|
||||
const handleDeleteHistoryBatch = useCallback(
|
||||
(entryId: string) => {
|
||||
@ -1002,6 +1015,10 @@ export function AiWashWorkspace({
|
||||
? comparePositionById[selectedPreviewResult.presetId] ?? 50
|
||||
: 50;
|
||||
const previewHasImage = Boolean(selectedPreviewResult?.imageUrl);
|
||||
const previewIsPending =
|
||||
selectedPreviewResult?.status === "running" ||
|
||||
selectedPreviewResult?.status === "submitting" ||
|
||||
selectedPreviewResult?.status === "failed";
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full bg-[#ffffff] text-[#111111]">
|
||||
@ -1088,7 +1105,12 @@ export function AiWashWorkspace({
|
||||
: "border-[#e9e9e9] bg-white/92 text-[#666666] hover:border-[#d6d6d6] hover:text-[#111111]"
|
||||
)}
|
||||
key={item.preset.id}
|
||||
onClick={() => setSelectedResultId(item.preset.id)}
|
||||
onClick={() => {
|
||||
setSelectedResultId(item.preset.id);
|
||||
setSelectedLevelIndexByControl(
|
||||
getPresetDefaultLevelIndexByControl(item.preset)
|
||||
);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
@ -1309,19 +1331,17 @@ export function AiWashWorkspace({
|
||||
) : null}
|
||||
</div>
|
||||
) : compareSourceUrl ? (
|
||||
<div className="relative max-h-full max-w-full">
|
||||
<div className="relative max-h-full max-w-full overflow-hidden rounded-[22px] shadow-[0_16px_44px_rgba(0,0,0,0.07)]">
|
||||
<img
|
||||
alt="AI洗图底图"
|
||||
className="block max-h-[calc(100vh-284px)] max-w-[min(62vw,1080px)] rounded-[22px] object-contain shadow-[0_16px_44px_rgba(0,0,0,0.07)]"
|
||||
className="block max-h-[calc(100vh-284px)] max-w-[min(62vw,1080px)] object-contain"
|
||||
src={getDisplayImageUrl(compareSourceUrl, 1800)}
|
||||
/>
|
||||
<div className="pointer-events-none absolute left-4 top-4 rounded-full bg-black/58 px-2.5 py-1 text-[11px] text-white">
|
||||
原图
|
||||
</div>
|
||||
{selectedPreviewResult?.status === "running" ||
|
||||
selectedPreviewResult?.status === "submitting" ||
|
||||
selectedPreviewResult?.status === "failed" ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded-[22px] bg-white/58 px-6 text-center backdrop-blur-[1px]">
|
||||
{previewIsPending ? (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/78 px-6 text-center backdrop-blur-[2px]">
|
||||
<div className="max-w-sm rounded-[22px] border border-[#ececec] bg-white/92 px-5 py-4 shadow-[0_14px_34px_rgba(0,0,0,0.07)]">
|
||||
{selectedPreviewResult.status === "running" ||
|
||||
selectedPreviewResult.status === "submitting" ? (
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
type ModelDescriptor = {
|
||||
displayName?: unknown;
|
||||
modelId?: unknown;
|
||||
supportsVision?: boolean | null;
|
||||
capabilities?: unknown;
|
||||
@ -11,6 +12,8 @@ type ProviderDescriptor = {
|
||||
|
||||
const BFL_REFERENCE_IMAGE_MODEL_REGEX =
|
||||
/^(?:bfl\/)?(?:flux-2-(?:max|flex|pro(?:-preview)?|klein-(?:4b|9b)(?:-preview)?)|flux-kontext-(?:pro|max))$/i;
|
||||
const REFERENCE_IMAGE_MODE_REGEX =
|
||||
/^(?:image-edit|image-to-image|img2img|reference-images?|image-reference|reference-image)$/i;
|
||||
|
||||
function readStringField(source: unknown, key: string) {
|
||||
if (!source || typeof source !== "object" || !(key in source)) return "";
|
||||
@ -28,14 +31,29 @@ function normalizeBflModelId(value: unknown) {
|
||||
.replace(/[?#].*$/, "");
|
||||
}
|
||||
|
||||
function readStringArrayField(source: unknown, key: string) {
|
||||
if (!source || typeof source !== "object" || !(key in source)) return [];
|
||||
const value = (source as Record<string, unknown>)[key];
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.map((item) => String(item || "").trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function hasReferenceImageMode(value: unknown) {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const supportedModes = [
|
||||
...readStringArrayField(value, "supportedModes"),
|
||||
...readStringArrayField(value, "modes"),
|
||||
];
|
||||
return supportedModes.some((mode) => REFERENCE_IMAGE_MODE_REGEX.test(mode));
|
||||
}
|
||||
|
||||
export function isKnownReferenceImageModel(
|
||||
model: ModelDescriptor | null | undefined,
|
||||
provider?: ProviderDescriptor | null
|
||||
) {
|
||||
const providerType = String(provider?.type || "").trim();
|
||||
if (providerType !== "bfl-image") return false;
|
||||
|
||||
const candidates = [
|
||||
model?.displayName,
|
||||
model?.modelId,
|
||||
readStringField(model?.defaultParams, "endpoint"),
|
||||
readStringField(model?.defaultParams, "submitEndpoint"),
|
||||
@ -51,9 +69,16 @@ export function isKnownReferenceImageModel(
|
||||
|
||||
candidates.push(readStringField(capabilitiesEndpoints, "submit"));
|
||||
|
||||
return candidates
|
||||
const matchesKnownBflModel = candidates
|
||||
.map(normalizeBflModelId)
|
||||
.some((modelId) => BFL_REFERENCE_IMAGE_MODEL_REGEX.test(modelId));
|
||||
if (matchesKnownBflModel) return true;
|
||||
|
||||
if (providerType === "bfl-image") {
|
||||
return hasReferenceImageMode(model?.capabilities) || hasReferenceImageMode(model?.defaultParams);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function modelLacksReferenceImageInputSupport(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user