diff --git a/invokeai/app/services/image_record_storage.py b/invokeai/app/services/image_record_storage.py index 5e5996ae76..014006eb7a 100644 --- a/invokeai/app/services/image_record_storage.py +++ b/invokeai/app/services/image_record_storage.py @@ -1,22 +1,16 @@ +import sqlite3 +import threading from abc import ABC, abstractmethod from datetime import datetime from typing import Generic, Optional, TypeVar, cast -import sqlite3 -import threading from pydantic import BaseModel, Field from pydantic.generics import GenericModel +from invokeai.app.models.image import ImageCategory, ResourceOrigin from invokeai.app.models.metadata import ImageMetadata -from invokeai.app.models.image import ( - ImageCategory, - ResourceOrigin, -) from invokeai.app.services.models.image_record import ( - ImageRecord, - ImageRecordChanges, - deserialize_image_record, -) + ImageRecord, ImageRecordChanges, deserialize_image_record) T = TypeVar("T", bound=BaseModel) @@ -162,7 +156,6 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): node_id TEXT, metadata TEXT, is_intermediate BOOLEAN DEFAULT FALSE, - board_id TEXT, created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), -- Updated via trigger updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index b403fde2c6..6734f1dcd1 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -528,7 +528,8 @@ "hidePreview": "Hide Preview", "showPreview": "Show Preview", "controlNetControlMode": "Control Mode", - "clipSkip": "Clip Skip" + "clipSkip": "Clip Skip", + "aspectRatio": "Ratio" }, "settings": { "models": "Models", @@ -671,6 +672,7 @@ }, "ui": { "showProgressImages": "Show Progress Images", - "hideProgressImages": "Hide Progress Images" + "hideProgressImages": "Hide Progress Images", + "swapSizes": "Swap Sizes" } } diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryImage.tsx index a8d4c84adc..468db558b3 100644 --- a/invokeai/frontend/web/src/features/gallery/components/GalleryImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/GalleryImage.tsx @@ -53,13 +53,15 @@ const GalleryImage = (props: HoverableImageProps) => { const handleClick = useCallback( (e: MouseEvent) => { - if (e.shiftKey) { - dispatch(imageRangeEndSelected(props.imageDTO.image_name)); - } else if (e.ctrlKey || e.metaKey) { - dispatch(imageSelectionToggled(props.imageDTO.image_name)); - } else { - dispatch(imageSelected(props.imageDTO.image_name)); - } + // multiselect disabled for now + // if (e.shiftKey) { + // dispatch(imageRangeEndSelected(props.imageDTO.image_name)); + // } else if (e.ctrlKey || e.metaKey) { + // dispatch(imageSelectionToggled(props.imageDTO.image_name)); + // } else { + // dispatch(imageSelected(props.imageDTO.image_name)); + // } + dispatch(imageSelected(props.imageDTO.image_name)); }, [dispatch, props.imageDTO.image_name] ); @@ -121,6 +123,7 @@ const GalleryImage = (props: HoverableImageProps) => { // withResetIcon // removed bc it's too easy to accidentally delete images isDropDisabled={true} isUploadDisabled={true} + thumbnail={true} /> )} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu.tsx index 1e5f95ab0d..64b1d349d8 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu.tsx @@ -1,49 +1,32 @@ -import { MenuItem, MenuList } from '@chakra-ui/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { memo, useCallback, useContext } from 'react'; -import { - FaExpand, - FaFolder, - FaFolderPlus, - FaShare, - FaTrash, -} from 'react-icons/fa'; -import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu'; -import { - resizeAndScaleCanvas, - setInitialCanvasImage, -} from 'features/canvas/store/canvasSlice'; -import { setActiveTab } from 'features/ui/store/uiSlice'; -import { useTranslation } from 'react-i18next'; import { ExternalLinkIcon } from '@chakra-ui/icons'; -import { IoArrowUndoCircleOutline } from 'react-icons/io5'; +import { MenuItem, MenuList } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; -import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; -import { initialImageSelected } from 'features/parameters/store/actions'; -import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions'; import { useAppToaster } from 'app/components/Toaster'; -import { AddImageToBoardContext } from '../../../app/contexts/AddImageToBoardContext'; -import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages'; -import { ImageDTO } from 'services/api/types'; -import { RootState, stateSelector } from 'app/store/store'; +import { stateSelector } from 'app/store/store'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu'; import { imagesAddedToBatch, selectionAddedToBatch, } from 'features/batch/store/batchSlice'; -import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { + resizeAndScaleCanvas, + setInitialCanvasImage, +} from 'features/canvas/store/canvasSlice'; import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletionSlice'; - -const selector = createSelector( - [stateSelector, (state: RootState, imageDTO: ImageDTO) => imageDTO], - ({ gallery, batch }, imageDTO) => { - const selectionCount = gallery.selection.length; - const isInBatch = batch.imageNames.includes(imageDTO.image_name); - - return { selectionCount, isInBatch }; - }, - defaultSelectorOptions -); +import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; +import { initialImageSelected } from 'features/parameters/store/actions'; +import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; +import { setActiveTab } from 'features/ui/store/uiSlice'; +import { memo, useCallback, useContext, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FaExpand, FaFolder, FaShare, FaTrash } from 'react-icons/fa'; +import { IoArrowUndoCircleOutline } from 'react-icons/io5'; +import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages'; +import { ImageDTO } from 'services/api/types'; +import { AddImageToBoardContext } from '../../../app/contexts/AddImageToBoardContext'; +import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions'; type Props = { image: ImageDTO; @@ -51,9 +34,21 @@ type Props = { }; const ImageContextMenu = ({ image, children }: Props) => { - const { selectionCount, isInBatch } = useAppSelector((state) => - selector(state, image) + const selector = useMemo( + () => + createSelector( + [stateSelector], + ({ gallery, batch }) => { + const selectionCount = gallery.selection.length; + const isInBatch = batch.imageNames.includes(image.image_name); + + return { selectionCount, isInBatch }; + }, + defaultSelectorOptions + ), + [image.image_name] ); + const { selectionCount, isInBatch } = useAppSelector(selector); const dispatch = useAppDispatch(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx index 4177db9a1b..3fcdd54cc9 100644 --- a/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx @@ -8,7 +8,7 @@ import { selectImagesById, } from 'features/gallery/store/gallerySlice'; import { clamp, isEqual } from 'lodash-es'; -import { useCallback, useState } from 'react'; +import { memo, useCallback, useState } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { FaAngleDoubleRight, FaAngleLeft, FaAngleRight } from 'react-icons/fa'; @@ -227,4 +227,4 @@ const NextPrevImageButtons = () => { ); }; -export default NextPrevImageButtons; +export default memo(NextPrevImageButtons); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamAspectRatio.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamAspectRatio.tsx new file mode 100644 index 0000000000..0a568fb84d --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamAspectRatio.tsx @@ -0,0 +1,37 @@ +import { ButtonGroup, Flex } from '@chakra-ui/react'; +import { RootState } from 'app/store/store'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAIButton from 'common/components/IAIButton'; +import { setAspectRatio } from 'features/ui/store/uiSlice'; + +const aspectRatios = [ + { name: 'Free', value: null }, + { name: 'Portrait', value: 0.67 / 1 }, + { name: 'Wide', value: 16 / 9 }, + { name: 'Square', value: 1 / 1 }, +]; + +export default function ParamAspectRatio() { + const aspectRatio = useAppSelector( + (state: RootState) => state.ui.aspectRatio + ); + + const dispatch = useAppDispatch(); + + return ( + + + {aspectRatios.map((ratio) => ( + dispatch(setAspectRatio(ratio.value))} + > + {ratio.name} + + ))} + + + ); +} diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamHeight.tsx index 6939ede424..63abe0ddf9 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamHeight.tsx @@ -2,19 +2,22 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAISlider, { IAIFullSliderProps } from 'common/components/IAISlider'; +import { roundToMultiple } from 'common/util/roundDownToMultiple'; import { generationSelector } from 'features/parameters/store/generationSelectors'; -import { setHeight } from 'features/parameters/store/generationSlice'; +import { setHeight, setWidth } from 'features/parameters/store/generationSlice'; import { configSelector } from 'features/system/store/configSelectors'; import { hotkeysSelector } from 'features/ui/store/hotkeysSlice'; +import { uiSelector } from 'features/ui/store/uiSelectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const selector = createSelector( - [generationSelector, hotkeysSelector, configSelector], - (generation, hotkeys, config) => { + [generationSelector, hotkeysSelector, configSelector, uiSelector], + (generation, hotkeys, config, ui) => { const { initial, min, sliderMax, inputMax, fineStep, coarseStep } = config.sd.height; const { height } = generation; + const { aspectRatio } = ui; const step = hotkeys.shift ? fineStep : coarseStep; @@ -25,6 +28,7 @@ const selector = createSelector( sliderMax, inputMax, step, + aspectRatio, }; }, defaultSelectorOptions @@ -36,7 +40,7 @@ type ParamHeightProps = Omit< >; const ParamHeight = (props: ParamHeightProps) => { - const { height, initial, min, sliderMax, inputMax, step } = + const { height, initial, min, sliderMax, inputMax, step, aspectRatio } = useAppSelector(selector); const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -44,13 +48,21 @@ const ParamHeight = (props: ParamHeightProps) => { const handleChange = useCallback( (v: number) => { dispatch(setHeight(v)); + if (aspectRatio) { + const newWidth = roundToMultiple(v * aspectRatio, 8); + dispatch(setWidth(newWidth)); + } }, - [dispatch] + [dispatch, aspectRatio] ); const handleReset = useCallback(() => { dispatch(setHeight(initial)); - }, [dispatch, initial]); + if (aspectRatio) { + const newWidth = roundToMultiple(initial * aspectRatio, 8); + dispatch(setWidth(newWidth)); + } + }, [dispatch, initial, aspectRatio]); return ( state.generation.shouldFitToWidthHeight + ); + return ( + + + + {t('parameters.aspectRatio')} + + + + } + fontSize={20} + onClick={() => dispatch(toggleSize())} + /> + + + + + + + + + ); +} diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamWidth.tsx index b4121184b5..991db19097 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamWidth.tsx @@ -2,19 +2,22 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAISlider, { IAIFullSliderProps } from 'common/components/IAISlider'; +import { roundToMultiple } from 'common/util/roundDownToMultiple'; import { generationSelector } from 'features/parameters/store/generationSelectors'; -import { setWidth } from 'features/parameters/store/generationSlice'; +import { setHeight, setWidth } from 'features/parameters/store/generationSlice'; import { configSelector } from 'features/system/store/configSelectors'; import { hotkeysSelector } from 'features/ui/store/hotkeysSlice'; +import { uiSelector } from 'features/ui/store/uiSelectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const selector = createSelector( - [generationSelector, hotkeysSelector, configSelector], - (generation, hotkeys, config) => { + [generationSelector, hotkeysSelector, configSelector, uiSelector], + (generation, hotkeys, config, ui) => { const { initial, min, sliderMax, inputMax, fineStep, coarseStep } = config.sd.width; const { width } = generation; + const { aspectRatio } = ui; const step = hotkeys.shift ? fineStep : coarseStep; @@ -25,6 +28,7 @@ const selector = createSelector( sliderMax, inputMax, step, + aspectRatio, }; }, defaultSelectorOptions @@ -33,7 +37,7 @@ const selector = createSelector( type ParamWidthProps = Omit; const ParamWidth = (props: ParamWidthProps) => { - const { width, initial, min, sliderMax, inputMax, step } = + const { width, initial, min, sliderMax, inputMax, step, aspectRatio } = useAppSelector(selector); const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -41,13 +45,21 @@ const ParamWidth = (props: ParamWidthProps) => { const handleChange = useCallback( (v: number) => { dispatch(setWidth(v)); + if (aspectRatio) { + const newHeight = roundToMultiple(v / aspectRatio, 8); + dispatch(setHeight(newHeight)); + } }, - [dispatch] + [dispatch, aspectRatio] ); const handleReset = useCallback(() => { dispatch(setWidth(initial)); - }, [dispatch, initial]); + if (aspectRatio) { + const newHeight = roundToMultiple(initial / aspectRatio, 8); + dispatch(setHeight(newHeight)); + } + }, [dispatch, initial, aspectRatio]); return ( } - isDisabled={!isReady} + isDisabled={!isReady || isProcessing} onClick={handleInvoke} tooltip={t('parameters.invoke')} tooltipProps={{ placement: 'top' }} @@ -95,7 +95,7 @@ export default function InvokeButton(props: InvokeButton) { ) => { state.width = action.payload; }, + toggleSize: (state) => { + const [width, height] = [state.width, state.height]; + state.width = height; + state.height = width; + }, setScheduler: (state, action: PayloadAction) => { state.scheduler = action.payload; }, @@ -262,6 +271,12 @@ export const generationSlice = createSlice({ const advancedOptionsStatus = action.payload; if (!advancedOptionsStatus) state.clipSkip = 0; }); + builder.addCase(setAspectRatio, (state, action) => { + const ratio = action.payload; + if (ratio) { + state.height = roundToMultiple(state.width / ratio, 8); + } + }); }, }); @@ -271,7 +286,9 @@ export const { resetParametersState, resetSeed, setCfgScale, + setWidth, setHeight, + toggleSize, setImg2imgStrength, setInfillMethod, setIterations, @@ -292,7 +309,6 @@ export const { setThreshold, setTileSize, setVariationAmount, - setWidth, setShouldUseSymmetry, setHorizontalSymmetrySteps, setVerticalSymmetrySteps, diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabCoreParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabCoreParameters.tsx index b333a0caf2..cda908da50 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabCoreParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabCoreParameters.tsx @@ -4,11 +4,10 @@ import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAICollapse from 'common/components/IAICollapse'; import ParamCFGScale from 'features/parameters/components/Parameters/Core/ParamCFGScale'; -import ParamHeight from 'features/parameters/components/Parameters/Core/ParamHeight'; import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations'; import ParamModelandVAEandScheduler from 'features/parameters/components/Parameters/Core/ParamModelandVAEandScheduler'; +import ParamSize from 'features/parameters/components/Parameters/Core/ParamSize'; import ParamSteps from 'features/parameters/components/Parameters/Core/ParamSteps'; -import ParamWidth from 'features/parameters/components/Parameters/Core/ParamWidth'; import ImageToImageFit from 'features/parameters/components/Parameters/ImageToImage/ImageToImageFit'; import ImageToImageStrength from 'features/parameters/components/Parameters/ImageToImage/ImageToImageStrength'; import ParamSeedFull from 'features/parameters/components/Parameters/Seed/ParamSeedFull'; @@ -47,15 +46,14 @@ const ImageToImageTabCoreParameters = () => { > {shouldUseSliders ? ( <> + + + - - - - - + ) : ( <> @@ -68,8 +66,7 @@ const ImageToImageTabCoreParameters = () => { - - + )} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabCoreParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabCoreParameters.tsx index b007497db2..de8f64e661 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabCoreParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabCoreParameters.tsx @@ -5,11 +5,10 @@ import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAICollapse from 'common/components/IAICollapse'; import ParamCFGScale from 'features/parameters/components/Parameters/Core/ParamCFGScale'; -import ParamHeight from 'features/parameters/components/Parameters/Core/ParamHeight'; import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations'; import ParamModelandVAEandScheduler from 'features/parameters/components/Parameters/Core/ParamModelandVAEandScheduler'; +import ParamSize from 'features/parameters/components/Parameters/Core/ParamSize'; import ParamSteps from 'features/parameters/components/Parameters/Core/ParamSteps'; -import ParamWidth from 'features/parameters/components/Parameters/Core/ParamWidth'; import ParamSeedFull from 'features/parameters/components/Parameters/Seed/ParamSeedFull'; import { memo } from 'react'; @@ -43,15 +42,14 @@ const TextToImageTabCoreParameters = () => { > {shouldUseSliders ? ( <> + + + - - - - - + ) : ( <> @@ -64,8 +62,7 @@ const TextToImageTabCoreParameters = () => { - - + )} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx index ecce61c218..6ea9d4bc8d 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx @@ -44,13 +44,13 @@ const UnifiedCanvasCoreParameters = () => { > {shouldUseSliders ? ( <> + + + - - - diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index 04fee42126..4f38f84fe2 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -21,6 +21,7 @@ export const initialUIState: UIState = { shouldShowProgressInViewer: true, shouldShowEmbeddingPicker: false, shouldShowAdvancedOptions: false, + aspectRatio: null, favoriteSchedulers: [], }; @@ -104,6 +105,9 @@ export const uiSlice = createSlice({ setShouldShowAdvancedOptions: (state, action: PayloadAction) => { state.shouldShowAdvancedOptions = action.payload; }, + setAspectRatio: (state, action: PayloadAction) => { + state.aspectRatio = action.payload; + }, }, extraReducers(builder) { builder.addCase(initialImageChanged, (state) => { @@ -132,6 +136,7 @@ export const { favoriteSchedulersChanged, toggleEmbeddingPicker, setShouldShowAdvancedOptions, + setAspectRatio, } = uiSlice.actions; export default uiSlice.reducer; diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index 2356446030..e574f0ab79 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -29,5 +29,6 @@ export interface UIState { shouldShowProgressInViewer: boolean; shouldShowEmbeddingPicker: boolean; shouldShowAdvancedOptions: boolean; + aspectRatio: number | null; favoriteSchedulers: SchedulerParam[]; }