diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 4686ad070e..c13020c608 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -1490,6 +1490,7 @@
"copyImage": "Copy Image",
"denoisingStrength": "Denoising Strength",
"disabledNoRasterContent": "Disabled (No Raster Content)",
+ "disabledNotSupported": "Not supported by model",
"downloadImage": "Download Image",
"general": "General",
"guidance": "Guidance",
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts
index ed2c67d529..25bad13f4b 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts
@@ -4,9 +4,11 @@ import { bboxSyncedToOptimalDimension, rgRefImageModelChanged } from 'features/c
import { buildSelectIsStaging, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { loraIsEnabledChanged } from 'features/controlLayers/store/lorasSlice';
import {
+ aspectRatioIdChanged,
kleinQwen3EncoderModelSelected,
kleinVaeModelSelected,
modelChanged,
+ resolutionPresetSelected,
setZImageScheduler,
syncedToOptimalDimension,
vaeSelected,
@@ -24,7 +26,7 @@ import {
selectBboxModelBase,
selectCanvasSlice,
} from 'features/controlLayers/store/selectors';
-import { getEntityIdentifier, isFlux2ReferenceImageConfig } from 'features/controlLayers/store/types';
+import { getEntityIdentifier, isAspectRatioID, isFlux2ReferenceImageConfig } from 'features/controlLayers/store/types';
import {
initialFlux2ReferenceImage,
initialFluxKontextReferenceImage,
@@ -46,7 +48,7 @@ import {
selectZImageDiffusersModels,
} from 'services/api/hooks/modelsByType';
import type { FLUXKontextModelConfig, FLUXReduxModelConfig, IPAdapterModelConfig } from 'services/api/types';
-import { isFluxKontextModelConfig, isFluxReduxModelConfig } from 'services/api/types';
+import { isExternalApiModelConfig, isFluxKontextModelConfig, isFluxReduxModelConfig } from 'services/api/types';
const log = logger('models');
@@ -352,6 +354,34 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
dispatch(bboxSyncedToOptimalDimension());
}
}
+
+ // When switching to an external model, sync bbox to the model's first preset dimensions
+ if (newBase === 'external') {
+ const modelConfigsResult = selectModelConfigsQuery(getState());
+ if (modelConfigsResult.data) {
+ const newModelConfig = modelConfigsAdapterSelectors.selectById(modelConfigsResult.data, newModel.key);
+ if (newModelConfig && isExternalApiModelConfig(newModelConfig)) {
+ const { aspect_ratio_sizes, resolution_presets } = newModelConfig.capabilities;
+ if (resolution_presets && resolution_presets.length > 0) {
+ const firstPreset = resolution_presets[0]!;
+ dispatch(
+ resolutionPresetSelected({
+ imageSize: firstPreset.image_size,
+ aspectRatio: firstPreset.aspect_ratio,
+ width: firstPreset.width,
+ height: firstPreset.height,
+ })
+ );
+ } else if (aspect_ratio_sizes) {
+ const firstRatio = Object.keys(aspect_ratio_sizes)[0];
+ const firstSize = firstRatio ? aspect_ratio_sizes[firstRatio] : undefined;
+ if (firstRatio && firstSize && isAspectRatioID(firstRatio)) {
+ dispatch(aspectRatioIdChanged({ id: firstRatio, fixedSize: firstSize }));
+ }
+ }
+ }
+ }
+ }
},
});
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ParamDenoisingStrength.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ParamDenoisingStrength.tsx
index 34fb96f063..658fb8b745 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/ParamDenoisingStrength.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ParamDenoisingStrength.tsx
@@ -11,7 +11,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import WavyLine from 'common/components/WavyLine';
-import { selectImg2imgStrength, setImg2imgStrength } from 'features/controlLayers/store/paramsSlice';
+import { selectImg2imgStrength, selectIsExternal, setImg2imgStrength } from 'features/controlLayers/store/paramsSlice';
import { selectActiveRasterLayerEntities } from 'features/controlLayers/store/selectors';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -37,6 +37,7 @@ export const ParamDenoisingStrength = memo(() => {
const img2imgStrength = useAppSelector(selectImg2imgStrength);
const dispatch = useAppDispatch();
const hasRasterLayersWithContent = useAppSelector(selectHasRasterLayersWithContent);
+ const isExternal = useAppSelector(selectIsExternal);
const selectedModelConfig = useSelectedModelConfig();
const onChange = useCallback(
@@ -55,12 +56,16 @@ export const ParamDenoisingStrength = memo(() => {
// Denoising strength does nothing if there are no raster layers w/ content
return true;
}
+ if (isExternal) {
+ // External models don't support denoise strength - they handle img2img via prompt
+ return true;
+ }
if (selectedModelConfig && isFluxFillMainModelModelConfig(selectedModelConfig)) {
// Denoising strength is ignored by FLUX Fill, which is indicated by the variant being 'inpaint'
return true;
}
return false;
- }, [hasRasterLayersWithContent, selectedModelConfig]);
+ }, [hasRasterLayersWithContent, isExternal, selectedModelConfig]);
return (
@@ -96,7 +101,9 @@ export const ParamDenoisingStrength = memo(() => {
>
) : (
- {t('parameters.disabledNoRasterContent')}
+
+ {isExternal ? t('parameters.disabledNotSupported') : t('parameters.disabledNoRasterContent')}
+
)}
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasBboxToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasBboxToolModule.ts
index ecf9a5d1c7..2ab2d1f281 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasBboxToolModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasBboxToolModule.ts
@@ -10,7 +10,7 @@ import {
getPrefixedId,
} from 'features/controlLayers/konva/util';
import { selectBboxOverlay } from 'features/controlLayers/store/canvasSettingsSlice';
-import { selectModel } from 'features/controlLayers/store/paramsSlice';
+import { selectHasFixedDimensionSizes, selectModel } from 'features/controlLayers/store/paramsSlice';
import { selectBbox } from 'features/controlLayers/store/selectors';
import type { Coordinate, Rect, Tool } from 'features/controlLayers/store/types';
import Konva from 'konva';
@@ -191,6 +191,9 @@ export class CanvasBboxToolModule extends CanvasModuleBase {
// Listen for the model changing - some model types constraint the bbox to a certain size or aspect ratio.
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectModel, this.render));
+ // Listen for fixed dimension sizes changes - external models may lock bbox resizing
+ this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectHasFixedDimensionSizes, this.render));
+
// Update on busy state changes
this.subscriptions.add(this.manager.$isBusy.listen(this.render));
@@ -246,6 +249,10 @@ export class CanvasBboxToolModule extends CanvasModuleBase {
if (tool !== 'bbox') {
return NO_ANCHORS;
}
+ // External models with fixed dimension presets don't allow free bbox resizing
+ if (this.manager.stateApi.runSelector(selectHasFixedDimensionSizes)) {
+ return NO_ANCHORS;
+ }
return ALL_ANCHORS;
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
index 79d3963d12..9c283f188f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
@@ -7,7 +7,7 @@ import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMul
import { merge } from 'es-toolkit/compat';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { canvasReset } from 'features/controlLayers/store/actions';
-import { modelChanged } from 'features/controlLayers/store/paramsSlice';
+import { aspectRatioIdChanged, modelChanged, resolutionPresetSelected } from 'features/controlLayers/store/paramsSlice';
import {
selectAllEntities,
selectAllEntitiesOfType,
@@ -31,6 +31,7 @@ import type {
RgbColor,
SimpleAdjustmentsConfig,
} from 'features/controlLayers/store/types';
+import { isAspectRatioID } from 'features/controlLayers/store/types';
import {
calculateNewSize,
getScaledBoundingBoxDimensions,
@@ -1279,21 +1280,31 @@ const slice = createSlice({
state.bbox.aspectRatio.isLocked = !state.bbox.aspectRatio.isLocked;
syncScaledSize(state);
},
- bboxAspectRatioIdChanged: (state, action: PayloadAction<{ id: AspectRatioID }>) => {
- const { id } = action.payload;
+ bboxAspectRatioIdChanged: (
+ state,
+ action: PayloadAction<{ id: AspectRatioID; fixedSize?: { width: number; height: number } }>
+ ) => {
+ const { id, fixedSize } = action.payload;
state.bbox.aspectRatio.id = id;
if (id === 'Free') {
state.bbox.aspectRatio.isLocked = false;
} else {
state.bbox.aspectRatio.isLocked = true;
- state.bbox.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio;
- const { width, height } = calculateNewSize(
- state.bbox.aspectRatio.value,
- state.bbox.rect.width * state.bbox.rect.height,
- state.bbox.modelBase
- );
- state.bbox.rect.width = width;
- state.bbox.rect.height = height;
+ if (fixedSize) {
+ // External models provide fixed dimensions for each aspect ratio
+ state.bbox.aspectRatio.value = fixedSize.width / fixedSize.height;
+ state.bbox.rect.width = fixedSize.width;
+ state.bbox.rect.height = fixedSize.height;
+ } else {
+ state.bbox.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio;
+ const { width, height } = calculateNewSize(
+ state.bbox.aspectRatio.value,
+ state.bbox.rect.width * state.bbox.rect.height,
+ state.bbox.modelBase
+ );
+ state.bbox.rect.width = width;
+ state.bbox.rect.height = height;
+ }
}
syncScaledSize(state);
@@ -1744,6 +1755,29 @@ const slice = createSlice({
syncScaledSize(state);
}
});
+ // Sync bbox when external model resolution preset is selected (aspect_ratio_sizes)
+ builder.addCase(aspectRatioIdChanged, (state, action) => {
+ const { id, fixedSize } = action.payload;
+ // Only sync when fixedSize is provided (external models with aspect_ratio_sizes)
+ if (fixedSize) {
+ state.bbox.rect.width = fixedSize.width;
+ state.bbox.rect.height = fixedSize.height;
+ state.bbox.aspectRatio.value = fixedSize.width / fixedSize.height;
+ state.bbox.aspectRatio.id = id;
+ state.bbox.aspectRatio.isLocked = true;
+ syncScaledSize(state);
+ }
+ });
+ // Sync bbox when external model resolution preset is selected (resolution_presets)
+ builder.addCase(resolutionPresetSelected, (state, action) => {
+ const { width, height, aspectRatio } = action.payload;
+ state.bbox.rect.width = width;
+ state.bbox.rect.height = height;
+ state.bbox.aspectRatio.value = width / height;
+ state.bbox.aspectRatio.id = isAspectRatioID(aspectRatio) ? aspectRatio : 'Free';
+ state.bbox.aspectRatio.isLocked = true;
+ syncScaledSize(state);
+ });
},
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index 77ad6619db..2b09b4b8ed 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -636,19 +636,42 @@ export const zLoRA = z.object({
});
export type LoRA = z.infer;
-export const zAspectRatioID = z.enum(['Free', '21:9', '16:9', '3:2', '4:3', '1:1', '3:4', '2:3', '9:16', '9:21']);
+export const zAspectRatioID = z.enum([
+ 'Free',
+ '8:1',
+ '4:1',
+ '21:9',
+ '16:9',
+ '3:2',
+ '5:4',
+ '4:3',
+ '1:1',
+ '3:4',
+ '4:5',
+ '2:3',
+ '9:16',
+ '1:4',
+ '9:21',
+ '1:8',
+]);
export type AspectRatioID = z.infer;
export const isAspectRatioID = (v: unknown): v is AspectRatioID => zAspectRatioID.safeParse(v).success;
export const ASPECT_RATIO_MAP: Record, { ratio: number; inverseID: AspectRatioID }> = {
+ '8:1': { ratio: 8 / 1, inverseID: '1:8' },
+ '4:1': { ratio: 4 / 1, inverseID: '1:4' },
'21:9': { ratio: 21 / 9, inverseID: '9:21' },
'16:9': { ratio: 16 / 9, inverseID: '9:16' },
'3:2': { ratio: 3 / 2, inverseID: '2:3' },
+ '5:4': { ratio: 5 / 4, inverseID: '4:5' },
'4:3': { ratio: 4 / 3, inverseID: '4:3' },
'1:1': { ratio: 1, inverseID: '1:1' },
'3:4': { ratio: 3 / 4, inverseID: '4:3' },
+ '4:5': { ratio: 4 / 5, inverseID: '5:4' },
'2:3': { ratio: 2 / 3, inverseID: '3:2' },
'9:16': { ratio: 9 / 16, inverseID: '16:9' },
+ '1:4': { ratio: 1 / 4, inverseID: '4:1' },
'9:21': { ratio: 9 / 21, inverseID: '21:9' },
+ '1:8': { ratio: 1 / 8, inverseID: '8:1' },
};
const zAspectRatioConfig = z.object({
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts
index 0ba82234a6..2d7ee19897 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildExternalGraph.ts
@@ -6,7 +6,6 @@ import { type ModelIdentifierField, zImageField } from 'features/nodes/types/com
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import {
getOriginalAndScaledSizesForOtherModes,
- getOriginalAndScaledSizesForTextToImage,
selectCanvasOutputFields,
} from 'features/nodes/util/graph/graphBuilderUtils';
import {
@@ -110,16 +109,15 @@ export const buildExternalGraph = async (arg: GraphBuilderArg): Promise {
const id = useAppSelector(selectAspectRatioID);
const isStaging = useCanvasIsStaging();
const allowedAspectRatios = useAppSelector(selectAllowedAspectRatioIDs);
- const options = allowedAspectRatios ?? zAspectRatioID.options;
+ const aspectRatioSizes = useAppSelector(selectAspectRatioSizes);
+ const hasFixedSizes = useAppSelector(selectHasFixedDimensionSizes);
+ const options = useMemo(() => allowedAspectRatios ?? zAspectRatioID.options, [allowedAspectRatios]);
const onChange = useCallback>(
(e) => {
if (!isAspectRatioID(e.target.value)) {
return;
}
- dispatch(bboxAspectRatioIdChanged({ id: e.target.value }));
+ const fixedSize = aspectRatioSizes?.[e.target.value] ?? undefined;
+ dispatch(bboxAspectRatioIdChanged({ id: e.target.value, fixedSize }));
+ // For external models with fixed sizes, also sync to params so buildExternalGraph uses correct dimensions
+ if (fixedSize) {
+ dispatch(aspectRatioIdChanged({ id: e.target.value, fixedSize }));
+ }
},
- [dispatch]
+ [dispatch, aspectRatioSizes]
);
return (
-
+
{t('parameters.aspect')}
diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSwapDimensionsButton.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSwapDimensionsButton.tsx
index 54614419a5..372f8187ea 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSwapDimensionsButton.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSwapDimensionsButton.tsx
@@ -1,7 +1,8 @@
import { IconButton } from '@invoke-ai/ui-library';
-import { useAppDispatch } from 'app/store/storeHooks';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { bboxDimensionsSwapped } from 'features/controlLayers/store/canvasSlice';
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { selectHasFixedDimensionSizes } from 'features/controlLayers/store/paramsSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowsDownUpBold } from 'react-icons/pi';
@@ -10,6 +11,7 @@ export const BboxSwapDimensionsButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isStaging = useCanvasIsStaging();
+ const hasFixedSizes = useAppSelector(selectHasFixedDimensionSizes);
const onClick = useCallback(() => {
dispatch(bboxDimensionsSwapped());
}, [dispatch]);
@@ -21,7 +23,7 @@ export const BboxSwapDimensionsButton = memo(() => {
variant="ghost"
size="sm"
icon={}
- isDisabled={isStaging}
+ isDisabled={isStaging || hasFixedSizes}
/>
);
});
diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/use-is-bbox-size-locked.ts b/invokeai/frontend/web/src/features/parameters/components/Bbox/use-is-bbox-size-locked.ts
index eaf1381108..18f453708e 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Bbox/use-is-bbox-size-locked.ts
+++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/use-is-bbox-size-locked.ts
@@ -1,6 +1,9 @@
+import { useAppSelector } from 'app/store/storeHooks';
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { selectHasFixedDimensionSizes } from 'features/controlLayers/store/paramsSlice';
export const useIsBboxSizeLocked = () => {
const isStaging = useCanvasIsStaging();
- return isStaging;
+ const hasFixedSizes = useAppSelector(selectHasFixedDimensionSizes);
+ return isStaging || hasFixedSizes;
};