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] 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 ( + +