From fc752a4e75810abc781a4b6d89e0392f57a9d887 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sat, 4 Feb 2023 16:14:29 -0500 Subject: [PATCH 1/9] move old .venv directory away during install - To ensure a clean environment, the installer will now detect whether a previous .venv exists in the install location, and move it to .venv-backup before creating a fresh .venv. - Any previous .venv-backup is deleted. - User is informed of process. --- installer/installer.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/installer/installer.py b/installer/installer.py index dfdc21730e..066b5bbf87 100644 --- a/installer/installer.py +++ b/installer/installer.py @@ -130,10 +130,24 @@ class Installer: else: venv_dir = self.dest / ".venv" + # if there is an existing venv directory, then we move it out of the way + # to create a clean install + if venv_dir.exists(): + backup_venv = self.dest / ".venv-backup" + print(f"Previous .venv directory detected. Renaming to {backup_venv}",end='') + if backup_venv.exists(): + print(f' and removing existing {backup_venv} directory',end='') + shutil.rmtree(backup_venv) + venv_dir.replace(backup_venv) + print('') + # Prefer to copy python executables # so that updates to system python don't break InvokeAI try: venv.create(venv_dir, with_pip=True) + # Because we moved away the previous .venv, the following code + # isn't strictly necessary, but keeping it here in case we decide + # it better to upgrade an existing venv rather than replace. # If installing over an existing environment previously created with symlinks, # the executables will fail to copy. Keep symlinks in that case except shutil.SameFileError: From eff7fb89d869f468214cb2214a8a6d363ea6504e Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sun, 5 Feb 2023 13:39:46 -0500 Subject: [PATCH 2/9] installer will --force-reinstall torch --- installer/installer.py | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/installer/installer.py b/installer/installer.py index 066b5bbf87..6d33fe943d 100644 --- a/installer/installer.py +++ b/installer/installer.py @@ -130,24 +130,10 @@ class Installer: else: venv_dir = self.dest / ".venv" - # if there is an existing venv directory, then we move it out of the way - # to create a clean install - if venv_dir.exists(): - backup_venv = self.dest / ".venv-backup" - print(f"Previous .venv directory detected. Renaming to {backup_venv}",end='') - if backup_venv.exists(): - print(f' and removing existing {backup_venv} directory',end='') - shutil.rmtree(backup_venv) - venv_dir.replace(backup_venv) - print('') - # Prefer to copy python executables # so that updates to system python don't break InvokeAI try: venv.create(venv_dir, with_pip=True) - # Because we moved away the previous .venv, the following code - # isn't strictly necessary, but keeping it here in case we decide - # it better to upgrade an existing venv rather than replace. # If installing over an existing environment previously created with symlinks, # the executables will fail to copy. Keep symlinks in that case except shutil.SameFileError: @@ -263,6 +249,7 @@ class InvokeAiInstance: "--require-virtualenv", "torch", "torchvision", + "--force-reinstall", "--find-links" if find_links is not None else None, find_links, "--extra-index-url" if extra_index_url is not None else None, @@ -339,6 +326,7 @@ class InvokeAiInstance: Configure the InvokeAI runtime directory """ + # set sys.argv to a consistent state new_argv = [sys.argv[0]] for i in range(1,len(sys.argv)): el = sys.argv[i] @@ -353,15 +341,13 @@ class InvokeAiInstance: introduction() - from ldm.invoke.config import configure_invokeai + from ldm.invoke.config import invokeai_configure # NOTE: currently the config script does its own arg parsing! this means the command-line switches # from the installer will also automatically propagate down to the config script. # this may change in the future with config refactoring! - # set sys.argv to a consistent state - - configure_invokeai.main() + invokeai_configure.main() def install_user_scripts(self): """ From 2432adb38f5c2bf55b31a4aa9aef5368d11a5d1e Mon Sep 17 00:00:00 2001 From: Jonathan <34005131+JPPhoto@users.noreply.github.com> Date: Mon, 6 Feb 2023 09:33:24 -0600 Subject: [PATCH 3/9] In exception handlers, clear the torch CUDA cache (if we're using CUDA) to free up memory for other programs using the GPU and to reduce fragmentation. (#2549) --- invokeai/backend/invoke_ai_web_server.py | 12 ++++++++++++ ldm/generate.py | 10 +++++++++- ldm/invoke/generator/txt2img2img.py | 4 ++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/invokeai/backend/invoke_ai_web_server.py b/invokeai/backend/invoke_ai_web_server.py index 9dd18ebe65..c08dee596a 100644 --- a/invokeai/backend/invoke_ai_web_server.py +++ b/invokeai/backend/invoke_ai_web_server.py @@ -1208,12 +1208,18 @@ class InvokeAIWebServer: ) except KeyboardInterrupt: + # Clear the CUDA cache on an exception + self.empty_cuda_cache() self.socketio.emit("processingCanceled") raise except CanceledException: + # Clear the CUDA cache on an exception + self.empty_cuda_cache() self.socketio.emit("processingCanceled") pass except Exception as e: + # Clear the CUDA cache on an exception + self.empty_cuda_cache() print(e) self.socketio.emit("error", {"message": (str(e))}) print("\n") @@ -1221,6 +1227,12 @@ class InvokeAIWebServer: traceback.print_exc() print("\n") + def empty_cuda_cache(self): + if self.generate.device.type == "cuda": + import torch.cuda + + torch.cuda.empty_cache() + def parameters_to_generated_image_metadata(self, parameters): try: # top-level metadata minus `image` or `images` diff --git a/ldm/generate.py b/ldm/generate.py index 002ba47a97..9101ac3f01 100644 --- a/ldm/generate.py +++ b/ldm/generate.py @@ -211,7 +211,7 @@ class Generate: print('>> xformers memory-efficient attention is available but disabled') else: print('>> xformers not installed') - + # model caching system for fast switching self.model_manager = ModelManager(mconfig,self.device,self.precision,max_loaded_models=max_loaded_models) # don't accept invalid models @@ -565,11 +565,19 @@ class Generate: image_callback = image_callback) except KeyboardInterrupt: + # Clear the CUDA cache on an exception + if self._has_cuda(): + torch.cuda.empty_cache() + if catch_interrupts: print('**Interrupted** Partial results will be returned.') else: raise KeyboardInterrupt except RuntimeError: + # Clear the CUDA cache on an exception + if self._has_cuda(): + torch.cuda.empty_cache() + print(traceback.format_exc(), file=sys.stderr) print('>> Could not generate image.') diff --git a/ldm/invoke/generator/txt2img2img.py b/ldm/invoke/generator/txt2img2img.py index 1c398fb95d..0e9493aa44 100644 --- a/ldm/invoke/generator/txt2img2img.py +++ b/ldm/invoke/generator/txt2img2img.py @@ -65,6 +65,10 @@ class Txt2Img2Img(Generator): mode="bilinear" ) + # Free up memory from the last generation. + if self.model.device.type == 'cuda': + torch.cuda.empty_cache() + second_pass_noise = self.get_noise_like(resized_latents) verbosity = get_verbosity() From 25d7d71dd88af2a595a04bab3c92237fbd8ad13f Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Mon, 6 Feb 2023 19:58:46 +1300 Subject: [PATCH 4/9] Slightly decrease the size of the tab list icons --- .../frontend/src/features/tabs/components/InvokeTabs.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/src/features/tabs/components/InvokeTabs.scss b/invokeai/frontend/src/features/tabs/components/InvokeTabs.scss index 2a0212bd1f..1a113b4148 100644 --- a/invokeai/frontend/src/features/tabs/components/InvokeTabs.scss +++ b/invokeai/frontend/src/features/tabs/components/InvokeTabs.scss @@ -24,8 +24,8 @@ } svg { - width: 26px; - height: 26px; + width: 24px; + height: 24px; } &[aria-selected='true'] { From 6241fc19e0c396859c6fb70cc9a0aad7af83c288 Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Mon, 6 Feb 2023 19:59:02 +1300 Subject: [PATCH 5/9] Fix the model manager edit placeholder not being full height --- .../system/components/ModelManager/CheckpointModelEdit.tsx | 1 - .../system/components/ModelManager/DiffusersModelEdit.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/invokeai/frontend/src/features/system/components/ModelManager/CheckpointModelEdit.tsx b/invokeai/frontend/src/features/system/components/ModelManager/CheckpointModelEdit.tsx index 6747963d98..fe842cd874 100644 --- a/invokeai/frontend/src/features/system/components/ModelManager/CheckpointModelEdit.tsx +++ b/invokeai/frontend/src/features/system/components/ModelManager/CheckpointModelEdit.tsx @@ -316,7 +316,6 @@ export default function CheckpointModelEdit() { ) : ( Date: Mon, 6 Feb 2023 19:59:15 +1300 Subject: [PATCH 6/9] Organize language picker items alphabetically --- .../system/components/LanguagePicker.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/invokeai/frontend/src/features/system/components/LanguagePicker.tsx b/invokeai/frontend/src/features/system/components/LanguagePicker.tsx index c668cd4291..030429c7d8 100644 --- a/invokeai/frontend/src/features/system/components/LanguagePicker.tsx +++ b/invokeai/frontend/src/features/system/components/LanguagePicker.tsx @@ -13,16 +13,16 @@ export default function LanguagePicker() { const LANGUAGES = { en: t('common:langEnglish'), - ru: t('common:langRussian'), - it: t('common:langItalian'), - pt_br: t('common:langBrPortuguese'), - de: t('common:langGerman'), - pl: t('common:langPolish'), - zh_cn: t('common:langSimplifiedChinese'), - es: t('common:langSpanish'), - ja: t('common:langJapanese'), nl: t('common:langDutch'), fr: t('common:langFrench'), + de: t('common:langGerman'), + it: t('common:langItalian'), + ja: t('common:langJapanese'), + pl: t('common:langPolish'), + pt_br: t('common:langBrPortuguese'), + ru: t('common:langRussian'), + zh_cn: t('common:langSimplifiedChinese'), + es: t('common:langSpanish'), ua: t('common:langUkranian'), }; From 7604b365770acf0346ce99dbb928f366545d33aa Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Tue, 7 Feb 2023 01:58:49 +1300 Subject: [PATCH 7/9] Add Negative Prompts Box --- .../frontend/public/locales/options/en.json | 1 + .../common/hooks/useClickOutsideWatcher.ts | 1 - .../src/common/util/getPromptAndNegative.ts | 20 ++++++++++ .../src/common/util/parameterTranslation.ts | 5 +++ .../components/CurrentImageButtons.tsx | 17 +++++++-- .../gallery/components/HoverableImage.tsx | 18 +++++++-- .../gallery/store/thunks/uploadImage.ts | 1 - .../PromptInput/NegativePromptInput.tsx | 38 +++++++++++++++++++ .../features/options/store/optionsSlice.ts | 24 +++++++++++- .../ImageToImage/ImageToImagePanel.tsx | 7 +++- .../TextToImage/TextToImagePanel.tsx | 7 +++- .../UnifiedCanvas/UnifiedCanvasPanel.tsx | 7 +++- 12 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 invokeai/frontend/src/common/util/getPromptAndNegative.ts create mode 100644 invokeai/frontend/src/features/options/components/PromptInput/NegativePromptInput.tsx diff --git a/invokeai/frontend/public/locales/options/en.json b/invokeai/frontend/public/locales/options/en.json index cc38efc0b7..32cadf1998 100644 --- a/invokeai/frontend/public/locales/options/en.json +++ b/invokeai/frontend/public/locales/options/en.json @@ -43,6 +43,7 @@ "invoke": "Invoke", "cancel": "Cancel", "promptPlaceholder": "Type prompt here. [negative tokens], (upweight)++, (downweight)--, swap and blend are available (see docs)", + "negativePrompts": "Negative Prompts", "sendTo": "Send to", "sendToImg2Img": "Send to Image to Image", "sendToUnifiedCanvas": "Send To Unified Canvas", diff --git a/invokeai/frontend/src/common/hooks/useClickOutsideWatcher.ts b/invokeai/frontend/src/common/hooks/useClickOutsideWatcher.ts index 1f20c06302..3dd4c7cfc7 100644 --- a/invokeai/frontend/src/common/hooks/useClickOutsideWatcher.ts +++ b/invokeai/frontend/src/common/hooks/useClickOutsideWatcher.ts @@ -11,7 +11,6 @@ const useClickOutsideWatcher = () => { function handleClickOutside(e: MouseEvent) { watchers.forEach(({ ref, enable, callback }) => { if (enable && ref.current && !ref.current.contains(e.target as Node)) { - console.log('callback'); callback(); } }); diff --git a/invokeai/frontend/src/common/util/getPromptAndNegative.ts b/invokeai/frontend/src/common/util/getPromptAndNegative.ts new file mode 100644 index 0000000000..8f47687929 --- /dev/null +++ b/invokeai/frontend/src/common/util/getPromptAndNegative.ts @@ -0,0 +1,20 @@ +import * as InvokeAI from 'app/invokeai'; +import promptToString from './promptToString'; + +export function getPromptAndNegative(input_prompt: InvokeAI.Prompt) { + let prompt: string = promptToString(input_prompt); + let negativePrompt: string | null = null; + + const negativePromptRegExp = new RegExp(/(?<=\[)[^\][]*(?=])/, 'gi'); + const negativePromptMatches = [...prompt.matchAll(negativePromptRegExp)]; + + if (negativePromptMatches && negativePromptMatches.length > 0) { + negativePrompt = negativePromptMatches.join(', '); + prompt = prompt + .replaceAll(negativePromptRegExp, '') + .replaceAll('[]', '') + .trim(); + } + + return [prompt, negativePrompt]; +} diff --git a/invokeai/frontend/src/common/util/parameterTranslation.ts b/invokeai/frontend/src/common/util/parameterTranslation.ts index 02ae01fdba..2853b21b1d 100644 --- a/invokeai/frontend/src/common/util/parameterTranslation.ts +++ b/invokeai/frontend/src/common/util/parameterTranslation.ts @@ -106,6 +106,7 @@ export const frontendToBackendParameters = ( iterations, perlin, prompt, + negativePrompt, sampler, seamBlur, seamless, @@ -155,6 +156,10 @@ export const frontendToBackendParameters = ( let esrganParameters: false | BackendEsrGanParameters = false; let facetoolParameters: false | BackendFacetoolParameters = false; + if (negativePrompt !== '') { + generationParameters.prompt = `${prompt} [${negativePrompt}]`; + } + generationParameters.seed = shouldRandomizeSeed ? randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX) : seed; diff --git a/invokeai/frontend/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/src/features/gallery/components/CurrentImageButtons.tsx index 6612bea8d2..6f078dabde 100644 --- a/invokeai/frontend/src/features/gallery/components/CurrentImageButtons.tsx +++ b/invokeai/frontend/src/features/gallery/components/CurrentImageButtons.tsx @@ -9,6 +9,7 @@ import { setAllParameters, setInitialImage, setIsLightBoxOpen, + setNegativePrompt, setPrompt, setSeed, setShouldShowImageDetails, @@ -44,6 +45,7 @@ import { GalleryState } from 'features/gallery/store/gallerySlice'; import { activeTabNameSelector } from 'features/options/store/optionsSelectors'; import IAIPopover from 'common/components/IAIPopover'; import { useTranslation } from 'react-i18next'; +import { getPromptAndNegative } from 'common/util/getPromptAndNegative'; const systemSelector = createSelector( [ @@ -241,9 +243,18 @@ const CurrentImageButtons = () => { [currentImage] ); - const handleClickUsePrompt = () => - currentImage?.metadata?.image?.prompt && - dispatch(setPrompt(currentImage.metadata.image.prompt)); + const handleClickUsePrompt = () => { + if (currentImage?.metadata?.image?.prompt) { + const [prompt, negativePrompt] = getPromptAndNegative( + currentImage?.metadata?.image?.prompt + ); + + prompt && dispatch(setPrompt(prompt)); + negativePrompt + ? dispatch(setNegativePrompt(negativePrompt)) + : dispatch(setNegativePrompt('')); + } + }; useHotkeys( 'p', diff --git a/invokeai/frontend/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/src/features/gallery/components/HoverableImage.tsx index aca85899d8..c06dbc515a 100644 --- a/invokeai/frontend/src/features/gallery/components/HoverableImage.tsx +++ b/invokeai/frontend/src/features/gallery/components/HoverableImage.tsx @@ -10,9 +10,10 @@ import { DragEvent, memo, useState } from 'react'; import { setActiveTab, setAllImageToImageParameters, - setAllTextToImageParameters, + setAllParameters, setInitialImage, setIsLightBoxOpen, + setNegativePrompt, setPrompt, setSeed, } from 'features/options/store/optionsSlice'; @@ -24,6 +25,7 @@ import { } from 'features/canvas/store/canvasSlice'; import { hoverableImageSelector } from 'features/gallery/store/gallerySliceSelectors'; import { useTranslation } from 'react-i18next'; +import { getPromptAndNegative } from 'common/util/getPromptAndNegative'; interface HoverableImageProps { image: InvokeAI.Image; @@ -62,7 +64,17 @@ const HoverableImage = memo((props: HoverableImageProps) => { const handleMouseOut = () => setIsHovered(false); const handleUsePrompt = () => { - image.metadata && dispatch(setPrompt(image.metadata.image.prompt)); + if (image.metadata) { + const [prompt, negativePrompt] = getPromptAndNegative( + image.metadata?.image?.prompt + ); + + prompt && dispatch(setPrompt(prompt)); + negativePrompt + ? dispatch(setNegativePrompt(negativePrompt)) + : dispatch(setNegativePrompt('')); + } + toast({ title: t('toast:promptSet'), status: 'success', @@ -115,7 +127,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { }; const handleUseAllParameters = () => { - metadata && dispatch(setAllTextToImageParameters(metadata)); + metadata && dispatch(setAllParameters(metadata)); toast({ title: t('toast:parametersSet'), status: 'success', diff --git a/invokeai/frontend/src/features/gallery/store/thunks/uploadImage.ts b/invokeai/frontend/src/features/gallery/store/thunks/uploadImage.ts index 81aa6846c2..ad7de71d38 100644 --- a/invokeai/frontend/src/features/gallery/store/thunks/uploadImage.ts +++ b/invokeai/frontend/src/features/gallery/store/thunks/uploadImage.ts @@ -38,7 +38,6 @@ export const uploadImage = }); const image = (await response.json()) as InvokeAI.ImageUploadResponse; - console.log(image); const newImage: InvokeAI.Image = { uuid: uuidv4(), category: 'user', diff --git a/invokeai/frontend/src/features/options/components/PromptInput/NegativePromptInput.tsx b/invokeai/frontend/src/features/options/components/PromptInput/NegativePromptInput.tsx new file mode 100644 index 0000000000..43acd85313 --- /dev/null +++ b/invokeai/frontend/src/features/options/components/PromptInput/NegativePromptInput.tsx @@ -0,0 +1,38 @@ +import { FormControl, Textarea } from '@chakra-ui/react'; +import type { RootState } from 'app/store'; +import { useAppDispatch, useAppSelector } from 'app/storeHooks'; +import { setNegativePrompt } from 'features/options/store/optionsSlice'; +import { useTranslation } from 'react-i18next'; + +export function NegativePromptInput() { + const negativePrompt = useAppSelector( + (state: RootState) => state.options.negativePrompt + ); + + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + + return ( + +