diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 6c9db74bbc..33d83f043a 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -85,6 +85,7 @@ "konva": "^9.2.0", "lodash-es": "^4.17.21", "nanostores": "^0.9.2", + "ohm-js": "^17.1.0", "openapi-fetch": "^0.6.1", "overlayscrollbars": "^2.2.0", "overlayscrollbars-react": "^0.5.0", diff --git a/invokeai/frontend/web/src/common/util/dynamicPrompts/grammar.ts b/invokeai/frontend/web/src/common/util/dynamicPrompts/grammar.ts new file mode 100644 index 0000000000..ccd865f2ff --- /dev/null +++ b/invokeai/frontend/web/src/common/util/dynamicPrompts/grammar.ts @@ -0,0 +1,152 @@ +import * as ohm from 'ohm-js'; + +const grammarSource = ` +Prompt { + exp + = exp choicesStart multi_number multi_trigger choices choicesEnd exp --multi_choices + | exp choicesStart multi_number multi_trigger multi_joinString multi_trigger choices choicesEnd exp --multi_choicesWithJoinString + | exp choicesStart choices choicesEnd exp --choices + | text --text + + choices + = listOf + + text + = (~choicesStart any)* + + choice + = (~reservedChar any)* + + reservedChar + = (choicesEnd | choicesStart | choicesDelimiter | multi_trigger) + + choicesStart + = "{" + + choicesEnd + = "}" + + choicesDelimiter + = "|" + + multi_trigger + = "$$" + + multi_number + = digit+ + + multi_joinString + = (~multi_trigger choice) + +}`; + +const getPermutationsOfSize = (array: string[], size: number): string[][] => { + const result: string[][] = []; + + function permute(arr: string[], m: string[] = []) { + if (m.length === size) { + result.push(m); + return; + } + for (let i = 0; i < arr.length; i++) { + const curr = arr.slice(); + const next = curr.splice(i, 1); + permute(curr, m.concat(next)); + } + } + + permute(array); + + return result; +}; + +export const dynamicPromptsGrammar = ohm.grammar(grammarSource); + +export const dynamicPromptsSemantics = dynamicPromptsGrammar + .createSemantics() + .addOperation('expand', { + exp_multi_choices: function ( + before, + _choicesStart, + number, + _multiTrigger, + choices, + _choicesEnd, + after + ) { + const beforePermutations = before.expand(); + const choicePermutations = getPermutationsOfSize( + choices.expand(), + Number(number.sourceString) + ); + const afterPermutations = after.expand(); + const sep = ','; + + const combined = []; + for (const b of beforePermutations) { + for (const c of choicePermutations) { + for (const a of afterPermutations) { + combined.push(b + c.join(sep) + a); + } + } + } + return combined; + }, + exp_multi_choicesWithJoinString: function ( + before, + _choicesStart, + number, + _multiTrigger1, + _multiJoinString, + _multiTrigger2, + choices, + _choicesEnd, + after + ) { + const beforePermutations = before.expand(); + const choicePermutations = getPermutationsOfSize( + choices.expand(), + Number(number.sourceString) + ); + const afterPermutations = after.expand(); + const sep = _multiJoinString.sourceString; + + const combined = []; + for (const b of beforePermutations) { + for (const c of choicePermutations) { + for (const a of afterPermutations) { + combined.push(b + c.join(sep) + a); + } + } + } + return combined; + }, + exp_choices: function (before, _choicesStart, choices, _choicesEnd, after) { + const beforePermutations = before.expand(); + const choicePermutations = choices.expand(); + const afterPermutations = after.expand(); + + const combined: string[] = []; + for (const b of beforePermutations) { + for (const c of choicePermutations) { + for (const a of afterPermutations) { + combined.push(b + c + a); + } + } + } + + return combined; + }, + exp_text: function (text) { + return [text.sourceString]; + }, + choices: function (choices) { + return choices.asIteration().children.map((c) => c.sourceString); + }, + _iter: function (...children) { + children.map((c) => c.expand()); + }, + _terminal: function () { + return this.sourceString; + }, + }); diff --git a/invokeai/frontend/web/src/common/util/dynamicPrompts/useDynamicPrompts.ts b/invokeai/frontend/web/src/common/util/dynamicPrompts/useDynamicPrompts.ts new file mode 100644 index 0000000000..8ad2bf7ad6 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/dynamicPrompts/useDynamicPrompts.ts @@ -0,0 +1,17 @@ +import { useMemo } from 'react'; +import { useDebounce } from 'use-debounce'; +import { dynamicPromptsGrammar, dynamicPromptsSemantics } from './grammar'; + +const parsePrompt = (prompt: string): { prompts: string[]; error: boolean } => { + const match = dynamicPromptsGrammar.match(prompt); + if (match.failed()) { + return { prompts: [prompt], error: true }; + } + return { prompts: dynamicPromptsSemantics(match).expand(), error: false }; +}; + +export const useDynamicPrompts = (prompt: string) => { + const [debouncedPrompt] = useDebounce(prompt, 500, { leading: false }); + const result = useMemo(() => parsePrompt(debouncedPrompt), [debouncedPrompt]); + return result; +}; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx index 59b5138e3e..18caf469f9 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx @@ -1,4 +1,4 @@ -import { Box, FormControl, useDisclosure } from '@chakra-ui/react'; +import { Box, FormControl, Text, useDisclosure } from '@chakra-ui/react'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { ChangeEvent, KeyboardEvent, useCallback, useRef } from 'react'; @@ -20,6 +20,7 @@ import { flushSync } from 'react-dom'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus'; +import { useDynamicPrompts } from 'common/util/dynamicPrompts/useDynamicPrompts'; const promptInputSelector = createSelector( [stateSelector, activeTabNameSelector], @@ -48,6 +49,7 @@ const ParamPositiveConditioning = () => { const promptRef = useRef(null); const { isOpen, onClose, onOpen } = useDisclosure(); const { t } = useTranslation(); + const parseResult = useDynamicPrompts(prompt); const handleChangePrompt = useCallback( (e: ChangeEvent) => { dispatch(setPositivePrompt(e.target.value)); @@ -155,6 +157,13 @@ const ParamPositiveConditioning = () => { )} + + generated {parseResult.prompts.length} prompts: + {parseResult.error && Parsing failed} + {parseResult.prompts.map((p, i) => ( + {p} + ))} + ); }; diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock index 834e6368c4..f8b5bba26e 100644 --- a/invokeai/frontend/web/yarn.lock +++ b/invokeai/frontend/web/yarn.lock @@ -5074,6 +5074,11 @@ object.values@^1.1.6: define-properties "^1.1.4" es-abstract "^1.20.4" +ohm-js@^17.1.0: + version "17.1.0" + resolved "https://registry.yarnpkg.com/ohm-js/-/ohm-js-17.1.0.tgz#50d8e08f69d7909931998d75202d35e2a90c8885" + integrity sha512-xc3B5dgAjTBQGHaH7B58M2Pmv6WvzrJ/3/7LeUzXNg0/sY3jQPdSd/S2SstppaleO77rifR1tyhdfFGNIwxf2Q== + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"