diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 0cd77dc2e7..eac8922a79 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -24,6 +24,7 @@ import { import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { imagesApi } from 'services/api/endpoints/images'; +import { upscaleInitialImageChanged } from '../../../../../features/parameters/store/upscaleSlice'; export const dndDropped = createAction<{ overData: TypesafeDroppableData; @@ -243,6 +244,20 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => return; } + /** + * Image dropped on upscale initial image + */ + if ( + overData.actionType === 'SET_UPSCALE_INITIAL_IMAGE' && + activeData.payloadType === 'IMAGE_DTO' && + activeData.payload.imageDTO + ) { + const { imageDTO } = activeData.payload; + + dispatch(upscaleInitialImageChanged(imageDTO)); + return; + } + /** * Multiple images dropped on user board */ diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index bd1ee47825..5a4d5dc078 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -19,6 +19,7 @@ import { t } from 'i18next'; import { omit } from 'lodash-es'; import { boardsApi } from 'services/api/endpoints/boards'; import { imagesApi } from 'services/api/endpoints/images'; +import { upscaleInitialImageChanged } from '../../../../../features/parameters/store/upscaleSlice'; export const addImageUploadedFulfilledListener = (startAppListening: AppStartListening) => { startAppListening({ @@ -89,6 +90,15 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis return; } + if (postUploadAction?.type === 'SET_UPSCALE_INITIAL_IMAGE') { + dispatch(upscaleInitialImageChanged(imageDTO)); + toast({ + ...DEFAULT_UPLOADED_TOAST, + description: "set as upscale initial image", + }); + return; + } + if (postUploadAction?.type === 'SET_CONTROL_ADAPTER_IMAGE') { const { id } = postUploadAction; dispatch( diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index eb86f54c84..eef1f8f49f 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -17,7 +17,8 @@ import { forEach } from 'lodash-es'; import type { Logger } from 'roarr'; import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models'; import type { AnyModelConfig } from 'services/api/types'; -import { isNonRefinerMainModelConfig, isRefinerMainModelModelConfig, isVAEModelConfig } from 'services/api/types'; +import { isNonRefinerMainModelConfig, isRefinerMainModelModelConfig, isSpandrelImageToImageModelConfig, isVAEModelConfig } from 'services/api/types'; +import { upscaleModelChanged } from '../../../../../features/parameters/store/upscaleSlice'; export const addModelsLoadedListener = (startAppListening: AppStartListening) => { startAppListening({ @@ -36,6 +37,7 @@ export const addModelsLoadedListener = (startAppListening: AppStartListening) => handleVAEModels(models, state, dispatch, log); handleLoRAModels(models, state, dispatch, log); handleControlAdapterModels(models, state, dispatch, log); + handleSpandrelImageToImageModels(models, state, dispatch, log); }, }); }; @@ -177,3 +179,24 @@ const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log) dispatch(controlAdapterModelCleared({ id: ca.id })); }); }; + +const handleSpandrelImageToImageModels: ModelHandler = (models, state, dispatch, _log) => { + const currentUpscaleModel = state.upscale.upscaleModel; + const upscaleModels = models.filter(isSpandrelImageToImageModelConfig); + + if (currentUpscaleModel) { + const isCurrentUpscaleModelAvailable = upscaleModels.some((m) => m.key === currentUpscaleModel.key); + if (isCurrentUpscaleModelAvailable) { + return; + } + } + + const firstModel = upscaleModels[0]; + if (firstModel) { + dispatch(upscaleModelChanged(firstModel)) + return + } + + dispatch(upscaleModelChanged(null)) + +}; diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 062cdc1cbf..804d3629ab 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -46,6 +46,7 @@ import { actionSanitizer } from './middleware/devtools/actionSanitizer'; import { actionsDenylist } from './middleware/devtools/actionsDenylist'; import { stateSanitizer } from './middleware/devtools/stateSanitizer'; import { listenerMiddleware } from './middleware/listenerMiddleware'; +import { upscalePersistConfig, upscaleSlice } from '../../features/parameters/store/upscaleSlice'; const allReducers = { [canvasSlice.name]: canvasSlice.reducer, @@ -69,6 +70,7 @@ const allReducers = { [controlLayersSlice.name]: undoable(controlLayersSlice.reducer, controlLayersUndoableConfig), [workflowSettingsSlice.name]: workflowSettingsSlice.reducer, [api.reducerPath]: api.reducer, + [upscaleSlice.name]: upscaleSlice.reducer }; const rootReducer = combineReducers(allReducers); @@ -114,6 +116,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [hrfPersistConfig.name]: hrfPersistConfig, [controlLayersPersistConfig.name]: controlLayersPersistConfig, [workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig, + [upscalePersistConfig.name]: upscalePersistConfig }; const unserialize: UnserializeFunction = (data, key) => { diff --git a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts index 5b1bf1f5b3..d8e7d70a8c 100644 --- a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts +++ b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts @@ -21,6 +21,10 @@ const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (ac postUploadAction = { type: 'SET_CANVAS_INITIAL_IMAGE' }; } + if (activeTabName === 'upscaling') { + postUploadAction = { type: 'SET_UPSCALE_INITIAL_IMAGE' }; + } + return postUploadAction; }); diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index 6fcf18421e..1e72123b75 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -62,6 +62,10 @@ export type CanvasInitialImageDropData = BaseDropData & { actionType: 'SET_CANVAS_INITIAL_IMAGE'; }; +export type UpscaleInitialImageDropData = BaseDropData & { + actionType: 'SET_UPSCALE_INITIAL_IMAGE'; +}; + type NodesImageDropData = BaseDropData & { actionType: 'SET_NODES_IMAGE'; context: { @@ -87,6 +91,8 @@ export type SelectForCompareDropData = BaseDropData & { }; }; + + export type TypesafeDroppableData = | CurrentImageDropData | ControlAdapterDropData @@ -98,7 +104,8 @@ export type TypesafeDroppableData = | IPALayerImageDropData | RGLayerIPAdapterImageDropData | IILayerImageDropData - | SelectForCompareDropData; + | SelectForCompareDropData + | UpscaleInitialImageDropData; type BaseDragData = { id: string; @@ -159,11 +166,11 @@ interface DragEvent { over: TypesafeOver | null; } -export interface DragStartEvent extends Pick {} -interface DragMoveEvent extends DragEvent {} -interface DragOverEvent extends DragMoveEvent {} -export interface DragEndEvent extends DragEvent {} -interface DragCancelEvent extends DragEndEvent {} +export interface DragStartEvent extends Pick { } +interface DragMoveEvent extends DragEvent { } +interface DragOverEvent extends DragMoveEvent { } +export interface DragEndEvent extends DragEvent { } +interface DragCancelEvent extends DragEndEvent { } export interface DndContextTypesafeProps extends Omit { diff --git a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts index 6dec862345..3f8fe5ab73 100644 --- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts +++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts @@ -27,6 +27,8 @@ export const isValidDrop = (overData?: TypesafeDroppableData | null, activeData? return payloadType === 'IMAGE_DTO'; case 'SET_CANVAS_INITIAL_IMAGE': return payloadType === 'IMAGE_DTO'; + case 'SET_UPSCALE_INITIAL_IMAGE': + return payloadType === 'IMAGE_DTO'; case 'SET_NODES_IMAGE': return payloadType === 'IMAGE_DTO'; case 'SELECT_FOR_COMPARE': diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx index d500d692fe..da0253cc36 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx @@ -11,12 +11,8 @@ import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMe import { useImageActions } from 'features/gallery/hooks/useImageActions'; import { sentImageToImg2Img } from 'features/gallery/store/actions'; import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; -import { selectGallerySlice } from 'features/gallery/store/gallerySlice'; import { parseAndRecallImageDimensions } from 'features/metadata/util/handlers'; import { $templates } from 'features/nodes/store/nodesSlice'; -import ParamUpscalePopover from 'features/parameters/components/Upscale/ParamUpscaleSettings'; -import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress'; -import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { selectSystemSlice } from 'features/system/store/systemSlice'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow'; @@ -37,9 +33,8 @@ import { useGetImageDTOQuery } from 'services/api/endpoints/images'; const selectShouldDisableToolbarButtons = createSelector( selectSystemSlice, - selectGallerySlice, selectLastSelectedImage, - (system, gallery, lastSelectedImage) => { + (system, lastSelectedImage) => { const hasProgressImage = Boolean(system.denoiseProgress?.progress_image); return hasProgressImage || !lastSelectedImage; } @@ -47,13 +42,10 @@ const selectShouldDisableToolbarButtons = createSelector( const CurrentImageButtons = () => { const dispatch = useAppDispatch(); - const isConnected = useAppSelector((s) => s.system.isConnected); const lastSelectedImage = useAppSelector(selectLastSelectedImage); const selection = useAppSelector((s) => s.gallery.selection); const shouldDisableToolbarButtons = useAppSelector(selectShouldDisableToolbarButtons); const templates = useStore($templates); - const isUpscalingEnabled = useFeatureStatus('upscaling'); - const isQueueMutationInProgress = useIsQueueMutationInProgress(); const { t } = useTranslation(); const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken); @@ -107,17 +99,6 @@ const CurrentImageButtons = () => { dispatch(imagesToDeleteSelected(selection)); }, [dispatch, imageDTO, selection]); - useHotkeys( - 'Shift+U', - () => { - handleClickUpscale(); - }, - { - enabled: () => Boolean(isUpscalingEnabled && !shouldDisableToolbarButtons && isConnected), - }, - [isUpscalingEnabled, imageDTO, shouldDisableToolbarButtons, isConnected] - ); - useHotkeys( 'delete', () => { @@ -191,12 +172,6 @@ const CurrentImageButtons = () => { /> - {isUpscalingEnabled && ( - - {isUpscalingEnabled && } - - )} - diff --git a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamRealESRGANModel.tsx b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamRealESRGANModel.tsx index 6c1a3ab3a7..d02bfd2b03 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamRealESRGANModel.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamRealESRGANModel.tsx @@ -63,7 +63,7 @@ const ParamESRGANModel = () => { return ( - {t('models.esrganModel')} + {t('models.esrganModel')} ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamSpandrelModel.tsx b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamSpandrelModel.tsx new file mode 100644 index 0000000000..b16a77feae --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamSpandrelModel.tsx @@ -0,0 +1,47 @@ +import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSpandrelImageToImageModels } from '../../../../services/api/hooks/modelsByType'; +import { useModelCombobox } from '../../../../common/hooks/useModelCombobox'; +import { SpandrelImageToImageModelConfig } from '../../../../services/api/types'; +import { upscaleModelChanged } from '../../store/upscaleSlice'; + +const ParamSpandrelModel = () => { + const { t } = useTranslation(); + const [modelConfigs, { isLoading }] = useSpandrelImageToImageModels(); + + const model = useAppSelector((s) => s.upscale.upscaleModel); + + const dispatch = useAppDispatch(); + + const _onChange = useCallback( + (v: SpandrelImageToImageModelConfig | null) => { + dispatch(upscaleModelChanged(v)); + }, + [dispatch] + ); + + const { options, value, onChange, placeholder, noOptionsMessage } = useModelCombobox({ + modelConfigs, + onChange: _onChange, + selectedModel: model, + isLoading, + }); + + return ( + + Upscale Model + + + ); +}; + +export default memo(ParamSpandrelModel); diff --git a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleSettings.tsx index c0309bebe4..15fec1d214 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleSettings.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleSettings.tsx @@ -17,7 +17,8 @@ import { useTranslation } from 'react-i18next'; import { PiFrameCornersBold } from 'react-icons/pi'; import type { ImageDTO } from 'services/api/types'; -import ParamESRGANModel from './ParamRealESRGANModel'; +import ParamSpandrelModel from './ParamSpandrelModel'; +import { useSpandrelImageToImageModels } from '../../../../services/api/hooks/modelsByType'; type Props = { imageDTO?: ImageDTO }; @@ -28,6 +29,7 @@ const ParamUpscalePopover = (props: Props) => { const { t } = useTranslation(); const { isOpen, onOpen, onClose } = useDisclosure(); const { isAllowedToUpscale, detail } = useIsAllowedToUpscale(imageDTO); + const [modelConfigs] = useSpandrelImageToImageModels(); const handleClickUpscale = useCallback(() => { onClose(); @@ -45,16 +47,17 @@ const ParamUpscalePopover = (props: Props) => { onClick={onOpen} icon={} aria-label={t('parameters.upscale')} + isDisabled={!modelConfigs.length} /> - +