feat(ui): add dynamic prompts to t2i tab

- add param accordion for dynamic prompts
- update graphs
This commit is contained in:
psychedelicious 2023-06-26 16:20:57 +10:00
parent 9cfac4175f
commit 6390af229d
29 changed files with 479 additions and 576 deletions

View File

@ -22,6 +22,7 @@ import boardsReducer from 'features/gallery/store/boardSlice';
import configReducer from 'features/system/store/configSlice';
import hotkeysReducer from 'features/ui/store/hotkeysSlice';
import uiReducer from 'features/ui/store/uiSlice';
import dynamicPromptsReducer from 'features/dynamicPrompts/store/slice';
import { listenerMiddleware } from './middleware/listenerMiddleware';
@ -48,6 +49,7 @@ const allReducers = {
controlNet: controlNetReducer,
boards: boardsReducer,
// session: sessionReducer,
dynamicPrompts: dynamicPromptsReducer,
[api.reducerPath]: api.reducer,
};
@ -65,6 +67,7 @@ const rememberedKeys: (keyof typeof allReducers)[] = [
'system',
'ui',
'controlNet',
'dynamicPrompts',
// 'boards',
// 'hotkeys',
// 'config',
@ -100,3 +103,4 @@ export type AppGetState = typeof store.getState;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunkDispatch = ThunkDispatch<RootState, any, AnyAction>;
export type AppDispatch = typeof store.dispatch;
export const stateSelector = (state: RootState) => state;

View File

@ -171,6 +171,14 @@ export type AppConfig = {
fineStep: number;
coarseStep: number;
};
dynamicPrompts: {
maxPrompts: {
initial: number;
min: number;
sliderMax: number;
inputMax: number;
};
};
};
};

View File

@ -41,7 +41,15 @@ const IAISwitch = (props: Props) => {
{...formControlProps}
>
{label && (
<FormLabel my={1} flexGrow={1} {...formLabelProps}>
<FormLabel
my={1}
flexGrow={1}
sx={{
cursor: isDisabled ? 'not-allowed' : 'pointer',
...formLabelProps?.sx,
}}
{...formLabelProps}
>
{label}
</FormLabel>
)}

View File

@ -0,0 +1,45 @@
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAICollapse from 'common/components/IAICollapse';
import { useCallback } from 'react';
import { isEnabledToggled } from '../store/slice';
import ParamDynamicPromptsMaxPrompts from './ParamDynamicPromptsMaxPrompts';
import ParamDynamicPromptsCombinatorial from './ParamDynamicPromptsCombinatorial';
import { Flex } from '@chakra-ui/react';
const selector = createSelector(
stateSelector,
(state) => {
const { isEnabled } = state.dynamicPrompts;
return { isEnabled };
},
defaultSelectorOptions
);
const ParamDynamicPromptsCollapse = () => {
const dispatch = useAppDispatch();
const { isEnabled } = useAppSelector(selector);
const handleToggleIsEnabled = useCallback(() => {
dispatch(isEnabledToggled());
}, [dispatch]);
return (
<IAICollapse
isOpen={isEnabled}
onToggle={handleToggleIsEnabled}
label="Dynamic Prompts"
withSwitch
>
<Flex sx={{ gap: 2, flexDir: 'column' }}>
<ParamDynamicPromptsMaxPrompts />
<ParamDynamicPromptsCombinatorial />
</Flex>
</IAICollapse>
);
};
export default ParamDynamicPromptsCollapse;

View File

@ -0,0 +1,36 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { combinatorialToggled } from '../store/slice';
import { createSelector } from '@reduxjs/toolkit';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { useCallback } from 'react';
import { stateSelector } from 'app/store/store';
import IAISwitch from 'common/components/IAISwitch';
const selector = createSelector(
stateSelector,
(state) => {
const { combinatorial } = state.dynamicPrompts;
return { combinatorial };
},
defaultSelectorOptions
);
const ParamDynamicPromptsCombinatorial = () => {
const { combinatorial } = useAppSelector(selector);
const dispatch = useAppDispatch();
const handleChange = useCallback(() => {
dispatch(combinatorialToggled());
}, [dispatch]);
return (
<IAISwitch
label="Combinatorial Generation"
isChecked={combinatorial}
onChange={handleChange}
/>
);
};
export default ParamDynamicPromptsCombinatorial;

View File

@ -0,0 +1,53 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { maxPromptsChanged, maxPromptsReset } from '../store/slice';
import { createSelector } from '@reduxjs/toolkit';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { useCallback } from 'react';
import { stateSelector } from 'app/store/store';
const selector = createSelector(
stateSelector,
(state) => {
const { maxPrompts } = state.dynamicPrompts;
const { min, sliderMax, inputMax } =
state.config.sd.dynamicPrompts.maxPrompts;
return { maxPrompts, min, sliderMax, inputMax };
},
defaultSelectorOptions
);
const ParamDynamicPromptsMaxPrompts = () => {
const { maxPrompts, min, sliderMax, inputMax } = useAppSelector(selector);
const dispatch = useAppDispatch();
const handleChange = useCallback(
(v: number) => {
dispatch(maxPromptsChanged(v));
},
[dispatch]
);
const handleReset = useCallback(() => {
dispatch(maxPromptsReset());
}, [dispatch]);
return (
<IAISlider
label="Max Prompts"
min={min}
max={sliderMax}
value={maxPrompts}
onChange={handleChange}
sliderNumberInputProps={{ max: inputMax }}
withSliderMarks
withInput
inputReadOnly
withReset
handleReset={handleReset}
/>
);
};
export default ParamDynamicPromptsMaxPrompts;

View File

@ -0,0 +1,50 @@
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store';
export interface DynamicPromptsState {
isEnabled: boolean;
maxPrompts: number;
combinatorial: boolean;
}
export const initialDynamicPromptsState: DynamicPromptsState = {
isEnabled: false,
maxPrompts: 100,
combinatorial: true,
};
const initialState: DynamicPromptsState = initialDynamicPromptsState;
export const dynamicPromptsSlice = createSlice({
name: 'dynamicPrompts',
initialState,
reducers: {
maxPromptsChanged: (state, action: PayloadAction<number>) => {
state.maxPrompts = action.payload;
},
maxPromptsReset: (state) => {
state.maxPrompts = initialDynamicPromptsState.maxPrompts;
},
combinatorialToggled: (state) => {
state.combinatorial = !state.combinatorial;
},
isEnabledToggled: (state) => {
state.isEnabled = !state.isEnabled;
},
},
extraReducers: (builder) => {
//
},
});
export const {
isEnabledToggled,
maxPromptsChanged,
maxPromptsReset,
combinatorialToggled,
} = dynamicPromptsSlice.actions;
export default dynamicPromptsSlice.reducer;
export const dynamicPromptsSelector = (state: RootState) =>
state.dynamicPrompts;

View File

@ -1,5 +1,5 @@
import { RootState } from 'app/store/store';
import { filter, forEach, size } from 'lodash-es';
import { filter } from 'lodash-es';
import { CollectInvocation, ControlNetInvocation } from 'services/api/types';
import { NonNullableGraph } from '../types/types';
import { CONTROL_NET_COLLECT } from './graphBuilders/constants';
@ -19,9 +19,9 @@ export const addControlNetToLinearGraph = (
(c.processorType === 'none' && Boolean(c.controlImage)))
);
// Add ControlNet
if (isControlNetEnabled && validControlNets.length > 0) {
if (size(controlNets) > 1) {
if (isControlNetEnabled && Boolean(validControlNets.length)) {
if (validControlNets.length > 1) {
// We have multiple controlnets, add ControlNet collector
const controlNetIterateNode: CollectInvocation = {
id: CONTROL_NET_COLLECT,
type: 'collect',
@ -36,10 +36,9 @@ export const addControlNetToLinearGraph = (
});
}
forEach(controlNets, (controlNet) => {
validControlNets.forEach((controlNet) => {
const {
controlNetId,
isEnabled,
controlImage,
processedControlImage,
beginStepPct,
@ -50,11 +49,6 @@ export const addControlNetToLinearGraph = (
weight,
} = controlNet;
if (!isEnabled) {
// Skip disabled ControlNets
return;
}
const controlNetNode: ControlNetInvocation = {
id: `control_net_${controlNetId}`,
type: 'controlnet',
@ -82,7 +76,8 @@ export const addControlNetToLinearGraph = (
graph.nodes[controlNetNode.id] = controlNetNode;
if (size(controlNets) > 1) {
if (validControlNets.length > 1) {
// if we have multiple controlnets, link to the collector
graph.edges.push({
source: { node_id: controlNetNode.id, field: 'control' },
destination: {
@ -91,6 +86,7 @@ export const addControlNetToLinearGraph = (
},
});
} else {
// otherwise, link directly to the base node
graph.edges.push({
source: { node_id: controlNetNode.id, field: 'control' },
destination: {

View File

@ -0,0 +1,153 @@
import { RootState } from 'app/store/store';
import { NonNullableGraph } from 'features/nodes/types/types';
import {
DynamicPromptInvocation,
IterateInvocation,
NoiseInvocation,
RandomIntInvocation,
RangeOfSizeInvocation,
} from 'services/api/types';
import {
DYNAMIC_PROMPT,
ITERATE,
NOISE,
POSITIVE_CONDITIONING,
RANDOM_INT,
RANGE_OF_SIZE,
} from './constants';
import { unset } from 'lodash-es';
export const addDynamicPromptsToGraph = (
graph: NonNullableGraph,
state: RootState
): void => {
const { positivePrompt, iterations, seed, shouldRandomizeSeed } =
state.generation;
const {
combinatorial,
isEnabled: isDynamicPromptsEnabled,
maxPrompts,
} = state.dynamicPrompts;
if (isDynamicPromptsEnabled) {
// iteration is handled via dynamic prompts
unset(graph.nodes[POSITIVE_CONDITIONING], 'prompt');
const dynamicPromptNode: DynamicPromptInvocation = {
id: DYNAMIC_PROMPT,
type: 'dynamic_prompt',
max_prompts: maxPrompts,
combinatorial,
prompt: positivePrompt,
};
const iterateNode: IterateInvocation = {
id: ITERATE,
type: 'iterate',
};
graph.nodes[DYNAMIC_PROMPT] = dynamicPromptNode;
graph.nodes[ITERATE] = iterateNode;
// connect dynamic prompts to compel nodes
graph.edges.push(
{
source: {
node_id: DYNAMIC_PROMPT,
field: 'prompt_collection',
},
destination: {
node_id: ITERATE,
field: 'collection',
},
},
{
source: {
node_id: ITERATE,
field: 'item',
},
destination: {
node_id: POSITIVE_CONDITIONING,
field: 'prompt',
},
}
);
if (shouldRandomizeSeed) {
// Random int node to generate the starting seed
const randomIntNode: RandomIntInvocation = {
id: RANDOM_INT,
type: 'rand_int',
};
graph.nodes[RANDOM_INT] = randomIntNode;
// Connect random int to the start of the range of size so the range starts on the random first seed
graph.edges.push({
source: { node_id: RANDOM_INT, field: 'a' },
destination: { node_id: NOISE, field: 'seed' },
});
} else {
// User specified seed, so set the start of the range of size to the seed
(graph.nodes[NOISE] as NoiseInvocation).seed = seed;
}
} else {
const rangeOfSizeNode: RangeOfSizeInvocation = {
id: RANGE_OF_SIZE,
type: 'range_of_size',
size: iterations,
step: 1,
};
const iterateNode: IterateInvocation = {
id: ITERATE,
type: 'iterate',
};
graph.nodes[ITERATE] = iterateNode;
graph.nodes[RANGE_OF_SIZE] = rangeOfSizeNode;
graph.edges.push({
source: {
node_id: RANGE_OF_SIZE,
field: 'collection',
},
destination: {
node_id: ITERATE,
field: 'collection',
},
});
graph.edges.push({
source: {
node_id: ITERATE,
field: 'item',
},
destination: {
node_id: NOISE,
field: 'seed',
},
});
// handle seed
if (shouldRandomizeSeed) {
// Random int node to generate the starting seed
const randomIntNode: RandomIntInvocation = {
id: RANDOM_INT,
type: 'rand_int',
};
graph.nodes[RANDOM_INT] = randomIntNode;
// Connect random int to the start of the range of size so the range starts on the random first seed
graph.edges.push({
source: { node_id: RANDOM_INT, field: 'a' },
destination: { node_id: RANGE_OF_SIZE, field: 'start' },
});
} else {
// User specified seed, so set the start of the range of size to the seed
rangeOfSizeNode.start = seed;
}
}
};

View File

@ -2,6 +2,7 @@ import { RootState } from 'app/store/store';
import {
ImageDTO,
ImageResizeInvocation,
ImageToLatentsInvocation,
RandomIntInvocation,
RangeOfSizeInvocation,
} from 'services/api/types';
@ -10,7 +11,7 @@ import { log } from 'app/logging/useLogger';
import {
ITERATE,
LATENTS_TO_IMAGE,
MODEL_LOADER,
PIPELINE_MODEL_LOADER,
NEGATIVE_CONDITIONING,
NOISE,
POSITIVE_CONDITIONING,
@ -24,6 +25,7 @@ import {
import { set } from 'lodash-es';
import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph';
import { modelIdToPipelineModelField } from '../modelIdToPipelineModelField';
import { addDynamicPromptsToGraph } from './addDynamicPromptsToGraph';
const moduleLog = log.child({ namespace: 'nodes' });
@ -75,31 +77,19 @@ export const buildCanvasImageToImageGraph = (
id: NEGATIVE_CONDITIONING,
prompt: negativePrompt,
},
[RANGE_OF_SIZE]: {
type: 'range_of_size',
id: RANGE_OF_SIZE,
// seed - must be connected manually
// start: 0,
size: iterations,
step: 1,
},
[NOISE]: {
type: 'noise',
id: NOISE,
},
[MODEL_LOADER]: {
[PIPELINE_MODEL_LOADER]: {
type: 'pipeline_model_loader',
id: MODEL_LOADER,
id: PIPELINE_MODEL_LOADER,
model,
},
[LATENTS_TO_IMAGE]: {
type: 'l2i',
id: LATENTS_TO_IMAGE,
},
[ITERATE]: {
type: 'iterate',
id: ITERATE,
},
[LATENTS_TO_LATENTS]: {
type: 'l2l',
id: LATENTS_TO_LATENTS,
@ -120,7 +110,7 @@ export const buildCanvasImageToImageGraph = (
edges: [
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'clip',
},
destination: {
@ -130,7 +120,7 @@ export const buildCanvasImageToImageGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'clip',
},
destination: {
@ -140,7 +130,7 @@ export const buildCanvasImageToImageGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'vae',
},
destination: {
@ -148,26 +138,6 @@ export const buildCanvasImageToImageGraph = (
field: 'vae',
},
},
{
source: {
node_id: RANGE_OF_SIZE,
field: 'collection',
},
destination: {
node_id: ITERATE,
field: 'collection',
},
},
{
source: {
node_id: ITERATE,
field: 'item',
},
destination: {
node_id: NOISE,
field: 'seed',
},
},
{
source: {
node_id: LATENTS_TO_LATENTS,
@ -200,7 +170,7 @@ export const buildCanvasImageToImageGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'vae',
},
destination: {
@ -210,7 +180,7 @@ export const buildCanvasImageToImageGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'unet',
},
destination: {
@ -241,26 +211,6 @@ export const buildCanvasImageToImageGraph = (
],
};
// handle seed
if (shouldRandomizeSeed) {
// Random int node to generate the starting seed
const randomIntNode: RandomIntInvocation = {
id: RANDOM_INT,
type: 'rand_int',
};
graph.nodes[RANDOM_INT] = randomIntNode;
// Connect random int to the start of the range of size so the range starts on the random first seed
graph.edges.push({
source: { node_id: RANDOM_INT, field: 'a' },
destination: { node_id: RANGE_OF_SIZE, field: 'start' },
});
} else {
// User specified seed, so set the start of the range of size to the seed
(graph.nodes[RANGE_OF_SIZE] as RangeOfSizeInvocation).start = seed;
}
// handle `fit`
if (initialImage.width !== width || initialImage.height !== height) {
// The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS`
@ -306,9 +256,9 @@ export const buildCanvasImageToImageGraph = (
});
} else {
// We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly
set(graph.nodes[IMAGE_TO_LATENTS], 'image', {
(graph.nodes[IMAGE_TO_LATENTS] as ImageToLatentsInvocation).image = {
image_name: initialImage.image_name,
});
};
// Pass the image's dimensions to the `NOISE` node
graph.edges.push({
@ -327,7 +277,10 @@ export const buildCanvasImageToImageGraph = (
});
}
// add controlnet
// add dynamic prompts, mutating `graph`
addDynamicPromptsToGraph(graph, state);
// add controlnet, mutating `graph`
addControlNetToLinearGraph(graph, LATENTS_TO_LATENTS, state);
return graph;

View File

@ -9,7 +9,7 @@ import { NonNullableGraph } from 'features/nodes/types/types';
import { log } from 'app/logging/useLogger';
import {
ITERATE,
MODEL_LOADER,
PIPELINE_MODEL_LOADER,
NEGATIVE_CONDITIONING,
POSITIVE_CONDITIONING,
RANDOM_INT,
@ -101,9 +101,9 @@ export const buildCanvasInpaintGraph = (
id: NEGATIVE_CONDITIONING,
prompt: negativePrompt,
},
[MODEL_LOADER]: {
[PIPELINE_MODEL_LOADER]: {
type: 'pipeline_model_loader',
id: MODEL_LOADER,
id: PIPELINE_MODEL_LOADER,
model,
},
[RANGE_OF_SIZE]: {
@ -142,7 +142,7 @@ export const buildCanvasInpaintGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'clip',
},
destination: {
@ -152,7 +152,7 @@ export const buildCanvasInpaintGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'clip',
},
destination: {
@ -162,7 +162,7 @@ export const buildCanvasInpaintGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'unet',
},
destination: {
@ -172,7 +172,7 @@ export const buildCanvasInpaintGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'vae',
},
destination: {

View File

@ -4,7 +4,7 @@ import { RandomIntInvocation, RangeOfSizeInvocation } from 'services/api/types';
import {
ITERATE,
LATENTS_TO_IMAGE,
MODEL_LOADER,
PIPELINE_MODEL_LOADER,
NEGATIVE_CONDITIONING,
NOISE,
POSITIVE_CONDITIONING,
@ -15,6 +15,7 @@ import {
} from './constants';
import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph';
import { modelIdToPipelineModelField } from '../modelIdToPipelineModelField';
import { addDynamicPromptsToGraph } from './addDynamicPromptsToGraph';
/**
* Builds the Canvas tab's Text to Image graph.
@ -62,13 +63,6 @@ export const buildCanvasTextToImageGraph = (
id: NEGATIVE_CONDITIONING,
prompt: negativePrompt,
},
[RANGE_OF_SIZE]: {
type: 'range_of_size',
id: RANGE_OF_SIZE,
// start: 0, // seed - must be connected manually
size: iterations,
step: 1,
},
[NOISE]: {
type: 'noise',
id: NOISE,
@ -82,19 +76,15 @@ export const buildCanvasTextToImageGraph = (
scheduler,
steps,
},
[MODEL_LOADER]: {
[PIPELINE_MODEL_LOADER]: {
type: 'pipeline_model_loader',
id: MODEL_LOADER,
id: PIPELINE_MODEL_LOADER,
model,
},
[LATENTS_TO_IMAGE]: {
type: 'l2i',
id: LATENTS_TO_IMAGE,
},
[ITERATE]: {
type: 'iterate',
id: ITERATE,
},
},
edges: [
{
@ -119,7 +109,7 @@ export const buildCanvasTextToImageGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'clip',
},
destination: {
@ -129,7 +119,7 @@ export const buildCanvasTextToImageGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'clip',
},
destination: {
@ -139,7 +129,7 @@ export const buildCanvasTextToImageGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'unet',
},
destination: {
@ -159,7 +149,7 @@ export const buildCanvasTextToImageGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'vae',
},
destination: {
@ -167,26 +157,6 @@ export const buildCanvasTextToImageGraph = (
field: 'vae',
},
},
{
source: {
node_id: RANGE_OF_SIZE,
field: 'collection',
},
destination: {
node_id: ITERATE,
field: 'collection',
},
},
{
source: {
node_id: ITERATE,
field: 'item',
},
destination: {
node_id: NOISE,
field: 'seed',
},
},
{
source: {
node_id: NOISE,
@ -200,27 +170,10 @@ export const buildCanvasTextToImageGraph = (
],
};
// handle seed
if (shouldRandomizeSeed) {
// Random int node to generate the starting seed
const randomIntNode: RandomIntInvocation = {
id: RANDOM_INT,
type: 'rand_int',
};
// add dynamic prompts, mutating `graph`
addDynamicPromptsToGraph(graph, state);
graph.nodes[RANDOM_INT] = randomIntNode;
// Connect random int to the start of the range of size so the range starts on the random first seed
graph.edges.push({
source: { node_id: RANDOM_INT, field: 'a' },
destination: { node_id: RANGE_OF_SIZE, field: 'start' },
});
} else {
// User specified seed, so set the start of the range of size to the seed
(graph.nodes[RANGE_OF_SIZE] as RangeOfSizeInvocation).start = seed;
}
// add controlnet
// add controlnet, mutating `graph`
addControlNetToLinearGraph(graph, TEXT_TO_LATENTS, state);
return graph;

View File

@ -1,28 +1,24 @@
import { RootState } from 'app/store/store';
import {
ImageResizeInvocation,
RandomIntInvocation,
RangeOfSizeInvocation,
ImageToLatentsInvocation,
} from 'services/api/types';
import { NonNullableGraph } from 'features/nodes/types/types';
import { log } from 'app/logging/useLogger';
import {
ITERATE,
LATENTS_TO_IMAGE,
MODEL_LOADER,
PIPELINE_MODEL_LOADER,
NEGATIVE_CONDITIONING,
NOISE,
POSITIVE_CONDITIONING,
RANDOM_INT,
RANGE_OF_SIZE,
IMAGE_TO_IMAGE_GRAPH,
IMAGE_TO_LATENTS,
LATENTS_TO_LATENTS,
RESIZE,
} from './constants';
import { set } from 'lodash-es';
import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph';
import { modelIdToPipelineModelField } from '../modelIdToPipelineModelField';
import { addDynamicPromptsToGraph } from './addDynamicPromptsToGraph';
const moduleLog = log.child({ namespace: 'nodes' });
@ -44,9 +40,6 @@ export const buildLinearImageToImageGraph = (
shouldFitToWidthHeight,
width,
height,
iterations,
seed,
shouldRandomizeSeed,
} = state.generation;
/**
@ -79,31 +72,19 @@ export const buildLinearImageToImageGraph = (
id: NEGATIVE_CONDITIONING,
prompt: negativePrompt,
},
[RANGE_OF_SIZE]: {
type: 'range_of_size',
id: RANGE_OF_SIZE,
// seed - must be connected manually
// start: 0,
size: iterations,
step: 1,
},
[NOISE]: {
type: 'noise',
id: NOISE,
},
[MODEL_LOADER]: {
[PIPELINE_MODEL_LOADER]: {
type: 'pipeline_model_loader',
id: MODEL_LOADER,
id: PIPELINE_MODEL_LOADER,
model,
},
[LATENTS_TO_IMAGE]: {
type: 'l2i',
id: LATENTS_TO_IMAGE,
},
[ITERATE]: {
type: 'iterate',
id: ITERATE,
},
[LATENTS_TO_LATENTS]: {
type: 'l2l',
id: LATENTS_TO_LATENTS,
@ -124,7 +105,7 @@ export const buildLinearImageToImageGraph = (
edges: [
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'clip',
},
destination: {
@ -134,7 +115,7 @@ export const buildLinearImageToImageGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'clip',
},
destination: {
@ -144,7 +125,7 @@ export const buildLinearImageToImageGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'vae',
},
destination: {
@ -152,26 +133,6 @@ export const buildLinearImageToImageGraph = (
field: 'vae',
},
},
{
source: {
node_id: RANGE_OF_SIZE,
field: 'collection',
},
destination: {
node_id: ITERATE,
field: 'collection',
},
},
{
source: {
node_id: ITERATE,
field: 'item',
},
destination: {
node_id: NOISE,
field: 'seed',
},
},
{
source: {
node_id: LATENTS_TO_LATENTS,
@ -204,7 +165,7 @@ export const buildLinearImageToImageGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'vae',
},
destination: {
@ -214,7 +175,7 @@ export const buildLinearImageToImageGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'unet',
},
destination: {
@ -245,26 +206,6 @@ export const buildLinearImageToImageGraph = (
],
};
// handle seed
if (shouldRandomizeSeed) {
// Random int node to generate the starting seed
const randomIntNode: RandomIntInvocation = {
id: RANDOM_INT,
type: 'rand_int',
};
graph.nodes[RANDOM_INT] = randomIntNode;
// Connect random int to the start of the range of size so the range starts on the random first seed
graph.edges.push({
source: { node_id: RANDOM_INT, field: 'a' },
destination: { node_id: RANGE_OF_SIZE, field: 'start' },
});
} else {
// User specified seed, so set the start of the range of size to the seed
(graph.nodes[RANGE_OF_SIZE] as RangeOfSizeInvocation).start = seed;
}
// handle `fit`
if (
shouldFitToWidthHeight &&
@ -313,9 +254,9 @@ export const buildLinearImageToImageGraph = (
});
} else {
// We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly
set(graph.nodes[IMAGE_TO_LATENTS], 'image', {
(graph.nodes[IMAGE_TO_LATENTS] as ImageToLatentsInvocation).image = {
image_name: initialImage.imageName,
});
};
// Pass the image's dimensions to the `NOISE` node
graph.edges.push({
@ -334,7 +275,10 @@ export const buildLinearImageToImageGraph = (
});
}
// add controlnet
// add dynamic prompts, mutating `graph`
addDynamicPromptsToGraph(graph, state);
// add controlnet, mutating `graph`
addControlNetToLinearGraph(graph, LATENTS_TO_LATENTS, state);
return graph;

View File

@ -1,33 +1,20 @@
import { RootState } from 'app/store/store';
import { NonNullableGraph } from 'features/nodes/types/types';
import {
BaseModelType,
RandomIntInvocation,
RangeOfSizeInvocation,
} from 'services/api/types';
import {
ITERATE,
LATENTS_TO_IMAGE,
MODEL_LOADER,
PIPELINE_MODEL_LOADER,
NEGATIVE_CONDITIONING,
NOISE,
POSITIVE_CONDITIONING,
RANDOM_INT,
RANGE_OF_SIZE,
TEXT_TO_IMAGE_GRAPH,
TEXT_TO_LATENTS,
} from './constants';
import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph';
import { modelIdToPipelineModelField } from '../modelIdToPipelineModelField';
type TextToImageGraphOverrides = {
width: number;
height: number;
};
import { addDynamicPromptsToGraph } from './addDynamicPromptsToGraph';
export const buildLinearTextToImageGraph = (
state: RootState,
overrides?: TextToImageGraphOverrides
state: RootState
): NonNullableGraph => {
const {
positivePrompt,
@ -38,9 +25,6 @@ export const buildLinearTextToImageGraph = (
steps,
width,
height,
iterations,
seed,
shouldRandomizeSeed,
} = state.generation;
const model = modelIdToPipelineModelField(modelId);
@ -68,18 +52,11 @@ export const buildLinearTextToImageGraph = (
id: NEGATIVE_CONDITIONING,
prompt: negativePrompt,
},
[RANGE_OF_SIZE]: {
type: 'range_of_size',
id: RANGE_OF_SIZE,
// start: 0, // seed - must be connected manually
size: iterations,
step: 1,
},
[NOISE]: {
type: 'noise',
id: NOISE,
width: overrides?.width || width,
height: overrides?.height || height,
width,
height,
},
[TEXT_TO_LATENTS]: {
type: 't2l',
@ -88,19 +65,15 @@ export const buildLinearTextToImageGraph = (
scheduler,
steps,
},
[MODEL_LOADER]: {
[PIPELINE_MODEL_LOADER]: {
type: 'pipeline_model_loader',
id: MODEL_LOADER,
id: PIPELINE_MODEL_LOADER,
model,
},
[LATENTS_TO_IMAGE]: {
type: 'l2i',
id: LATENTS_TO_IMAGE,
},
[ITERATE]: {
type: 'iterate',
id: ITERATE,
},
},
edges: [
{
@ -125,7 +98,7 @@ export const buildLinearTextToImageGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'clip',
},
destination: {
@ -135,7 +108,7 @@ export const buildLinearTextToImageGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'clip',
},
destination: {
@ -145,7 +118,7 @@ export const buildLinearTextToImageGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'unet',
},
destination: {
@ -165,7 +138,7 @@ export const buildLinearTextToImageGraph = (
},
{
source: {
node_id: MODEL_LOADER,
node_id: PIPELINE_MODEL_LOADER,
field: 'vae',
},
destination: {
@ -173,26 +146,6 @@ export const buildLinearTextToImageGraph = (
field: 'vae',
},
},
{
source: {
node_id: RANGE_OF_SIZE,
field: 'collection',
},
destination: {
node_id: ITERATE,
field: 'collection',
},
},
{
source: {
node_id: ITERATE,
field: 'item',
},
destination: {
node_id: NOISE,
field: 'seed',
},
},
{
source: {
node_id: NOISE,
@ -206,27 +159,10 @@ export const buildLinearTextToImageGraph = (
],
};
// handle seed
if (shouldRandomizeSeed) {
// Random int node to generate the starting seed
const randomIntNode: RandomIntInvocation = {
id: RANDOM_INT,
type: 'rand_int',
};
// add dynamic prompts, mutating `graph`
addDynamicPromptsToGraph(graph, state);
graph.nodes[RANDOM_INT] = randomIntNode;
// Connect random int to the start of the range of size so the range starts on the random first seed
graph.edges.push({
source: { node_id: RANDOM_INT, field: 'a' },
destination: { node_id: RANGE_OF_SIZE, field: 'start' },
});
} else {
// User specified seed, so set the start of the range of size to the seed
(graph.nodes[RANGE_OF_SIZE] as RangeOfSizeInvocation).start = seed;
}
// add controlnet
// add controlnet, mutating `graph`
addControlNetToLinearGraph(graph, TEXT_TO_LATENTS, state);
return graph;

View File

@ -7,12 +7,13 @@ export const NOISE = 'noise';
export const RANDOM_INT = 'rand_int';
export const RANGE_OF_SIZE = 'range_of_size';
export const ITERATE = 'iterate';
export const MODEL_LOADER = 'pipeline_model_loader';
export const PIPELINE_MODEL_LOADER = 'pipeline_model_loader';
export const IMAGE_TO_LATENTS = 'image_to_latents';
export const LATENTS_TO_LATENTS = 'latents_to_latents';
export const RESIZE = 'resize_image';
export const INPAINT = 'inpaint';
export const CONTROL_NET_COLLECT = 'control_net_collect';
export const DYNAMIC_PROMPT = 'dynamic_prompt';
// friendly graph ids
export const TEXT_TO_IMAGE_GRAPH = 'text_to_image_graph';

View File

@ -1,26 +0,0 @@
import { v4 as uuidv4 } from 'uuid';
import { RootState } from 'app/store/store';
import { CompelInvocation } from 'services/api/types';
import { O } from 'ts-toolbelt';
export const buildCompelNode = (
prompt: string,
state: RootState,
overrides: O.Partial<CompelInvocation, 'deep'> = {}
): CompelInvocation => {
const nodeId = uuidv4();
const { generation } = state;
const { model } = generation;
const compelNode: CompelInvocation = {
id: nodeId,
type: 'compel',
prompt,
model,
};
Object.assign(compelNode, overrides);
return compelNode;
};

View File

@ -1,107 +0,0 @@
import { v4 as uuidv4 } from 'uuid';
import { RootState } from 'app/store/store';
import {
Edge,
ImageToImageInvocation,
TextToImageInvocation,
} from 'services/api/types';
import { O } from 'ts-toolbelt';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
export const buildImg2ImgNode = (
state: RootState,
overrides: O.Partial<ImageToImageInvocation, 'deep'> = {}
): ImageToImageInvocation => {
const nodeId = uuidv4();
const { generation } = state;
const activeTabName = activeTabNameSelector(state);
const {
positivePrompt: prompt,
negativePrompt: negativePrompt,
seed,
steps,
width,
height,
cfgScale,
scheduler,
model,
img2imgStrength: strength,
shouldFitToWidthHeight: fit,
shouldRandomizeSeed,
initialImage,
} = generation;
// const initialImage = initialImageSelector(state);
const imageToImageNode: ImageToImageInvocation = {
id: nodeId,
type: 'img2img',
prompt: `${prompt} [${negativePrompt}]`,
steps,
width,
height,
cfg_scale: cfgScale,
scheduler,
model,
strength,
fit,
};
// on Canvas tab, we do not manually specific init image
if (activeTabName !== 'unifiedCanvas') {
if (!initialImage) {
// TODO: handle this more better
throw 'no initial image';
}
imageToImageNode.image = {
image_name: initialImage.imageName,
};
}
if (!shouldRandomizeSeed) {
imageToImageNode.seed = seed;
}
Object.assign(imageToImageNode, overrides);
return imageToImageNode;
};
type hiresReturnType = {
node: Record<string, ImageToImageInvocation>;
edge: Edge;
};
export const buildHiResNode = (
baseNode: Record<string, TextToImageInvocation>,
strength?: number
): hiresReturnType => {
const nodeId = uuidv4();
const baseNodeId = Object.keys(baseNode)[0];
const baseNodeValues = Object.values(baseNode)[0];
return {
node: {
[nodeId]: {
...baseNodeValues,
id: nodeId,
type: 'img2img',
strength,
fit: true,
},
},
edge: {
source: {
field: 'image',
node_id: baseNodeId,
},
destination: {
field: 'image',
node_id: nodeId,
},
},
};
};

View File

@ -1,48 +0,0 @@
import { v4 as uuidv4 } from 'uuid';
import { RootState } from 'app/store/store';
import { InpaintInvocation } from 'services/api/types';
import { O } from 'ts-toolbelt';
export const buildInpaintNode = (
state: RootState,
overrides: O.Partial<InpaintInvocation, 'deep'> = {}
): InpaintInvocation => {
const nodeId = uuidv4();
const {
positivePrompt: prompt,
negativePrompt: negativePrompt,
seed,
steps,
width,
height,
cfgScale,
scheduler,
model,
img2imgStrength: strength,
shouldFitToWidthHeight: fit,
shouldRandomizeSeed,
} = state.generation;
const inpaintNode: InpaintInvocation = {
id: nodeId,
type: 'inpaint',
prompt: `${prompt} [${negativePrompt}]`,
steps,
width,
height,
cfg_scale: cfgScale,
scheduler,
model,
strength,
fit,
};
if (!shouldRandomizeSeed) {
inpaintNode.seed = seed;
}
Object.assign(inpaintNode, overrides);
return inpaintNode;
};

View File

@ -1,13 +0,0 @@
import { v4 as uuidv4 } from 'uuid';
import { IterateInvocation } from 'services/api/types';
export const buildIterateNode = (): IterateInvocation => {
const nodeId = uuidv4();
return {
id: nodeId,
type: 'iterate',
// collection: [],
// index: 0,
};
};

View File

@ -1,26 +0,0 @@
import { v4 as uuidv4 } from 'uuid';
import { RootState } from 'app/store/store';
import { RandomRangeInvocation, RangeInvocation } from 'services/api/types';
export const buildRangeNode = (
state: RootState
): RangeInvocation | RandomRangeInvocation => {
const nodeId = uuidv4();
const { shouldRandomizeSeed, iterations, seed } = state.generation;
if (shouldRandomizeSeed) {
return {
id: nodeId,
type: 'random_range',
size: iterations,
};
}
return {
id: nodeId,
type: 'range',
start: seed,
stop: seed + iterations,
};
};

View File

@ -1,45 +0,0 @@
import { v4 as uuidv4 } from 'uuid';
import { RootState } from 'app/store/store';
import { TextToImageInvocation } from 'services/api/types';
import { O } from 'ts-toolbelt';
export const buildTxt2ImgNode = (
state: RootState,
overrides: O.Partial<TextToImageInvocation, 'deep'> = {}
): TextToImageInvocation => {
const nodeId = uuidv4();
const { generation } = state;
const {
positivePrompt: prompt,
negativePrompt: negativePrompt,
seed,
steps,
width,
height,
cfgScale: cfg_scale,
scheduler,
shouldRandomizeSeed,
model,
} = generation;
const textToImageNode: NonNullable<TextToImageInvocation> = {
id: nodeId,
type: 'txt2img',
prompt: `${prompt} [${negativePrompt}]`,
steps,
width,
height,
cfg_scale,
scheduler,
model,
};
if (!shouldRandomizeSeed) {
textToImageNode.seed = seed;
}
Object.assign(textToImageNode, overrides);
return textToImageNode;
};

View File

@ -1,4 +1,5 @@
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput';
import IAISlider from 'common/components/IAISlider';
@ -10,27 +11,26 @@ import { uiSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const selector = createSelector(
[generationSelector, configSelector, uiSelector, hotkeysSelector],
(generation, config, ui, hotkeys) => {
const { initial, min, sliderMax, inputMax, fineStep, coarseStep } =
config.sd.iterations;
const { iterations } = generation;
const { shouldUseSliders } = ui;
const selector = createSelector([stateSelector], (state) => {
const { initial, min, sliderMax, inputMax, fineStep, coarseStep } =
state.config.sd.iterations;
const { iterations } = state.generation;
const { shouldUseSliders } = state.ui;
const isDisabled = state.dynamicPrompts.isEnabled;
const step = hotkeys.shift ? fineStep : coarseStep;
const step = state.hotkeys.shift ? fineStep : coarseStep;
return {
iterations,
initial,
min,
sliderMax,
inputMax,
step,
shouldUseSliders,
};
}
);
return {
iterations,
initial,
min,
sliderMax,
inputMax,
step,
shouldUseSliders,
isDisabled,
};
});
const ParamIterations = () => {
const {
@ -41,6 +41,7 @@ const ParamIterations = () => {
inputMax,
step,
shouldUseSliders,
isDisabled,
} = useAppSelector(selector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
@ -58,6 +59,7 @@ const ParamIterations = () => {
return shouldUseSliders ? (
<IAISlider
isDisabled={isDisabled}
label={t('parameters.images')}
step={step}
min={min}
@ -72,6 +74,7 @@ const ParamIterations = () => {
/>
) : (
<IAINumberInput
isDisabled={isDisabled}
label={t('parameters.images')}
step={step}
min={min}

View File

@ -60,6 +60,14 @@ export const initialConfigState: AppConfig = {
fineStep: 0.01,
coarseStep: 0.05,
},
dynamicPrompts: {
maxPrompts: {
initial: 100,
min: 1,
sliderMax: 1000,
inputMax: 10000,
},
},
},
};

View File

@ -8,6 +8,7 @@ import ParamSymmetryCollapse from 'features/parameters/components/Parameters/Sym
import ParamSeamlessCollapse from 'features/parameters/components/Parameters/Seamless/ParamSeamlessCollapse';
import ImageToImageTabCoreParameters from './ImageToImageTabCoreParameters';
import ParamControlNetCollapse from 'features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse';
import ParamDynamicPromptsCollapse from 'features/dynamicPrompts/components/ParamDynamicPromptsCollapse';
const ImageToImageTabParameters = () => {
return (
@ -16,6 +17,7 @@ const ImageToImageTabParameters = () => {
<ParamNegativeConditioning />
<ProcessButtons />
<ImageToImageTabCoreParameters />
<ParamDynamicPromptsCollapse />
<ParamControlNetCollapse />
<ParamVariationCollapse />
<ParamNoiseCollapse />

View File

@ -9,6 +9,7 @@ import ParamHiresCollapse from 'features/parameters/components/Parameters/Hires/
import ParamSeamlessCollapse from 'features/parameters/components/Parameters/Seamless/ParamSeamlessCollapse';
import TextToImageTabCoreParameters from './TextToImageTabCoreParameters';
import ParamControlNetCollapse from 'features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse';
import ParamDynamicPromptsCollapse from 'features/dynamicPrompts/components/ParamDynamicPromptsCollapse';
const TextToImageTabParameters = () => {
return (
@ -17,6 +18,7 @@ const TextToImageTabParameters = () => {
<ParamNegativeConditioning />
<ProcessButtons />
<TextToImageTabCoreParameters />
<ParamDynamicPromptsCollapse />
<ParamControlNetCollapse />
<ParamVariationCollapse />
<ParamNoiseCollapse />

View File

@ -8,6 +8,7 @@ import { memo } from 'react';
import ParamPositiveConditioning from 'features/parameters/components/Parameters/Core/ParamPositiveConditioning';
import ParamNegativeConditioning from 'features/parameters/components/Parameters/Core/ParamNegativeConditioning';
import ParamControlNetCollapse from 'features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse';
import ParamDynamicPromptsCollapse from 'features/dynamicPrompts/components/ParamDynamicPromptsCollapse';
const UnifiedCanvasParameters = () => {
return (
@ -16,6 +17,7 @@ const UnifiedCanvasParameters = () => {
<ParamNegativeConditioning />
<ProcessButtons />
<UnifiedCanvasCoreParameters />
<ParamDynamicPromptsCollapse />
<ParamControlNetCollapse />
<ParamVariationCollapse />
<ParamSymmetryCollapse />

View File

@ -15,6 +15,7 @@ export const imagesApi = api.injectEndpoints({
}
return tags;
},
keepUnusedDataFor: 86400, // 24 hours
}),
}),
});

View File

@ -47,6 +47,15 @@ export type InpaintInvocation = Invocation<'InpaintInvocation'>;
export type ImageResizeInvocation = Invocation<'ImageResizeInvocation'>;
export type RandomIntInvocation = Invocation<'RandomIntInvocation'>;
export type CompelInvocation = Invocation<'CompelInvocation'>;
export type DynamicPromptInvocation = Invocation<'DynamicPromptInvocation'>;
export type NoiseInvocation = Invocation<'NoiseInvocation'>;
export type TextToLatentsInvocation = Invocation<'TextToLatentsInvocation'>;
export type LatentsToLatentsInvocation =
Invocation<'LatentsToLatentsInvocation'>;
export type ImageToLatentsInvocation = Invocation<'ImageToLatentsInvocation'>;
export type LatentsToImageInvocation = Invocation<'LatentsToImageInvocation'>;
export type PipelineModelLoaderInvocation =
Invocation<'PipelineModelLoaderInvocation'>;
// ControlNet Nodes
export type ControlNetInvocation = Invocation<'ControlNetInvocation'>;