Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@
"direction": "Direction",
"ipAdapter": "IP Adapter",
"t2iAdapter": "T2I Adapter",
"prompt": "Prompt",
"positivePrompt": "Positive Prompt",
"negativePrompt": "Negative Prompt",
"removeNegativePrompt": "Remove Negative Prompt",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { describe, expect, it } from 'vitest';

import {
paramsSliceConfig,
positivePromptAddedToHistory,
promptRemovedFromHistory,
selectModelSupportsDimensions,
selectModelSupportsGuidance,
selectModelSupportsNegativePrompt,
Expand Down Expand Up @@ -165,4 +167,52 @@ describe('paramsSliceConfig persisted state migration', () => {
expect(result.dimensions.width).toBe(768);
expect(result.dimensions.height).toBe(768);
});

it('migrates old positive prompt history entries to prompt pairs', () => {
expect(migrate).toBeDefined();

const initial = getInitialParamsState();
const v3State: Record<string, unknown> = {
...initial,
positivePromptHistory: ['a fluffy cat'],
};

const result = migrate?.(v3State) as ReturnType<typeof getInitialParamsState>;

expect(result.positivePromptHistory).toEqual([{ positivePrompt: 'a fluffy cat', negativePrompt: null }]);
});
});

describe('paramsSlice prompt history', () => {
it('stores positive and negative prompts in the same history item', () => {
const initial = getInitialParamsState();
const state = paramsSliceConfig.slice.reducer(
initial,
positivePromptAddedToHistory({ positivePrompt: ' a fluffy cat ', negativePrompt: ' blurry ' })
);

expect(state.positivePromptHistory).toEqual([{ positivePrompt: 'a fluffy cat', negativePrompt: 'blurry' }]);
});

it('deduplicates and removes prompt history by positive and negative prompt pair', () => {
const initial = getInitialParamsState();
const withFirstPrompt = paramsSliceConfig.slice.reducer(
initial,
positivePromptAddedToHistory({ positivePrompt: 'a cat', negativePrompt: 'blurry' })
);
const withSecondPrompt = paramsSliceConfig.slice.reducer(
withFirstPrompt,
positivePromptAddedToHistory({ positivePrompt: 'a cat', negativePrompt: 'low quality' })
);
const removed = paramsSliceConfig.slice.reducer(
withSecondPrompt,
promptRemovedFromHistory({ positivePrompt: 'a cat', negativePrompt: 'blurry' })
);

expect(withSecondPrompt.positivePromptHistory).toEqual([
{ positivePrompt: 'a cat', negativePrompt: 'low quality' },
{ positivePrompt: 'a cat', negativePrompt: 'blurry' },
]);
expect(removed.positivePromptHistory).toEqual([{ positivePrompt: 'a cat', negativePrompt: 'low quality' }]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMul
import { isPlainObject } from 'es-toolkit';
import { clamp } from 'es-toolkit/compat';
import { logout } from 'features/auth/store/authSlice';
import type { AspectRatioID, InfillMethod, ParamsState, RgbaColor } from 'features/controlLayers/store/types';
import type {
AspectRatioID,
InfillMethod,
ParamsState,
PromptHistoryItem,
RgbaColor,
} from 'features/controlLayers/store/types';
import {
ASPECT_RATIO_MAP,
DEFAULT_ASPECT_RATIO_CONFIG,
Expand Down Expand Up @@ -306,20 +312,33 @@ const slice = createSlice({
positivePromptChanged: (state, action: PayloadAction<ParameterPositivePrompt>) => {
state.positivePrompt = action.payload;
},
positivePromptAddedToHistory: (state, action: PayloadAction<ParameterPositivePrompt>) => {
const prompt = action.payload.trim();
if (prompt.length === 0) {
positivePromptAddedToHistory: (state, action: PayloadAction<PromptHistoryItem>) => {
const prompt: PromptHistoryItem = {
positivePrompt: action.payload.positivePrompt.trim(),
negativePrompt: action.payload.negativePrompt?.trim() || null,
};
if (prompt.positivePrompt.length === 0 && !prompt.negativePrompt) {
return;
}

state.positivePromptHistory = [prompt, ...state.positivePromptHistory.filter((p) => p !== prompt)];
state.positivePromptHistory = [
prompt,
...state.positivePromptHistory.filter(
(p) =>
p.positivePrompt !== prompt.positivePrompt || (p.negativePrompt ?? null) !== (prompt.negativePrompt ?? null)
),
];

if (state.positivePromptHistory.length > MAX_POSITIVE_PROMPT_HISTORY) {
state.positivePromptHistory = state.positivePromptHistory.slice(0, MAX_POSITIVE_PROMPT_HISTORY);
}
},
promptRemovedFromHistory: (state, action: PayloadAction<string>) => {
state.positivePromptHistory = state.positivePromptHistory.filter((p) => p !== action.payload);
promptRemovedFromHistory: (state, action: PayloadAction<PromptHistoryItem>) => {
state.positivePromptHistory = state.positivePromptHistory.filter(
(p) =>
p.positivePrompt !== action.payload.positivePrompt ||
(p.negativePrompt ?? null) !== (action.payload.negativePrompt ?? null)
);
},
promptHistoryCleared: (state) => {
state.positivePromptHistory = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -765,8 +765,16 @@ const zDimensionsState = z.object({
});

export const MAX_POSITIVE_PROMPT_HISTORY = 100;
const zPromptHistoryItem = z.union([
zParameterPositivePrompt.transform((positivePrompt) => ({ positivePrompt, negativePrompt: null })),
z.object({
positivePrompt: zParameterPositivePrompt,
negativePrompt: zParameterNegativePrompt,
}),
]);
export type PromptHistoryItem = z.infer<typeof zPromptHistoryItem>;
const zPositivePromptHistory = z
.array(zParameterPositivePrompt)
.array(zPromptHistoryItem)
.transform((arr) => arr.slice(0, MAX_POSITIVE_PROMPT_HISTORY));

export const zInfillMethod = z.enum(['patchmatch', 'lama', 'cv2', 'color', 'tile']);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { range } from 'es-toolkit/compat';
import type { SeedBehaviour } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import type { BaseModelType } from 'features/nodes/types/common';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import { selectPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import type { components } from 'services/api/schema';
import type { Batch, EnqueueBatchArg, Invocation } from 'services/api/types';

Expand All @@ -26,11 +27,12 @@ export const prepareLinearUIBatch = (arg: {
prepend: boolean;
base: BaseModelType;
positivePromptNode: Invocation<'string'>;
negativePromptNode?: Invocation<'string'>;
seedNode?: Invocation<'integer'>;
origin: string;
destination: string;
}): EnqueueBatchArg => {
const { state, g, base, prepend, positivePromptNode, seedNode, origin, destination } = arg;
const { state, g, base, prepend, positivePromptNode, negativePromptNode, seedNode, origin, destination } = arg;
const { iterations, shouldRandomizeSeed, seed } = state.params;
const { prompts, seedBehaviour } = state.dynamicPrompts;

Expand Down Expand Up @@ -74,6 +76,15 @@ export const prepareLinearUIBatch = (arg: {
items: extendedPrompts,
});

if (negativePromptNode) {
const negativePrompt = selectPresetModifiedPrompts(state).negative;
firstBatchDatumList.push({
node_path: negativePromptNode.id,
field_name: 'value',
items: extendedPrompts.map(() => negativePrompt),
});
}

data.push(firstBatchDatumList);

// Models without a seed node (e.g. external API models without seed support) can't express
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { isNonRefinerMainModelConfig, isSpandrelImageToImageModelConfig } from '
import { assert } from 'tsafe';

import { addLoRAs } from './generation/addLoRAs';
import { getBoardField, selectPresetModifiedPrompts } from './graphBuilderUtils';
import { getBoardField } from './graphBuilderUtils';
import type { GraphBuilderReturn } from './types';

export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise<GraphBuilderReturn> => {
Expand Down Expand Up @@ -35,6 +35,10 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise
id: getPrefixedId('positive_prompt'),
type: 'string',
});
const negativePrompt = g.addNode({
id: getPrefixedId('negative_prompt'),
type: 'string',
});

const spandrelAutoscale = g.addNode({
type: 'spandrel_image_to_image_autoscale',
Expand Down Expand Up @@ -105,17 +109,13 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise
let modelLoader;

if (model.base === 'sdxl') {
const prompts = selectPresetModifiedPrompts(state);

posCond = g.addNode({
type: 'sdxl_compel_prompt',
id: getPrefixedId('pos_cond'),
});
negCond = g.addNode({
type: 'sdxl_compel_prompt',
id: getPrefixedId('neg_cond'),
prompt: prompts.negative,
style: prompts.negative,
});
modelLoader = g.addNode({
type: 'sdxl_model_loader',
Expand All @@ -131,24 +131,21 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise

g.addEdge(positivePrompt, 'value', posCond, 'prompt');
g.addEdge(positivePrompt, 'value', posCond, 'style');
g.addEdge(negativePrompt, 'value', negCond, 'prompt');
g.addEdge(negativePrompt, 'value', negCond, 'style');

addSDXLLoRAs(state, g, tiledMultidiffusion, modelLoader, null, posCond, negCond);

g.upsertMetadata({
negative_prompt: prompts.negative,
});
g.addEdgeToMetadata(positivePrompt, 'value', 'positive_prompt');
g.addEdgeToMetadata(negativePrompt, 'value', 'negative_prompt');
} else {
const prompts = selectPresetModifiedPrompts(state);

posCond = g.addNode({
type: 'compel',
id: getPrefixedId('pos_cond'),
});
negCond = g.addNode({
type: 'compel',
id: getPrefixedId('neg_cond'),
prompt: prompts.negative,
});
modelLoader = g.addNode({
type: 'main_model_loader',
Expand All @@ -166,14 +163,12 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise
g.addEdge(modelLoader, 'unet', tiledMultidiffusion, 'unet');

g.addEdge(positivePrompt, 'value', posCond, 'prompt');
g.addEdge(negativePrompt, 'value', negCond, 'prompt');

addLoRAs(state, g, tiledMultidiffusion, modelLoader, null, clipSkipNode, posCond, negCond);

g.upsertMetadata({
negative_prompt: prompts.negative,
});

g.addEdgeToMetadata(positivePrompt, 'value', 'positive_prompt');
g.addEdgeToMetadata(negativePrompt, 'value', 'negative_prompt');
}

const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig);
Expand Down Expand Up @@ -261,5 +256,6 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise
g,
seed,
positivePrompt,
negativePrompt,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless';
import { addTextToImage } from 'features/nodes/util/graph/generation/addTextToImage';
import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import { selectCanvasOutputFields, selectPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import { selectCanvasOutputFields } from 'features/nodes/util/graph/graphBuilderUtils';
import type { GraphBuilderArg, GraphBuilderReturn, ImageOutputNodes } from 'features/nodes/util/graph/types';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import type { Invocation } from 'services/api/types';
Expand Down Expand Up @@ -51,8 +51,6 @@ export const buildSD1Graph = async (arg: GraphBuilderArg): Promise<GraphBuilderR
} = params;

const fp32 = vaePrecision === 'fp32';
const prompts = selectPresetModifiedPrompts(state);

const g = new Graph(getPrefixedId('sd1_graph'));
const seed = g.addNode({
id: getPrefixedId('seed'),
Expand All @@ -62,6 +60,10 @@ export const buildSD1Graph = async (arg: GraphBuilderArg): Promise<GraphBuilderR
id: getPrefixedId('positive_prompt'),
type: 'string',
});
const negativePrompt = g.addNode({
id: getPrefixedId('negative_prompt'),
type: 'string',
});
const modelLoader = g.addNode({
type: 'main_model_loader',
id: getPrefixedId('sd1_model_loader'),
Expand All @@ -83,7 +85,6 @@ export const buildSD1Graph = async (arg: GraphBuilderArg): Promise<GraphBuilderR
const negCond = g.addNode({
type: 'compel',
id: getPrefixedId('neg_cond'),
prompt: prompts.negative,
});
const negCondCollect = g.addNode({
type: 'collect',
Expand Down Expand Up @@ -127,6 +128,7 @@ export const buildSD1Graph = async (arg: GraphBuilderArg): Promise<GraphBuilderR
g.addEdge(posCond, 'conditioning', posCondCollect, 'item');
g.addEdge(posCondCollect, 'collection', denoise, 'positive_conditioning');

g.addEdge(negativePrompt, 'value', negCond, 'prompt');
g.addEdge(negCond, 'conditioning', negCondCollect, 'item');
g.addEdge(negCondCollect, 'collection', denoise, 'negative_conditioning');

Expand All @@ -137,7 +139,6 @@ export const buildSD1Graph = async (arg: GraphBuilderArg): Promise<GraphBuilderR
g.upsertMetadata({
cfg_scale,
cfg_rescale_multiplier,
negative_prompt: prompts.negative,
model: Graph.getModelMetadataField(model),
steps,
rand_device: shouldUseCpuNoise ? 'cpu' : 'cuda',
Expand All @@ -147,6 +148,7 @@ export const buildSD1Graph = async (arg: GraphBuilderArg): Promise<GraphBuilderR
});
g.addEdgeToMetadata(seed, 'value', 'seed');
g.addEdgeToMetadata(positivePrompt, 'value', 'positive_prompt');
g.addEdgeToMetadata(negativePrompt, 'value', 'negative_prompt');

const seamless = addSeamless(state, g, denoise, modelLoader, vaeLoader);

Expand Down Expand Up @@ -325,5 +327,6 @@ export const buildSD1Graph = async (arg: GraphBuilderArg): Promise<GraphBuilderR
g,
seed,
positivePrompt,
negativePrompt,
};
};
Loading
Loading