diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 3da94df7f47..57c2a601e7e 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -84,6 +84,17 @@ async def delete_image( # TODO: Does this need any exception handling at all? pass +@images_router.post("/clear-intermediates", operation_id="clear_intermediates") +async def clear_intermediates() -> int: + """Clears first 100 intermediates""" + + try: + count_deleted = ApiDependencies.invoker.services.images.delete_many(is_intermediate=True) + return count_deleted + except Exception as e: + # TODO: Does this need any exception handling at all? + pass + @images_router.patch( "/{image_name}", diff --git a/invokeai/app/services/image_record_storage.py b/invokeai/app/services/image_record_storage.py index 7b37307ce85..d28a0bc09da 100644 --- a/invokeai/app/services/image_record_storage.py +++ b/invokeai/app/services/image_record_storage.py @@ -97,8 +97,8 @@ def update( @abstractmethod def get_many( self, - offset: int = 0, - limit: int = 10, + offset: Optional[int] = None, + limit: Optional[int] = None, image_origin: Optional[ResourceOrigin] = None, categories: Optional[list[ImageCategory]] = None, is_intermediate: Optional[bool] = None, @@ -322,8 +322,8 @@ def update( def get_many( self, - offset: int = 0, - limit: int = 10, + offset: Optional[int] = None, + limit: Optional[int] = None, image_origin: Optional[ResourceOrigin] = None, categories: Optional[list[ImageCategory]] = None, is_intermediate: Optional[bool] = None, @@ -392,8 +392,12 @@ def get_many( images_query += query_conditions + query_pagination + ";" # Add all the parameters images_params = query_params.copy() - images_params.append(limit) - images_params.append(offset) + + if limit is not None: + images_params.append(limit) + if offset is not None: + images_params.append(offset) + # Build the list of images, deserializing each row self._cursor.execute(images_query, images_params) result = cast(list[sqlite3.Row], self._cursor.fetchall()) diff --git a/invokeai/app/services/images.py b/invokeai/app/services/images.py index a7d0b6ddeeb..5742a4cb4ba 100644 --- a/invokeai/app/services/images.py +++ b/invokeai/app/services/images.py @@ -109,6 +109,13 @@ def delete(self, image_name: str): """Deletes an image.""" pass + @abstractmethod + def delete_many(self, is_intermediate: bool) -> int: + """Deletes many images.""" + pass + + + @abstractmethod def delete_images_on_board(self, board_id: str): """Deletes all images on a board.""" @@ -397,3 +404,28 @@ def delete_images_on_board(self, board_id: str): except Exception as e: self._services.logger.error("Problem deleting image records and files") raise e + + def delete_many(self, is_intermediate: bool): + try: + # only clears 100 at a time + images = self._services.image_records.get_many(offset=0, limit=100, is_intermediate=is_intermediate,) + count = len(images.items) + image_name_list = list( + map( + lambda r: r.image_name, + images.items, + ) + ) + for image_name in image_name_list: + self._services.image_files.delete(image_name) + self._services.image_records.delete_many(image_name_list) + return count + except ImageRecordDeleteException: + self._services.logger.error(f"Failed to delete image records") + raise + except ImageFileDeleteException: + self._services.logger.error(f"Failed to delete image files") + raise + except Exception as e: + self._services.logger.error("Problem deleting image records and files") + raise e diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsClearIntermediates.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsClearIntermediates.tsx new file mode 100644 index 00000000000..86b5753cd44 --- /dev/null +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsClearIntermediates.tsx @@ -0,0 +1,57 @@ +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useCallback, useEffect, useState } from 'react'; +import { StyledFlex } from './SettingsModal'; +import { Heading, Text } from '@chakra-ui/react'; +import IAIButton from '../../../../common/components/IAIButton'; +import { useClearIntermediatesMutation } from '../../../../services/api/endpoints/images'; +import { addToast } from '../../store/systemSlice'; + +export default function SettingsClearIntermediates() { + const dispatch = useAppDispatch(); + const [isDisabled, setIsDisabled] = useState(false); + + const [clearIntermediates, { isLoading: isLoadingClearIntermediates }] = + useClearIntermediatesMutation(); + + const handleClickClearIntermediates = useCallback(() => { + clearIntermediates({}) + .unwrap() + .then((response) => { + dispatch( + addToast({ + title: + response === 0 + ? `No intermediates to clear` + : `Successfully cleared ${response} intermediates`, + status: 'info', + }) + ); + if (response < 100) { + setIsDisabled(true); + } + }); + }, [clearIntermediates, dispatch]); + + return ( + + Clear Intermediates + + {isDisabled ? 'Intermediates Cleared' : 'Clear 100 Intermediates'} + + + Will permanently delete first 100 intermediates found on disk and in + database + + + Intermediate images are byproducts of generation, different from the + result images in the gallery. Purging intermediates will free disk + space. Your gallery images will not be deleted. + + + ); +} diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx index 890ceb1f480..05056891844 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -48,6 +48,7 @@ import { import { useTranslation } from 'react-i18next'; import { LogLevelName } from 'roarr'; import SettingsSchedulers from './SettingsSchedulers'; +import SettingsClearIntermediates from './SettingsClearIntermediates'; const selector = createSelector( [systemSelector, uiSelector], @@ -91,6 +92,7 @@ type ConfigOptions = { shouldShowResetWebUiText: boolean; shouldShowBetaLayout: boolean; shouldShowAdvancedOptionsSettings: boolean; + shouldShowClearIntermediates: boolean; }; type SettingsModalProps = { @@ -109,6 +111,8 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => { const shouldShowResetWebUiText = config?.shouldShowResetWebUiText ?? true; const shouldShowAdvancedOptionsSettings = config?.shouldShowAdvancedOptionsSettings ?? true; + const shouldShowClearIntermediates = + config?.shouldShowClearIntermediates ?? true; useEffect(() => { if (!shouldShowDeveloperSettings) { @@ -280,6 +284,8 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => { )} + {shouldShowClearIntermediates && } + {t('settings.resetWebUI')} @@ -328,7 +334,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => { export default SettingsModal; -const StyledFlex = (props: PropsWithChildren) => { +export const StyledFlex = (props: PropsWithChildren) => { return ( ({ url: `images/clear-intermediates`, method: 'POST' }), + }), }), }); -export const { useGetImageDTOQuery, useGetImageMetadataQuery } = imagesApi; +export const { useGetImageDTOQuery, useGetImageMetadataQuery, useClearIntermediatesMutation } = imagesApi; diff --git a/invokeai/frontend/web/src/services/api/schema.d.ts b/invokeai/frontend/web/src/services/api/schema.d.ts index 805afe54b33..a91a81a7407 100644 --- a/invokeai/frontend/web/src/services/api/schema.d.ts +++ b/invokeai/frontend/web/src/services/api/schema.d.ts @@ -164,6 +164,13 @@ export type paths = { */ patch: operations["update_image"]; }; + "/api/v1/images/clear-intermediates": { + /** + * Clear Intermediates + * @description Clears first 100 intermediates + */ + post: operations["clear_intermediates"]; + }; "/api/v1/images/{image_name}/metadata": { /** * Get Image Metadata @@ -5299,17 +5306,17 @@ export type components = { image?: components["schemas"]["ImageField"]; }; /** - * StableDiffusion2ModelFormat + * StableDiffusionXLModelFormat * @description An enumeration. * @enum {string} */ - StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; + StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; /** - * StableDiffusionXLModelFormat + * StableDiffusion2ModelFormat * @description An enumeration. * @enum {string} */ - StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; + StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; /** * StableDiffusion1ModelFormat * @description An enumeration. @@ -6098,6 +6105,20 @@ export type operations = { }; }; }; + /** + * Clear Intermediates + * @description Clears first 100 intermediates + */ + clear_intermediates: { + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": unknown; + }; + }; + }; + }; /** * Get Image Metadata * @description Gets an image's metadata