diff --git a/invokeai/app/invocations/fields.py b/invokeai/app/invocations/fields.py index 1bca7ec3f53..8f74bc964b0 100644 --- a/invokeai/app/invocations/fields.py +++ b/invokeai/app/invocations/fields.py @@ -241,6 +241,12 @@ class BoardField(BaseModel): board_id: str = Field(description="The id of the board") +class StylePresetField(BaseModel): + """A style preset primitive field""" + + style_preset_id: str = Field(description="The id of the style preset") + + class DenoiseMaskField(BaseModel): """An inpaint mask field""" diff --git a/invokeai/app/invocations/prompt_template.py b/invokeai/app/invocations/prompt_template.py new file mode 100644 index 00000000000..d2ac86358e5 --- /dev/null +++ b/invokeai/app/invocations/prompt_template.py @@ -0,0 +1,57 @@ +from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output +from invokeai.app.invocations.fields import InputField, OutputField, StylePresetField, UIComponent +from invokeai.app.services.shared.invocation_context import InvocationContext + + +@invocation_output("prompt_template_output") +class PromptTemplateOutput(BaseInvocationOutput): + """Output for the Prompt Template node""" + + positive_prompt: str = OutputField(description="The positive prompt with the template applied") + negative_prompt: str = OutputField(description="The negative prompt with the template applied") + + +@invocation( + "prompt_template", + title="Prompt Template", + tags=["prompt", "template", "style", "preset"], + category="prompt", + version="1.0.0", +) +class PromptTemplateInvocation(BaseInvocation): + """Applies a Style Preset template to positive and negative prompts. + + Select a Style Preset and provide positive/negative prompts. The node replaces + {prompt} placeholders in the template with your input prompts. + """ + + style_preset: StylePresetField = InputField( + description="The Style Preset to use as a template", + ) + positive_prompt: str = InputField( + default="", + description="The positive prompt to insert into the template's {prompt} placeholder", + ui_component=UIComponent.Textarea, + ) + negative_prompt: str = InputField( + default="", + description="The negative prompt to insert into the template's {prompt} placeholder", + ui_component=UIComponent.Textarea, + ) + + def invoke(self, context: InvocationContext) -> PromptTemplateOutput: + # Fetch the style preset from the database + style_preset = context._services.style_preset_records.get(self.style_preset.style_preset_id) + + # Get the template prompts + positive_template = style_preset.preset_data.positive_prompt + negative_template = style_preset.preset_data.negative_prompt + + # Replace {prompt} placeholder with the input prompts + rendered_positive = positive_template.replace("{prompt}", self.positive_prompt) + rendered_negative = negative_template.replace("{prompt}", self.negative_prompt) + + return PromptTemplateOutput( + positive_prompt=rendered_positive, + negative_prompt=rendered_negative, + ) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index a50b0cb9efd..90ab00b26d3 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -2651,7 +2651,9 @@ "useForTemplate": "Use For Prompt Template", "viewList": "View Template List", "viewModeTooltip": "This is how your prompt will look with your currently selected template. To edit your prompt, click anywhere in the text box.", - "togglePromptPreviews": "Toggle Prompt Previews" + "togglePromptPreviews": "Toggle Prompt Previews", + "selectPreset": "Select Style Preset", + "noMatchingPresets": "No matching presets" }, "ui": { diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx index 7139d0e1f98..60a3f8e472a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx @@ -55,6 +55,8 @@ import { isStringFieldInputTemplate, isStringGeneratorFieldInputInstance, isStringGeneratorFieldInputTemplate, + isStylePresetFieldInputInstance, + isStylePresetFieldInputTemplate, } from 'features/nodes/types/field'; import type { NodeFieldElement } from 'features/nodes/types/workflow'; import { memo } from 'react'; @@ -67,6 +69,7 @@ import ColorFieldInputComponent from './inputs/ColorFieldInputComponent'; import EnumFieldInputComponent from './inputs/EnumFieldInputComponent'; import ImageFieldInputComponent from './inputs/ImageFieldInputComponent'; import SchedulerFieldInputComponent from './inputs/SchedulerFieldInputComponent'; +import StylePresetFieldInputComponent from './inputs/StylePresetFieldInputComponent'; type Props = { nodeId: string; @@ -206,6 +209,13 @@ export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props) return ; } + if (isStylePresetFieldInputTemplate(template)) { + if (!isStylePresetFieldInputInstance(field)) { + return null; + } + return ; + } + if (isModelIdentifierFieldInputTemplate(template)) { if (!isModelIdentifierFieldInputInstance(field)) { return null; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StylePresetFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StylePresetFieldInputComponent.tsx new file mode 100644 index 00000000000..7791ed3a3c9 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StylePresetFieldInputComponent.tsx @@ -0,0 +1,73 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { fieldStylePresetValueChanged } from 'features/nodes/store/nodesSlice'; +import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants'; +import type { StylePresetFieldInputInstance, StylePresetFieldInputTemplate } from 'features/nodes/types/field'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; + +import type { FieldComponentProps } from './types'; + +const StylePresetFieldInputComponent = ( + props: FieldComponentProps +) => { + const { nodeId, field } = props; + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const { data: stylePresets, isLoading } = useListStylePresetsQuery(); + + const options = useMemo(() => { + const _options: ComboboxOption[] = []; + if (stylePresets) { + for (const preset of stylePresets) { + _options.push({ + label: preset.name, + value: preset.id, + }); + } + } + return _options; + }, [stylePresets]); + + const onChange = useCallback( + (v) => { + if (!v) { + return; + } + + dispatch( + fieldStylePresetValueChanged({ + nodeId, + fieldName: field.name, + value: { style_preset_id: v.value }, + }) + ); + }, + [dispatch, field.name, nodeId] + ); + + const value = useMemo(() => { + const _value = field.value; + if (!_value) { + return null; + } + return options.find((o) => o.value === _value.style_preset_id) ?? null; + }, [field.value, options]); + + const noOptionsMessage = useCallback(() => t('stylePresets.noMatchingPresets'), [t]); + + return ( + + ); +}; + +export default memo(StylePresetFieldInputComponent); diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 98b41da3059..bdab6c1ae36 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -41,6 +41,7 @@ import type { StringFieldCollectionValue, StringFieldValue, StringGeneratorFieldValue, + StylePresetFieldValue, } from 'features/nodes/types/field'; import { zBoardFieldValue, @@ -62,6 +63,7 @@ import { zStringFieldCollectionValue, zStringFieldValue, zStringGeneratorFieldValue, + zStylePresetFieldValue, } from 'features/nodes/types/field'; import type { AnyEdge, AnyNode } from 'features/nodes/types/invocation'; import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation'; @@ -438,6 +440,9 @@ const slice = createSlice({ fieldBoardValueChanged: (state, action: FieldValueAction) => { fieldValueReducer(state, action, zBoardFieldValue); }, + fieldStylePresetValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zStylePresetFieldValue); + }, fieldImageValueChanged: (state, action: FieldValueAction) => { fieldValueReducer(state, action, zImageFieldValue); }, @@ -588,6 +593,7 @@ export const { fieldBoardValueChanged, fieldBooleanValueChanged, fieldColorValueChanged, + fieldStylePresetValueChanged, fieldEnumModelValueChanged, fieldImageValueChanged, fieldImageCollectionValueChanged, diff --git a/invokeai/frontend/web/src/features/nodes/types/common.ts b/invokeai/frontend/web/src/features/nodes/types/common.ts index 97c7fff795d..89e7cd8997c 100644 --- a/invokeai/frontend/web/src/features/nodes/types/common.ts +++ b/invokeai/frontend/web/src/features/nodes/types/common.ts @@ -16,6 +16,10 @@ export const zBoardField = z.object({ }); export type BoardField = z.infer; +export const zStylePresetField = z.object({ + style_preset_id: z.string().trim().min(1), +}); + export const zColorField = z.object({ r: z.number().int().min(0).max(255), g: z.number().int().min(0).max(255), diff --git a/invokeai/frontend/web/src/features/nodes/types/constants.ts b/invokeai/frontend/web/src/features/nodes/types/constants.ts index a8ab6d231e3..808bf6aecdf 100644 --- a/invokeai/frontend/web/src/features/nodes/types/constants.ts +++ b/invokeai/frontend/web/src/features/nodes/types/constants.ts @@ -35,6 +35,7 @@ export const NO_PAN_CLASS = 'nopan'; export const FIELD_COLORS: { [key: string]: string } = { BoardField: 'purple.500', BooleanField: 'green.500', + StylePresetField: 'purple.400', CLIPField: 'green.500', ColorField: 'pink.300', ConditioningField: 'cyan.500', diff --git a/invokeai/frontend/web/src/features/nodes/types/field.ts b/invokeai/frontend/web/src/features/nodes/types/field.ts index 356b7656609..98b20912ab2 100644 --- a/invokeai/frontend/web/src/features/nodes/types/field.ts +++ b/invokeai/frontend/web/src/features/nodes/types/field.ts @@ -19,6 +19,7 @@ import { zModelIdentifierField, zModelType, zSchedulerField, + zStylePresetField, } from './common'; /** @@ -169,6 +170,11 @@ const zBoardFieldType = zFieldTypeBase.extend({ originalType: zStatelessFieldType.optional(), }); +const zStylePresetFieldType = zFieldTypeBase.extend({ + name: z.literal('StylePresetField'), + originalType: zStatelessFieldType.optional(), +}); + const zColorFieldType = zFieldTypeBase.extend({ name: z.literal('ColorField'), originalType: zStatelessFieldType.optional(), @@ -205,6 +211,7 @@ const zStatefulFieldType = z.union([ zEnumFieldType, zImageFieldType, zBoardFieldType, + zStylePresetFieldType, zModelIdentifierFieldType, zColorFieldType, zSchedulerFieldType, @@ -607,6 +614,27 @@ export const isBoardFieldInputInstance = buildInstanceTypeGuard(zBoardFieldInput export const isBoardFieldInputTemplate = buildTemplateTypeGuard('BoardField'); // #endregion +// #region StylePresetField +export const zStylePresetFieldValue = zStylePresetField.optional(); +const zStylePresetFieldInputInstance = zFieldInputInstanceBase.extend({ + value: zStylePresetFieldValue, +}); +const zStylePresetFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zStylePresetFieldType, + originalType: zFieldType.optional(), + default: zStylePresetFieldValue, +}); +const zStylePresetFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zStylePresetFieldType, +}); +export type StylePresetFieldValue = z.infer; +export type StylePresetFieldInputInstance = z.infer; +export type StylePresetFieldInputTemplate = z.infer; +export const isStylePresetFieldInputInstance = buildInstanceTypeGuard(zStylePresetFieldInputInstance); +export const isStylePresetFieldInputTemplate = + buildTemplateTypeGuard('StylePresetField'); +// #endregion + // #region ColorField export const zColorFieldValue = zColorField.optional(); const zColorFieldInputInstance = zFieldInputInstanceBase.extend({ @@ -1257,6 +1285,7 @@ export const zStatefulFieldValue = z.union([ zImageFieldValue, zImageFieldCollectionValue, zBoardFieldValue, + zStylePresetFieldValue, zModelIdentifierFieldValue, zColorFieldValue, zSchedulerFieldValue, @@ -1284,6 +1313,7 @@ const zStatefulFieldInputInstance = z.union([ zImageFieldInputInstance, zImageFieldCollectionInputInstance, zBoardFieldInputInstance, + zStylePresetFieldInputInstance, zModelIdentifierFieldInputInstance, zColorFieldInputInstance, zSchedulerFieldInputInstance, @@ -1310,6 +1340,7 @@ const zStatefulFieldInputTemplate = z.union([ zImageFieldInputTemplate, zImageFieldCollectionInputTemplate, zBoardFieldInputTemplate, + zStylePresetFieldInputTemplate, zModelIdentifierFieldInputTemplate, zColorFieldInputTemplate, zSchedulerFieldInputTemplate, @@ -1337,6 +1368,7 @@ const zStatefulFieldOutputTemplate = z.union([ zImageFieldOutputTemplate, zImageFieldCollectionOutputTemplate, zBoardFieldOutputTemplate, + zStylePresetFieldOutputTemplate, zModelIdentifierFieldOutputTemplate, zColorFieldOutputTemplate, zSchedulerFieldOutputTemplate, diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputInstance.ts b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputInstance.ts index 1c14ab1f4d0..ef7b92efdd8 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputInstance.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputInstance.ts @@ -12,6 +12,7 @@ const FIELD_VALUE_FALLBACK_MAP: Record = ModelIdentifierField: undefined, SchedulerField: 'dpmpp_3m_k', StringField: '', + StylePresetField: undefined, FloatGeneratorField: undefined, IntegerGeneratorField: undefined, StringGeneratorField: undefined, diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts index 342dead58ca..27a0b21a7c9 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts @@ -23,6 +23,7 @@ import type { StringFieldCollectionInputTemplate, StringFieldInputTemplate, StringGeneratorFieldInputTemplate, + StylePresetFieldInputTemplate, } from 'features/nodes/types/field'; import { getFloatGeneratorArithmeticSequenceDefaults, @@ -289,6 +290,20 @@ const buildBoardFieldInputTemplate: FieldInputTemplateBuilder = ({ + schemaObject, + baseField, + fieldType, +}) => { + const template: StylePresetFieldInputTemplate = { + ...baseField, + type: fieldType, + default: schemaObject.default ?? undefined, + }; + + return template; +}; + const buildImageFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, @@ -460,6 +475,7 @@ const TEMPLATE_BUILDER_MAP: Record