From b1b94a3d5646d0549d1c79e09464a4b2d0d5d822 Mon Sep 17 00:00:00 2001 From: user1 Date: Tue, 30 May 2023 00:26:07 -0700 Subject: [PATCH 01/29] Fixed problem with inpainting after controlnet support added to main. Problem was that controlnet support involved adding **kwargs to method calls down in denoising loop, and AddsMaskLatents didn't accept **kwarg arg. So just changed to accept and pass on **kwargs. --- invokeai/backend/stable_diffusion/diffusers_pipeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/backend/stable_diffusion/diffusers_pipeline.py b/invokeai/backend/stable_diffusion/diffusers_pipeline.py index ec2902e4d6..48d73d6649 100644 --- a/invokeai/backend/stable_diffusion/diffusers_pipeline.py +++ b/invokeai/backend/stable_diffusion/diffusers_pipeline.py @@ -75,10 +75,10 @@ class AddsMaskLatents: initial_image_latents: torch.Tensor def __call__( - self, latents: torch.Tensor, t: torch.Tensor, text_embeddings: torch.Tensor + self, latents: torch.Tensor, t: torch.Tensor, text_embeddings: torch.Tensor, **kwargs, ) -> torch.Tensor: model_input = self.add_mask_channels(latents) - return self.forward(model_input, t, text_embeddings) + return self.forward(model_input, t, text_embeddings, **kwargs) def add_mask_channels(self, latents): batch_size = latents.size(0) From 2067757fab740478e50793fcbc753c534d90b35b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 May 2023 10:52:21 +1000 Subject: [PATCH 02/29] feat(ui): enable progress images by default --- invokeai/frontend/web/src/features/ui/store/uiSlice.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index 4893bb3bf6..65a48bc92c 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -19,7 +19,7 @@ export const initialUIState: UIState = { shouldPinGallery: true, shouldShowGallery: true, shouldHidePreview: false, - shouldShowProgressInViewer: false, + shouldShowProgressInViewer: true, schedulers: SCHEDULERS, }; From 33e5ed71809d3f449cd304586ef639234a38c2a8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 May 2023 11:55:58 +1000 Subject: [PATCH 03/29] fix(ui): fix edge case in nodes graph building Inputs with explicit values are validated by pydantic even if they also have a connection (which is the actual value that is used). Fix this by omitting explicit values for inputs that have a connection. --- .../util/graphBuilders/buildNodesGraph.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildNodesGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildNodesGraph.ts index eef7379624..6a700d4813 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildNodesGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildNodesGraph.ts @@ -1,8 +1,9 @@ import { Graph } from 'services/api'; import { v4 as uuidv4 } from 'uuid'; -import { cloneDeep, reduce } from 'lodash-es'; +import { cloneDeep, forEach, omit, reduce, values } from 'lodash-es'; import { RootState } from 'app/store/store'; import { InputFieldValue } from 'features/nodes/types/types'; +import { AnyInvocation } from 'services/events/types'; /** * We need to do special handling for some fields @@ -89,6 +90,24 @@ export const buildNodesGraph = (state: RootState): Graph => { [] ); + /** + * Omit all inputs that have edges connected. + * + * Fixes edge case where the user has connected an input, but also provided an invalid explicit, + * value. + * + * In this edge case, pydantic will invalidate the node based on the invalid explicit value, + * even though the actual value that will be used comes from the connection. + */ + parsedEdges.forEach((edge) => { + const destination_node = parsedNodes[edge.destination.node_id]; + const field = edge.destination.field; + parsedNodes[edge.destination.node_id] = omit( + destination_node, + field + ) as AnyInvocation; + }); + // Assemble! const graph = { id: uuidv4(), From 3afb6a387fb387ec6a85c8afb38af6b9aaec1bd1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 May 2023 15:56:50 +1000 Subject: [PATCH 04/29] chore(ui): regen api --- .../frontend/web/src/services/api/index.ts | 2 + .../web/src/services/api/models/Graph.ts | 4 +- .../api/models/ImageResizeInvocation.ts | 37 +++++++++++++++++++ .../api/models/ImageScaleInvocation.ts | 33 +++++++++++++++++ .../services/api/services/SessionsService.ts | 6 ++- 5 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 invokeai/frontend/web/src/services/api/models/ImageResizeInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ImageScaleInvocation.ts diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index 292cd5ce4d..ff083079f9 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -49,6 +49,8 @@ export type { ImageOutput } from './models/ImageOutput'; export type { ImagePasteInvocation } from './models/ImagePasteInvocation'; export type { ImageProcessorInvocation } from './models/ImageProcessorInvocation'; export type { ImageRecordChanges } from './models/ImageRecordChanges'; +export type { ImageResizeInvocation } from './models/ImageResizeInvocation'; +export type { ImageScaleInvocation } from './models/ImageScaleInvocation'; export type { ImageToImageInvocation } from './models/ImageToImageInvocation'; export type { ImageToLatentsInvocation } from './models/ImageToLatentsInvocation'; export type { ImageUrlsDTO } from './models/ImageUrlsDTO'; diff --git a/invokeai/frontend/web/src/services/api/models/Graph.ts b/invokeai/frontend/web/src/services/api/models/Graph.ts index af8a3ed0e6..e89e815ab2 100644 --- a/invokeai/frontend/web/src/services/api/models/Graph.ts +++ b/invokeai/frontend/web/src/services/api/models/Graph.ts @@ -22,6 +22,8 @@ import type { ImageLerpInvocation } from './ImageLerpInvocation'; import type { ImageMultiplyInvocation } from './ImageMultiplyInvocation'; import type { ImagePasteInvocation } from './ImagePasteInvocation'; import type { ImageProcessorInvocation } from './ImageProcessorInvocation'; +import type { ImageResizeInvocation } from './ImageResizeInvocation'; +import type { ImageScaleInvocation } from './ImageScaleInvocation'; import type { ImageToImageInvocation } from './ImageToImageInvocation'; import type { ImageToLatentsInvocation } from './ImageToLatentsInvocation'; import type { InfillColorInvocation } from './InfillColorInvocation'; @@ -67,7 +69,7 @@ export type Graph = { /** * The nodes in this graph */ - nodes?: Record; + nodes?: Record; /** * The connections between nodes and their fields in this graph */ diff --git a/invokeai/frontend/web/src/services/api/models/ImageResizeInvocation.ts b/invokeai/frontend/web/src/services/api/models/ImageResizeInvocation.ts new file mode 100644 index 0000000000..3b096c83b7 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageResizeInvocation.ts @@ -0,0 +1,37 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Resizes an image to specific dimensions + */ +export type ImageResizeInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; + type?: 'img_resize'; + /** + * The image to resize + */ + image?: ImageField; + /** + * The width to resize to (px) + */ + width: number; + /** + * The height to resize to (px) + */ + height: number; + /** + * The resampling mode + */ + resample_mode?: 'nearest' | 'box' | 'bilinear' | 'hamming' | 'bicubic' | 'lanczos'; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ImageScaleInvocation.ts b/invokeai/frontend/web/src/services/api/models/ImageScaleInvocation.ts new file mode 100644 index 0000000000..bf4da28a4a --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageScaleInvocation.ts @@ -0,0 +1,33 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Scales an image by a factor + */ +export type ImageScaleInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; + type?: 'img_scale'; + /** + * The image to scale + */ + image?: ImageField; + /** + * The factor by which to scale the image + */ + scale_factor: number; + /** + * The resampling mode + */ + resample_mode?: 'nearest' | 'box' | 'bilinear' | 'hamming' | 'bicubic' | 'lanczos'; +}; + diff --git a/invokeai/frontend/web/src/services/api/services/SessionsService.ts b/invokeai/frontend/web/src/services/api/services/SessionsService.ts index de46d8fd3e..6ae6783313 100644 --- a/invokeai/frontend/web/src/services/api/services/SessionsService.ts +++ b/invokeai/frontend/web/src/services/api/services/SessionsService.ts @@ -23,6 +23,8 @@ import type { ImageLerpInvocation } from '../models/ImageLerpInvocation'; import type { ImageMultiplyInvocation } from '../models/ImageMultiplyInvocation'; import type { ImagePasteInvocation } from '../models/ImagePasteInvocation'; import type { ImageProcessorInvocation } from '../models/ImageProcessorInvocation'; +import type { ImageResizeInvocation } from '../models/ImageResizeInvocation'; +import type { ImageScaleInvocation } from '../models/ImageScaleInvocation'; import type { ImageToImageInvocation } from '../models/ImageToImageInvocation'; import type { ImageToLatentsInvocation } from '../models/ImageToLatentsInvocation'; import type { InfillColorInvocation } from '../models/InfillColorInvocation'; @@ -169,7 +171,7 @@ export class SessionsService { * The id of the session */ sessionId: string, - requestBody: (LoadImageInvocation | ShowImageInvocation | ImageCropInvocation | ImagePasteInvocation | MaskFromAlphaInvocation | ImageMultiplyInvocation | ImageChannelInvocation | ImageConvertInvocation | ImageBlurInvocation | ImageLerpInvocation | ImageInverseLerpInvocation | ControlNetInvocation | ImageProcessorInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | ParamFloatInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | CannyImageProcessorInvocation | HedImageprocessorInvocation | LineartImageProcessorInvocation | LineartAnimeImageProcessorInvocation | OpenposeImageProcessorInvocation | MidasDepthImageProcessorInvocation | NormalbaeImageProcessorInvocation | MlsdImageProcessorInvocation | PidiImageProcessorInvocation | ContentShuffleImageProcessorInvocation | ZoeDepthImageProcessorInvocation | MediapipeFaceProcessorInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), + requestBody: (LoadImageInvocation | ShowImageInvocation | ImageCropInvocation | ImagePasteInvocation | MaskFromAlphaInvocation | ImageMultiplyInvocation | ImageChannelInvocation | ImageConvertInvocation | ImageBlurInvocation | ImageResizeInvocation | ImageScaleInvocation | ImageLerpInvocation | ImageInverseLerpInvocation | ControlNetInvocation | ImageProcessorInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | ParamFloatInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | CannyImageProcessorInvocation | HedImageprocessorInvocation | LineartImageProcessorInvocation | LineartAnimeImageProcessorInvocation | OpenposeImageProcessorInvocation | MidasDepthImageProcessorInvocation | NormalbaeImageProcessorInvocation | MlsdImageProcessorInvocation | PidiImageProcessorInvocation | ContentShuffleImageProcessorInvocation | ZoeDepthImageProcessorInvocation | MediapipeFaceProcessorInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), }): CancelablePromise { return __request(OpenAPI, { method: 'POST', @@ -206,7 +208,7 @@ export class SessionsService { * The path to the node in the graph */ nodePath: string, - requestBody: (LoadImageInvocation | ShowImageInvocation | ImageCropInvocation | ImagePasteInvocation | MaskFromAlphaInvocation | ImageMultiplyInvocation | ImageChannelInvocation | ImageConvertInvocation | ImageBlurInvocation | ImageLerpInvocation | ImageInverseLerpInvocation | ControlNetInvocation | ImageProcessorInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | ParamFloatInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | CannyImageProcessorInvocation | HedImageprocessorInvocation | LineartImageProcessorInvocation | LineartAnimeImageProcessorInvocation | OpenposeImageProcessorInvocation | MidasDepthImageProcessorInvocation | NormalbaeImageProcessorInvocation | MlsdImageProcessorInvocation | PidiImageProcessorInvocation | ContentShuffleImageProcessorInvocation | ZoeDepthImageProcessorInvocation | MediapipeFaceProcessorInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), + requestBody: (LoadImageInvocation | ShowImageInvocation | ImageCropInvocation | ImagePasteInvocation | MaskFromAlphaInvocation | ImageMultiplyInvocation | ImageChannelInvocation | ImageConvertInvocation | ImageBlurInvocation | ImageResizeInvocation | ImageScaleInvocation | ImageLerpInvocation | ImageInverseLerpInvocation | ControlNetInvocation | ImageProcessorInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | ParamFloatInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | CannyImageProcessorInvocation | HedImageprocessorInvocation | LineartImageProcessorInvocation | LineartAnimeImageProcessorInvocation | OpenposeImageProcessorInvocation | MidasDepthImageProcessorInvocation | NormalbaeImageProcessorInvocation | MlsdImageProcessorInvocation | PidiImageProcessorInvocation | ContentShuffleImageProcessorInvocation | ZoeDepthImageProcessorInvocation | MediapipeFaceProcessorInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), }): CancelablePromise { return __request(OpenAPI, { method: 'PUT', From c22c6ca135ddf617cf5d3d053bd767dd651d2ce8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 May 2023 15:57:50 +1000 Subject: [PATCH 05/29] fix(ui): fix img2img fit --- .../graphBuilders/buildImageToImageGraph.ts | 306 +++++++++++++++++- .../graphBuilders/buildTextToImageGraph.ts | 215 +++++++++++- .../nodes/util/nodeBuilders/addNoiseNodes.ts | 208 ------------ 3 files changed, 504 insertions(+), 225 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/nodes/util/nodeBuilders/addNoiseNodes.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts index bd3d8a5460..728d75c2ae 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts @@ -2,21 +2,31 @@ import { RootState } from 'app/store/store'; import { CompelInvocation, Graph, + ImageResizeInvocation, ImageToLatentsInvocation, + IterateInvocation, LatentsToImageInvocation, LatentsToLatentsInvocation, + NoiseInvocation, + RandomIntInvocation, + RangeOfSizeInvocation, } from 'services/api'; import { NonNullableGraph } from 'features/nodes/types/types'; -import { addNoiseNodes } from '../nodeBuilders/addNoiseNodes'; import { log } from 'app/logging/useLogger'; +import { set } from 'lodash-es'; -const moduleLog = log.child({ namespace: 'buildImageToImageGraph' }); +const moduleLog = log.child({ namespace: 'nodes' }); const POSITIVE_CONDITIONING = 'positive_conditioning'; const NEGATIVE_CONDITIONING = 'negative_conditioning'; const IMAGE_TO_LATENTS = 'image_to_latents'; const LATENTS_TO_LATENTS = 'latents_to_latents'; const LATENTS_TO_IMAGE = 'latents_to_image'; +const RESIZE = 'resize_image'; +const NOISE = 'noise'; +const RANDOM_INT = 'rand_int'; +const RANGE_OF_SIZE = 'range_of_size'; +const ITERATE = 'iterate'; /** * Builds the Image to Image tab graph. @@ -31,6 +41,12 @@ export const buildImageToImageGraph = (state: RootState): Graph => { steps, initialImage, img2imgStrength: strength, + shouldFitToWidthHeight, + width, + height, + iterations, + seed, + shouldRandomizeSeed, } = state.generation; if (!initialImage) { @@ -38,12 +54,12 @@ export const buildImageToImageGraph = (state: RootState): Graph => { throw new Error('No initial image found in state'); } - let graph: NonNullableGraph = { + const graph: NonNullableGraph = { nodes: {}, edges: [], }; - // Create the conditioning, t2l and l2i nodes + // Create the positive conditioning (prompt) node const positiveConditioningNode: CompelInvocation = { id: POSITIVE_CONDITIONING, type: 'compel', @@ -51,6 +67,7 @@ export const buildImageToImageGraph = (state: RootState): Graph => { model, }; + // Negative conditioning const negativeConditioningNode: CompelInvocation = { id: NEGATIVE_CONDITIONING, type: 'compel', @@ -58,16 +75,15 @@ export const buildImageToImageGraph = (state: RootState): Graph => { model, }; + // This will encode the raster image to latents - but it may get its `image` from a resize node, + // so we do not set its `image` property yet const imageToLatentsNode: ImageToLatentsInvocation = { id: IMAGE_TO_LATENTS, type: 'i2l', model, - image: { - image_name: initialImage?.image_name, - image_origin: initialImage?.image_origin, - }, }; + // This does the actual img2img inference const latentsToLatentsNode: LatentsToLatentsInvocation = { id: LATENTS_TO_LATENTS, type: 'l2l', @@ -78,20 +94,21 @@ export const buildImageToImageGraph = (state: RootState): Graph => { strength, }; + // Finally we decode the latents back to an image const latentsToImageNode: LatentsToImageInvocation = { id: LATENTS_TO_IMAGE, type: 'l2i', model, }; - // Add to the graph + // Add all those nodes to the graph graph.nodes[POSITIVE_CONDITIONING] = positiveConditioningNode; graph.nodes[NEGATIVE_CONDITIONING] = negativeConditioningNode; graph.nodes[IMAGE_TO_LATENTS] = imageToLatentsNode; graph.nodes[LATENTS_TO_LATENTS] = latentsToLatentsNode; graph.nodes[LATENTS_TO_IMAGE] = latentsToImageNode; - // Connect them + // Connect the prompt nodes to the imageToLatents node graph.edges.push({ source: { node_id: POSITIVE_CONDITIONING, field: 'conditioning' }, destination: { @@ -99,7 +116,6 @@ export const buildImageToImageGraph = (state: RootState): Graph => { field: 'positive_conditioning', }, }); - graph.edges.push({ source: { node_id: NEGATIVE_CONDITIONING, field: 'conditioning' }, destination: { @@ -108,6 +124,7 @@ export const buildImageToImageGraph = (state: RootState): Graph => { }, }); + // Connect the image-encoding node graph.edges.push({ source: { node_id: IMAGE_TO_LATENTS, field: 'latents' }, destination: { @@ -116,6 +133,7 @@ export const buildImageToImageGraph = (state: RootState): Graph => { }, }); + // Connect the image-decoding node graph.edges.push({ source: { node_id: LATENTS_TO_LATENTS, field: 'latents' }, destination: { @@ -124,8 +142,270 @@ export const buildImageToImageGraph = (state: RootState): Graph => { }, }); - // Create and add the noise nodes - graph = addNoiseNodes(graph, latentsToLatentsNode.id, state); + /** + * Now we need to handle iterations and random seeds. There are four possible scenarios: + * - Single iteration, explicit seed + * - Single iteration, random seed + * - Multiple iterations, explicit seed + * - Multiple iterations, random seed + * + * They all have different graphs and connections. + */ + + // Single iteration, explicit seed + if (!shouldRandomizeSeed && iterations === 1) { + // Noise node using the explicit seed + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + seed: seed, + }; + + graph.nodes[NOISE] = noiseNode; + + // Connect noise to l2l + graph.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: LATENTS_TO_LATENTS, + field: 'noise', + }, + }); + } + + // Single iteration, random seed + if (shouldRandomizeSeed && iterations === 1) { + // Random int node to generate the seed + const randomIntNode: RandomIntInvocation = { + id: RANDOM_INT, + type: 'rand_int', + }; + + // Noise node without any seed + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + }; + + graph.nodes[RANDOM_INT] = randomIntNode; + graph.nodes[NOISE] = noiseNode; + + // Connect random int to the seed of the noise node + graph.edges.push({ + source: { node_id: RANDOM_INT, field: 'a' }, + destination: { + node_id: NOISE, + field: 'seed', + }, + }); + + // Connect noise to l2l + graph.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: LATENTS_TO_LATENTS, + field: 'noise', + }, + }); + } + + // Multiple iterations, explicit seed + if (!shouldRandomizeSeed && iterations > 1) { + // Range of size node to generate `iterations` count of seeds - range of size generates a collection + // of ints from `start` to `start + size`. The `start` is the seed, and the `size` is the number of + // iterations. + const rangeOfSizeNode: RangeOfSizeInvocation = { + id: RANGE_OF_SIZE, + type: 'range_of_size', + start: seed, + size: iterations, + }; + + // Iterate node to iterate over the seeds generated by the range of size node + const iterateNode: IterateInvocation = { + id: ITERATE, + type: 'iterate', + }; + + // Noise node without any seed + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + }; + + // Adding to the graph + graph.nodes[RANGE_OF_SIZE] = rangeOfSizeNode; + graph.nodes[ITERATE] = iterateNode; + graph.nodes[NOISE] = noiseNode; + + // Connect range of size to iterate + graph.edges.push({ + source: { node_id: RANGE_OF_SIZE, field: 'collection' }, + destination: { + node_id: ITERATE, + field: 'collection', + }, + }); + + // Connect iterate to noise + graph.edges.push({ + source: { + node_id: ITERATE, + field: 'item', + }, + destination: { + node_id: NOISE, + field: 'seed', + }, + }); + + // Connect noise to l2l + graph.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: LATENTS_TO_LATENTS, + field: 'noise', + }, + }); + } + + // Multiple iterations, random seed + if (shouldRandomizeSeed && iterations > 1) { + // Random int node to generate the seed + const randomIntNode: RandomIntInvocation = { + id: RANDOM_INT, + type: 'rand_int', + }; + + // Range of size node to generate `iterations` count of seeds - range of size generates a collection + const rangeOfSizeNode: RangeOfSizeInvocation = { + id: RANGE_OF_SIZE, + type: 'range_of_size', + size: iterations, + }; + + // Iterate node to iterate over the seeds generated by the range of size node + const iterateNode: IterateInvocation = { + id: ITERATE, + type: 'iterate', + }; + + // Noise node without any seed + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + width, + height, + }; + + // Adding to the graph + graph.nodes[RANDOM_INT] = randomIntNode; + graph.nodes[RANGE_OF_SIZE] = rangeOfSizeNode; + graph.nodes[ITERATE] = iterateNode; + graph.nodes[NOISE] = noiseNode; + + // 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' }, + }); + + // Connect range of size to iterate + graph.edges.push({ + source: { node_id: RANGE_OF_SIZE, field: 'collection' }, + destination: { + node_id: ITERATE, + field: 'collection', + }, + }); + + // Connect iterate to noise + graph.edges.push({ + source: { + node_id: ITERATE, + field: 'item', + }, + destination: { + node_id: NOISE, + field: 'seed', + }, + }); + + // Connect noise to l2l + graph.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: LATENTS_TO_LATENTS, + field: 'noise', + }, + }); + } + + if (shouldFitToWidthHeight) { + // The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS` + + // Create a resize node, explicitly setting its image + const resizeNode: ImageResizeInvocation = { + id: RESIZE, + type: 'img_resize', + image: { + image_name: initialImage.image_name, + image_origin: initialImage.image_origin, + }, + height, + width, + }; + + graph.nodes[RESIZE] = resizeNode; + + // The `RESIZE` node then passes its image to `IMAGE_TO_LATENTS` + graph.edges.push({ + source: { node_id: RESIZE, field: 'image' }, + destination: { + node_id: IMAGE_TO_LATENTS, + field: 'image', + }, + }); + + // The `RESIZE` node also passes its width and height to `NOISE` + graph.edges.push({ + source: { node_id: RESIZE, field: 'width' }, + destination: { + node_id: NOISE, + field: 'width', + }, + }); + + graph.edges.push({ + source: { node_id: RESIZE, field: 'height' }, + destination: { + node_id: NOISE, + field: 'height', + }, + }); + } else { + // We are not resizing, so we need to set the image on the `LATENTS_TO_LATENTS` node explicitly + set(graph.nodes[LATENTS_TO_LATENTS], 'image', { + image_name: initialImage.image_name, + image_origin: initialImage.image_origin, + }); + + // Pass the image's dimensions to the `NOISE` node + graph.edges.push({ + source: { node_id: IMAGE_TO_LATENTS, field: 'width' }, + destination: { + node_id: NOISE, + field: 'width', + }, + }); + graph.edges.push({ + source: { node_id: IMAGE_TO_LATENTS, field: 'height' }, + destination: { + node_id: NOISE, + field: 'height', + }, + }); + } return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts index 51f89e8f74..737d8d5b61 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts @@ -2,16 +2,23 @@ import { RootState } from 'app/store/store'; import { CompelInvocation, Graph, + IterateInvocation, LatentsToImageInvocation, + NoiseInvocation, + RandomIntInvocation, + RangeOfSizeInvocation, TextToLatentsInvocation, } from 'services/api'; import { NonNullableGraph } from 'features/nodes/types/types'; -import { addNoiseNodes } from '../nodeBuilders/addNoiseNodes'; const POSITIVE_CONDITIONING = 'positive_conditioning'; const NEGATIVE_CONDITIONING = 'negative_conditioning'; const TEXT_TO_LATENTS = 'text_to_latents'; const LATENTS_TO_IMAGE = 'latents_to_image'; +const NOISE = 'noise'; +const RANDOM_INT = 'rand_int'; +const RANGE_OF_SIZE = 'range_of_size'; +const ITERATE = 'iterate'; /** * Builds the Text to Image tab graph. @@ -24,9 +31,14 @@ export const buildTextToImageGraph = (state: RootState): Graph => { cfgScale: cfg_scale, scheduler, steps, + width, + height, + iterations, + seed, + shouldRandomizeSeed, } = state.generation; - let graph: NonNullableGraph = { + const graph: NonNullableGraph = { nodes: {}, edges: [], }; @@ -92,8 +104,203 @@ export const buildTextToImageGraph = (state: RootState): Graph => { }, }); - // Create and add the noise nodes - graph = addNoiseNodes(graph, TEXT_TO_LATENTS, state); + /** + * Now we need to handle iterations and random seeds. There are four possible scenarios: + * - Single iteration, explicit seed + * - Single iteration, random seed + * - Multiple iterations, explicit seed + * - Multiple iterations, random seed + * + * They all have different graphs and connections. + */ + // Single iteration, explicit seed + if (!shouldRandomizeSeed && iterations === 1) { + // Noise node using the explicit seed + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + seed: seed, + }; + + graph.nodes[NOISE] = noiseNode; + + // Connect noise to l2l + graph.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: TEXT_TO_LATENTS, + field: 'noise', + }, + }); + } + + // Single iteration, random seed + if (shouldRandomizeSeed && iterations === 1) { + // Random int node to generate the seed + const randomIntNode: RandomIntInvocation = { + id: RANDOM_INT, + type: 'rand_int', + }; + + // Noise node without any seed + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + }; + + graph.nodes[RANDOM_INT] = randomIntNode; + graph.nodes[NOISE] = noiseNode; + + // Connect random int to the seed of the noise node + graph.edges.push({ + source: { node_id: RANDOM_INT, field: 'a' }, + destination: { + node_id: NOISE, + field: 'seed', + }, + }); + + // Connect noise to t2l + graph.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: TEXT_TO_LATENTS, + field: 'noise', + }, + }); + } + + // Multiple iterations, explicit seed + if (!shouldRandomizeSeed && iterations > 1) { + // Range of size node to generate `iterations` count of seeds - range of size generates a collection + // of ints from `start` to `start + size`. The `start` is the seed, and the `size` is the number of + // iterations. + const rangeOfSizeNode: RangeOfSizeInvocation = { + id: RANGE_OF_SIZE, + type: 'range_of_size', + start: seed, + size: iterations, + }; + + // Iterate node to iterate over the seeds generated by the range of size node + const iterateNode: IterateInvocation = { + id: ITERATE, + type: 'iterate', + }; + + // Noise node without any seed + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + }; + + // Adding to the graph + graph.nodes[RANGE_OF_SIZE] = rangeOfSizeNode; + graph.nodes[ITERATE] = iterateNode; + graph.nodes[NOISE] = noiseNode; + + // Connect range of size to iterate + graph.edges.push({ + source: { node_id: RANGE_OF_SIZE, field: 'collection' }, + destination: { + node_id: ITERATE, + field: 'collection', + }, + }); + + // Connect iterate to noise + graph.edges.push({ + source: { + node_id: ITERATE, + field: 'item', + }, + destination: { + node_id: NOISE, + field: 'seed', + }, + }); + + // Connect noise to t2l + graph.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: TEXT_TO_LATENTS, + field: 'noise', + }, + }); + } + + // Multiple iterations, random seed + if (shouldRandomizeSeed && iterations > 1) { + // Random int node to generate the seed + const randomIntNode: RandomIntInvocation = { + id: RANDOM_INT, + type: 'rand_int', + }; + + // Range of size node to generate `iterations` count of seeds - range of size generates a collection + const rangeOfSizeNode: RangeOfSizeInvocation = { + id: RANGE_OF_SIZE, + type: 'range_of_size', + size: iterations, + }; + + // Iterate node to iterate over the seeds generated by the range of size node + const iterateNode: IterateInvocation = { + id: ITERATE, + type: 'iterate', + }; + + // Noise node without any seed + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + width, + height, + }; + + // Adding to the graph + graph.nodes[RANDOM_INT] = randomIntNode; + graph.nodes[RANGE_OF_SIZE] = rangeOfSizeNode; + graph.nodes[ITERATE] = iterateNode; + graph.nodes[NOISE] = noiseNode; + + // 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' }, + }); + + // Connect range of size to iterate + graph.edges.push({ + source: { node_id: RANGE_OF_SIZE, field: 'collection' }, + destination: { + node_id: ITERATE, + field: 'collection', + }, + }); + + // Connect iterate to noise + graph.edges.push({ + source: { + node_id: ITERATE, + field: 'item', + }, + destination: { + node_id: NOISE, + field: 'seed', + }, + }); + + // Connect noise to t2l + graph.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: TEXT_TO_LATENTS, + field: 'noise', + }, + }); + } return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/addNoiseNodes.ts b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/addNoiseNodes.ts deleted file mode 100644 index ba3d4d8168..0000000000 --- a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/addNoiseNodes.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { RootState } from 'app/store/store'; -import { - IterateInvocation, - NoiseInvocation, - RandomIntInvocation, - RangeOfSizeInvocation, -} from 'services/api'; -import { NonNullableGraph } from 'features/nodes/types/types'; -import { cloneDeep } from 'lodash-es'; - -const NOISE = 'noise'; -const RANDOM_INT = 'rand_int'; -const RANGE_OF_SIZE = 'range_of_size'; -const ITERATE = 'iterate'; -/** - * Adds the appropriate noise nodes to a linear UI t2l or l2l graph. - * - * @param graph The graph to add the noise nodes to. - * @param baseNodeId The id of the base node to connect the noise nodes to. - * @param state The app state.. - */ -export const addNoiseNodes = ( - graph: NonNullableGraph, - baseNodeId: string, - state: RootState -): NonNullableGraph => { - const graphClone = cloneDeep(graph); - - // Create and add the noise nodes - const { width, height, seed, iterations, shouldRandomizeSeed } = - state.generation; - - // Single iteration, explicit seed - if (!shouldRandomizeSeed && iterations === 1) { - const noiseNode: NoiseInvocation = { - id: NOISE, - type: 'noise', - seed: seed, - width, - height, - }; - - graphClone.nodes[NOISE] = noiseNode; - - // Connect them - graphClone.edges.push({ - source: { node_id: NOISE, field: 'noise' }, - destination: { - node_id: baseNodeId, - field: 'noise', - }, - }); - } - - // Single iteration, random seed - if (shouldRandomizeSeed && iterations === 1) { - // TODO: This assumes the `high` value is the max seed value - const randomIntNode: RandomIntInvocation = { - id: RANDOM_INT, - type: 'rand_int', - }; - - const noiseNode: NoiseInvocation = { - id: NOISE, - type: 'noise', - width, - height, - }; - - graphClone.nodes[RANDOM_INT] = randomIntNode; - graphClone.nodes[NOISE] = noiseNode; - - graphClone.edges.push({ - source: { node_id: RANDOM_INT, field: 'a' }, - destination: { - node_id: NOISE, - field: 'seed', - }, - }); - - graphClone.edges.push({ - source: { node_id: NOISE, field: 'noise' }, - destination: { - node_id: baseNodeId, - field: 'noise', - }, - }); - } - - // Multiple iterations, explicit seed - if (!shouldRandomizeSeed && iterations > 1) { - const rangeOfSizeNode: RangeOfSizeInvocation = { - id: RANGE_OF_SIZE, - type: 'range_of_size', - start: seed, - size: iterations, - }; - - const iterateNode: IterateInvocation = { - id: ITERATE, - type: 'iterate', - }; - - const noiseNode: NoiseInvocation = { - id: NOISE, - type: 'noise', - width, - height, - }; - - graphClone.nodes[RANGE_OF_SIZE] = rangeOfSizeNode; - graphClone.nodes[ITERATE] = iterateNode; - graphClone.nodes[NOISE] = noiseNode; - - graphClone.edges.push({ - source: { node_id: RANGE_OF_SIZE, field: 'collection' }, - destination: { - node_id: ITERATE, - field: 'collection', - }, - }); - - graphClone.edges.push({ - source: { - node_id: ITERATE, - field: 'item', - }, - destination: { - node_id: NOISE, - field: 'seed', - }, - }); - - graphClone.edges.push({ - source: { node_id: NOISE, field: 'noise' }, - destination: { - node_id: baseNodeId, - field: 'noise', - }, - }); - } - - // Multiple iterations, random seed - if (shouldRandomizeSeed && iterations > 1) { - // TODO: This assumes the `high` value is the max seed value - const randomIntNode: RandomIntInvocation = { - id: RANDOM_INT, - type: 'rand_int', - }; - - const rangeOfSizeNode: RangeOfSizeInvocation = { - id: RANGE_OF_SIZE, - type: 'range_of_size', - size: iterations, - }; - - const iterateNode: IterateInvocation = { - id: ITERATE, - type: 'iterate', - }; - - const noiseNode: NoiseInvocation = { - id: NOISE, - type: 'noise', - width, - height, - }; - - graphClone.nodes[RANDOM_INT] = randomIntNode; - graphClone.nodes[RANGE_OF_SIZE] = rangeOfSizeNode; - graphClone.nodes[ITERATE] = iterateNode; - graphClone.nodes[NOISE] = noiseNode; - - graphClone.edges.push({ - source: { node_id: RANDOM_INT, field: 'a' }, - destination: { node_id: RANGE_OF_SIZE, field: 'start' }, - }); - - graphClone.edges.push({ - source: { node_id: RANGE_OF_SIZE, field: 'collection' }, - destination: { - node_id: ITERATE, - field: 'collection', - }, - }); - - graphClone.edges.push({ - source: { - node_id: ITERATE, - field: 'item', - }, - destination: { - node_id: NOISE, - field: 'seed', - }, - }); - - graphClone.edges.push({ - source: { node_id: NOISE, field: 'noise' }, - destination: { - node_id: baseNodeId, - field: 'noise', - }, - }); - } - - return graphClone; -}; From 1a3fd05b815c645ee3157e264eb96ad41de98951 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 May 2023 16:32:35 +1000 Subject: [PATCH 06/29] fix(ui): fix canvas bbox autoscale --- .../features/canvas/util/createMaskStage.ts | 7 ++++--- .../src/features/canvas/util/getCanvasData.ts | 4 +++- .../util/graphBuilders/buildCanvasGraph.ts | 21 ++++++++++++------- .../util/nodeBuilders/buildInpaintNode.ts | 19 +---------------- 4 files changed, 21 insertions(+), 30 deletions(-) diff --git a/invokeai/frontend/web/src/features/canvas/util/createMaskStage.ts b/invokeai/frontend/web/src/features/canvas/util/createMaskStage.ts index 96ac592711..b417b3a786 100644 --- a/invokeai/frontend/web/src/features/canvas/util/createMaskStage.ts +++ b/invokeai/frontend/web/src/features/canvas/util/createMaskStage.ts @@ -9,7 +9,8 @@ import { IRect } from 'konva/lib/types'; */ const createMaskStage = async ( lines: CanvasMaskLine[], - boundingBox: IRect + boundingBox: IRect, + shouldInvertMask: boolean ): Promise => { // create an offscreen canvas and add the mask to it const { width, height } = boundingBox; @@ -29,7 +30,7 @@ const createMaskStage = async ( baseLayer.add( new Konva.Rect({ ...boundingBox, - fill: 'white', + fill: shouldInvertMask ? 'black' : 'white', }) ); @@ -37,7 +38,7 @@ const createMaskStage = async ( maskLayer.add( new Konva.Line({ points: line.points, - stroke: 'black', + stroke: shouldInvertMask ? 'white' : 'black', strokeWidth: line.strokeWidth * 2, tension: 0, lineCap: 'round', diff --git a/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts b/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts index 21a33aa349..d0190878e2 100644 --- a/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts +++ b/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts @@ -25,6 +25,7 @@ export const getCanvasData = async (state: RootState) => { boundingBoxCoordinates, boundingBoxDimensions, isMaskEnabled, + shouldPreserveMaskedArea, } = state.canvas; const boundingBox = { @@ -58,7 +59,8 @@ export const getCanvasData = async (state: RootState) => { // For the mask layer, use the normal boundingBox const maskStage = await createMaskStage( isMaskEnabled ? objects.filter(isCanvasMaskLine) : [], // only include mask lines, and only if mask is enabled - boundingBox + boundingBox, + shouldPreserveMaskedArea ); const maskBlob = await konvaNodeToBlob(maskStage, boundingBox); const maskImageData = await konvaNodeToImageData(maskStage, boundingBox); diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasGraph.ts index 2e741443cf..2d23b882ea 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasGraph.ts @@ -16,7 +16,7 @@ import { buildEdges } from '../edgeBuilders/buildEdges'; import { log } from 'app/logging/useLogger'; import { buildInpaintNode } from '../nodeBuilders/buildInpaintNode'; -const moduleLog = log.child({ namespace: 'buildCanvasGraph' }); +const moduleLog = log.child({ namespace: 'nodes' }); const buildBaseNode = ( nodeType: 'txt2img' | 'img2img' | 'inpaint' | 'outpaint', @@ -80,18 +80,23 @@ export const buildCanvasGraphComponents = async ( infillMethod, } = state.generation; - // generationParameters.invert_mask = shouldPreserveMaskedArea; - // if (boundingBoxScale !== 'none') { - // generationParameters.inpaint_width = scaledBoundingBoxDimensions.width; - // generationParameters.inpaint_height = scaledBoundingBoxDimensions.height; - // } + const { scaledBoundingBoxDimensions, boundingBoxScaleMethod } = + state.canvas; + + if (boundingBoxScaleMethod !== 'none') { + baseNode.inpaint_width = scaledBoundingBoxDimensions.width; + baseNode.inpaint_height = scaledBoundingBoxDimensions.height; + } + baseNode.seam_size = seamSize; baseNode.seam_blur = seamBlur; baseNode.seam_strength = seamStrength; baseNode.seam_steps = seamSteps; - baseNode.tile_size = tileSize; baseNode.infill_method = infillMethod as InpaintInvocation['infill_method']; - // baseNode.force_outpaint = false; + + if (infillMethod === 'tile') { + baseNode.tile_size = tileSize; + } } // We always range and iterate nodes, no matter the iteration count diff --git a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildInpaintNode.ts b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildInpaintNode.ts index 0556a499be..593658e536 100644 --- a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildInpaintNode.ts +++ b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildInpaintNode.ts @@ -2,15 +2,12 @@ import { v4 as uuidv4 } from 'uuid'; import { RootState } from 'app/store/store'; import { InpaintInvocation } from 'services/api'; import { O } from 'ts-toolbelt'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; export const buildInpaintNode = ( state: RootState, overrides: O.Partial = {} ): InpaintInvocation => { const nodeId = uuidv4(); - const { generation } = state; - const activeTabName = activeTabNameSelector(state); const { positivePrompt: prompt, @@ -25,8 +22,7 @@ export const buildInpaintNode = ( img2imgStrength: strength, shouldFitToWidthHeight: fit, shouldRandomizeSeed, - initialImage, - } = generation; + } = state.generation; const inpaintNode: InpaintInvocation = { id: nodeId, @@ -42,19 +38,6 @@ export const buildInpaintNode = ( 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'; - } - - inpaintNode.image = { - image_name: initialImage.name, - image_origin: initialImage.type, - }; - } - if (!shouldRandomizeSeed) { inpaintNode.seed = seed; } From cffcf809774a1ebc6e07e4fa155d85f88bfd84df Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 May 2023 16:55:10 +1000 Subject: [PATCH 07/29] fix(ui): remove w/h from canvas params, add bbox w/h --- invokeai/frontend/web/public/locales/en.json | 2 ++ .../Canvas/BoundingBox/ParamBoundingBoxHeight.tsx | 15 ++++++++++----- .../Canvas/BoundingBox/ParamBoundingBoxWidth.tsx | 15 ++++++++++----- .../UnifiedCanvas/UnifiedCanvasCoreParameters.tsx | 12 ++++++------ .../UnifiedCanvas/UnifiedCanvasParameters.tsx | 2 -- 5 files changed, 28 insertions(+), 18 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 4bd1e5aab3..de11352233 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -454,6 +454,8 @@ "height": "Height", "scheduler": "Scheduler", "seed": "Seed", + "boundingBoxWidth": "Bounding Box Width", + "boundingBoxHeight": "Bounding Box Height", "imageToImage": "Image to Image", "randomizeSeed": "Randomize Seed", "shuffle": "Shuffle Seed", diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx index 75ec70f257..dc83ba8907 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx @@ -2,18 +2,22 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAISlider from 'common/components/IAISlider'; -import { canvasSelector } from 'features/canvas/store/canvasSelectors'; +import { + canvasSelector, + isStagingSelector, +} from 'features/canvas/store/canvasSelectors'; import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; const selector = createSelector( - canvasSelector, - (canvas) => { + [canvasSelector, isStagingSelector], + (canvas, isStaging) => { const { boundingBoxDimensions } = canvas; return { boundingBoxDimensions, + isStaging, }; }, defaultSelectorOptions @@ -21,7 +25,7 @@ const selector = createSelector( const ParamBoundingBoxWidth = () => { const dispatch = useAppDispatch(); - const { boundingBoxDimensions } = useAppSelector(selector); + const { boundingBoxDimensions, isStaging } = useAppSelector(selector); const { t } = useTranslation(); @@ -45,12 +49,13 @@ const ParamBoundingBoxWidth = () => { return ( { + [canvasSelector, isStagingSelector], + (canvas, isStaging) => { const { boundingBoxDimensions } = canvas; return { boundingBoxDimensions, + isStaging, }; }, defaultSelectorOptions @@ -21,7 +25,7 @@ const selector = createSelector( const ParamBoundingBoxWidth = () => { const dispatch = useAppDispatch(); - const { boundingBoxDimensions } = useAppSelector(selector); + const { boundingBoxDimensions, isStaging } = useAppSelector(selector); const { t } = useTranslation(); @@ -45,12 +49,13 @@ const ParamBoundingBoxWidth = () => { return ( { - - + + @@ -55,8 +55,8 @@ const UnifiedCanvasCoreParameters = () => { - - + + )} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx index 4aa68ad56a..c4501ffc44 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx @@ -2,7 +2,6 @@ import ProcessButtons from 'features/parameters/components/ProcessButtons/Proces import ParamSeedCollapse from 'features/parameters/components/Parameters/Seed/ParamSeedCollapse'; import ParamVariationCollapse from 'features/parameters/components/Parameters/Variations/ParamVariationCollapse'; import ParamSymmetryCollapse from 'features/parameters/components/Parameters/Symmetry/ParamSymmetryCollapse'; -import ParamBoundingBoxCollapse from 'features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxCollapse'; import ParamInfillAndScalingCollapse from 'features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamInfillAndScalingCollapse'; import ParamSeamCorrectionCollapse from 'features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamCorrectionCollapse'; import UnifiedCanvasCoreParameters from './UnifiedCanvasCoreParameters'; @@ -20,7 +19,6 @@ const UnifiedCanvasParameters = () => { - From fab7a1d337aa5cc42b54d636dabc0c25970702cd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 May 2023 16:55:59 +1000 Subject: [PATCH 08/29] fix(ui): fix bug with staging bbox not resetting --- .../frontend/web/src/features/canvas/store/canvasSlice.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index ad0581e42f..7f41066ba1 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -29,6 +29,7 @@ import { isCanvasMaskLine, } from './canvasTypes'; import { ImageDTO } from 'services/api'; +import { sessionCanceled } from 'services/thunks/session'; export const initialLayerState: CanvasLayerState = { objects: [], @@ -844,6 +845,13 @@ export const canvasSlice = createSlice({ state.isTransformingBoundingBox = false; }, }, + extraReducers: (builder) => { + builder.addCase(sessionCanceled.pending, (state) => { + if (!state.layerState.stagingArea.images.length) { + state.layerState.stagingArea = initialLayerState.stagingArea; + } + }); + }, }); export const { From 33bbae2f47ef95f497ab7c8131b2ac6a09f77115 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 May 2023 17:29:47 +1000 Subject: [PATCH 09/29] fix(ui): fix missing init image when fit disabled --- .../nodes/util/graphBuilders/buildImageToImageGraph.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts index 728d75c2ae..c7106a3f90 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts @@ -384,8 +384,8 @@ export const buildImageToImageGraph = (state: RootState): Graph => { }, }); } else { - // We are not resizing, so we need to set the image on the `LATENTS_TO_LATENTS` node explicitly - set(graph.nodes[LATENTS_TO_LATENTS], 'image', { + // 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', { image_name: initialImage.image_name, image_origin: initialImage.image_origin, }); From a9c47237b18181aab156bc4b16b386231c3e87d8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 May 2023 17:30:05 +1000 Subject: [PATCH 10/29] fix(ui): mark img2img resize node intermediate --- .../features/nodes/util/graphBuilders/buildImageToImageGraph.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts index c7106a3f90..fe4f6c63b5 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts @@ -352,6 +352,7 @@ export const buildImageToImageGraph = (state: RootState): Graph => { image_name: initialImage.image_name, image_origin: initialImage.image_origin, }, + is_intermediate: true, height, width, }; From 47ca71a7eb19a359cdc788fc2d0b7d0c330f728a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 May 2023 18:09:40 +1000 Subject: [PATCH 11/29] fix(nodes): set cfg_scale min to 1 in latents --- invokeai/app/invocations/latent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 58b0fdccbc..ececf40564 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -173,7 +173,7 @@ class TextToLatentsInvocation(BaseInvocation): negative_conditioning: Optional[ConditioningField] = Field(description="Negative conditioning for generation") noise: Optional[LatentsField] = Field(description="The noise to use") steps: int = Field(default=10, gt=0, description="The number of steps to use to generate the image") - cfg_scale: float = Field(default=7.5, gt=0, description="The Classifier-Free Guidance, higher values may result in a result closer to the prompt", ) + cfg_scale: float = Field(default=7.5, ge=1, description="The Classifier-Free Guidance, higher values may result in a result closer to the prompt", ) scheduler: SAMPLER_NAME_VALUES = Field(default="euler", description="The scheduler to use" ) model: str = Field(default="", description="The model to use (currently ignored)") control: Union[ControlField, list[ControlField]] = Field(default=None, description="The control to use") From a9a2bd90c23eef60c9084b537f00258c273e1b85 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 May 2023 18:10:48 +1000 Subject: [PATCH 12/29] fix(nodes): set min and max for l2l strength --- invokeai/app/invocations/latent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index ececf40564..4dc1f6456c 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -366,7 +366,7 @@ class LatentsToLatentsInvocation(TextToLatentsInvocation): # Inputs latents: Optional[LatentsField] = Field(description="The latents to use as a base image") - strength: float = Field(default=0.5, description="The strength of the latents to use") + strength: float = Field(default=0.7, ge=0, le=1, description="The strength of the latents to use") # Schema customisation class Config(InvocationConfig): From 9687fe7bace4abb782b576bac577df6668a28d23 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 May 2023 22:01:21 +1000 Subject: [PATCH 13/29] fix(ui): set default model to first model (alpha sort) --- .../web/src/features/parameters/store/generationSlice.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index 849f848ff3..9b2539550b 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -2,7 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import * as InvokeAI from 'app/types/invokeai'; import promptToString from 'common/util/promptToString'; -import { clamp, sample } from 'lodash-es'; +import { clamp, sample, sortBy } from 'lodash-es'; import { setAllParametersReducer } from './setAllParametersReducer'; import { receivedModels } from 'services/thunks/model'; import { Scheduler } from 'app/constants'; @@ -227,10 +227,8 @@ export const generationSlice = createSlice({ extraReducers: (builder) => { builder.addCase(receivedModels.fulfilled, (state, action) => { if (!state.model) { - const randomModel = sample(action.payload); - if (randomModel) { - state.model = randomModel.name; - } + const firstModel = sortBy(action.payload, 'name')[0]; + state.model = firstModel.name; } }); }, From e1ae7842ff600ad6434b36c6d7cbce120dc93dd9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 May 2023 22:12:21 +1000 Subject: [PATCH 14/29] feat(ui): add `defaultModel` to config --- invokeai/frontend/web/src/app/types/invokeai.ts | 1 + .../src/features/parameters/store/generationSlice.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index 0de1d8c84b..409a37ac36 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -351,6 +351,7 @@ export type AppConfig = { disabledSDFeatures: SDFeature[]; canRestoreDeletedImagesFromBin: boolean; sd: { + defaultModel?: string; iterations: { initial: number; min: number; diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index 9b2539550b..75e724ed4c 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -2,11 +2,12 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import * as InvokeAI from 'app/types/invokeai'; import promptToString from 'common/util/promptToString'; -import { clamp, sample, sortBy } from 'lodash-es'; +import { clamp, sortBy } from 'lodash-es'; import { setAllParametersReducer } from './setAllParametersReducer'; import { receivedModels } from 'services/thunks/model'; import { Scheduler } from 'app/constants'; import { ImageDTO } from 'services/api'; +import { configChanged } from 'features/system/store/configSlice'; export interface GenerationState { cfgScale: number; @@ -231,6 +232,13 @@ export const generationSlice = createSlice({ state.model = firstModel.name; } }); + + builder.addCase(configChanged, (state, action) => { + const defaultModel = action.payload.sd?.defaultModel; + if (defaultModel && !state.model) { + state.model = defaultModel; + } + }); }, }); From 7c7ffddb2b0de7e6b413a6dcb5e02bdc1b9ddd0a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 May 2023 22:50:42 +1000 Subject: [PATCH 15/29] feat(ui): upgrade IAICustomSelect to optionally display tooltips for each item --- .../src/common/components/IAICustomSelect.tsx | 92 +++++++++++-------- 1 file changed, 52 insertions(+), 40 deletions(-) diff --git a/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx b/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx index d9610346ec..6d6cdbadf5 100644 --- a/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx +++ b/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx @@ -21,9 +21,12 @@ import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import { memo } from 'react'; +export type ItemTooltips = { [key: string]: string }; + type IAICustomSelectProps = { label?: string; items: string[]; + itemTooltips?: ItemTooltips; selectedItem: string; setSelectedItem: (v: string | null | undefined) => void; withCheckIcon?: boolean; @@ -37,6 +40,7 @@ const IAICustomSelect = (props: IAICustomSelectProps) => { const { label, items, + itemTooltips, setSelectedItem, selectedItem, withCheckIcon, @@ -118,48 +122,56 @@ const IAICustomSelect = (props: IAICustomSelectProps) => { > {items.map((item, index) => ( - - {withCheckIcon ? ( - - - {selectedItem === item && } - - - - {item} - - - - ) : ( - - {item} - - )} - + + {withCheckIcon ? ( + + + {selectedItem === item && } + + + + {item} + + + + ) : ( + + {item} + + )} + + ))} From 296ee6b7ea8a56afc88f2826c8105d052ba9bfe7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 May 2023 22:51:03 +1000 Subject: [PATCH 16/29] feat(ui): tidy ParamScheduler component --- .../Parameters/Core/ParamScheduler.tsx | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamScheduler.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamScheduler.tsx index 2b5db18d93..5b6b07803a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamScheduler.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamScheduler.tsx @@ -1,24 +1,36 @@ +import { createSelector } from '@reduxjs/toolkit'; import { Scheduler } from 'app/constants'; -import { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAICustomSelect from 'common/components/IAICustomSelect'; +import { generationSelector } from 'features/parameters/store/generationSelectors'; import { setScheduler } from 'features/parameters/store/generationSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; +import { + activeTabNameSelector, + uiSelector, +} from 'features/ui/store/uiSelectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +const selector = createSelector( + [uiSelector, generationSelector, activeTabNameSelector], + (ui, generation, activeTabName) => { + const allSchedulers = ['img2img', 'unifiedCanvas'].includes(activeTabName) + ? ui.schedulers.filter((scheduler) => { + return !['dpmpp_2s'].includes(scheduler); + }) + : ui.schedulers; + + return { + scheduler: generation.scheduler, + allSchedulers, + }; + }, + defaultSelectorOptions +); + const ParamScheduler = () => { - const scheduler = useAppSelector( - (state: RootState) => state.generation.scheduler - ); - - const activeTabName = useAppSelector(activeTabNameSelector); - - const schedulers = useAppSelector((state: RootState) => state.ui.schedulers); - - const img2imgSchedulers = schedulers.filter((scheduler) => { - return !['dpmpp_2s'].includes(scheduler); - }); + const { allSchedulers, scheduler } = useAppSelector(selector); const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -38,11 +50,7 @@ const ParamScheduler = () => { label={t('parameters.scheduler')} selectedItem={scheduler} setSelectedItem={handleChange} - items={ - ['img2img', 'unifiedCanvas'].includes(activeTabName) - ? img2imgSchedulers - : schedulers - } + items={allSchedulers} withCheckIcon /> ); From 6e60f7517bf855c3a0ee9edcffc9dbed2518053e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 May 2023 22:51:19 +1000 Subject: [PATCH 17/29] feat(ui): add model description tooltips --- .../system/components/ModelSelect.tsx | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx b/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx index 520e30b60a..be4be8ceaa 100644 --- a/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx +++ b/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx @@ -4,19 +4,33 @@ import { isEqual } from 'lodash-es'; import { useTranslation } from 'react-i18next'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { selectModelsById, selectModelsIds } from '../store/modelSlice'; +import { + selectModelsAll, + selectModelsById, + selectModelsIds, +} from '../store/modelSlice'; import { RootState } from 'app/store/store'; import { modelSelected } from 'features/parameters/store/generationSlice'; import { generationSelector } from 'features/parameters/store/generationSelectors'; -import IAICustomSelect from 'common/components/IAICustomSelect'; +import IAICustomSelect, { + ItemTooltips, +} from 'common/components/IAICustomSelect'; const selector = createSelector( [(state: RootState) => state, generationSelector], (state, generation) => { const selectedModel = selectModelsById(state, generation.model); const allModelNames = selectModelsIds(state).map((id) => String(id)); + const allModelTooltips = selectModelsAll(state).reduce( + (allModelTooltips, model) => { + allModelTooltips[model.name] = model.description ?? ''; + return allModelTooltips; + }, + {} as ItemTooltips + ); return { allModelNames, + allModelTooltips, selectedModel, }; }, @@ -30,7 +44,8 @@ const selector = createSelector( const ModelSelect = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const { allModelNames, selectedModel } = useAppSelector(selector); + const { allModelNames, allModelTooltips, selectedModel } = + useAppSelector(selector); const handleChangeModel = useCallback( (v: string | null | undefined) => { if (!v) { @@ -46,6 +61,7 @@ const ModelSelect = () => { label={t('modelManager.model')} tooltip={selectedModel?.description} items={allModelNames} + itemTooltips={allModelTooltips} selectedItem={selectedModel?.name ?? ''} setSelectedItem={handleChangeModel} withCheckIcon={true} From 877959b413b348ca9dfed48a43023f631457f4a4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 May 2023 23:19:28 +1000 Subject: [PATCH 18/29] fix(ui): ensure download image opens in new tab --- .../src/features/gallery/components/CurrentImageButtons.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx index dc3022efb2..57d088d125 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx @@ -461,7 +461,11 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { {t('parameters.copyImageToLink')} - + } size="sm" w="100%"> {t('parameters.downloadImage')} From 062b2cf46fca43e25ae203a3657f515712ffa4c9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 31 May 2023 08:39:02 +1000 Subject: [PATCH 19/29] fix(ui): fix width and height not working on txt2img tab I missed a spot when working on the graph logic yesterday. --- .../nodes/util/graphBuilders/buildTextToImageGraph.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts index 737d8d5b61..753ccccff8 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts @@ -121,6 +121,8 @@ export const buildTextToImageGraph = (state: RootState): Graph => { id: NOISE, type: 'noise', seed: seed, + width, + height, }; graph.nodes[NOISE] = noiseNode; @@ -147,6 +149,8 @@ export const buildTextToImageGraph = (state: RootState): Graph => { const noiseNode: NoiseInvocation = { id: NOISE, type: 'noise', + width, + height, }; graph.nodes[RANDOM_INT] = randomIntNode; @@ -193,6 +197,8 @@ export const buildTextToImageGraph = (state: RootState): Graph => { const noiseNode: NoiseInvocation = { id: NOISE, type: 'noise', + width, + height, }; // Adding to the graph From 6571e4c2fd38721ebb82d2c05fa07c039d0abba3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 31 May 2023 21:05:31 +1000 Subject: [PATCH 20/29] feat(ui): refactor parameter recall - use zod to validate parameters before recalling - update recall params hook to handle all validation and UI feedback --- invokeai/frontend/web/package.json | 3 +- invokeai/frontend/web/public/locales/en.json | 2 + invokeai/frontend/web/src/app/constants.ts | 14 - .../frontend/web/src/app/types/invokeai.ts | 359 +++--------------- .../web/src/common/util/_parseMetadataZod.ts | 119 ------ .../components/CurrentImageButtons.tsx | 10 +- .../gallery/components/HoverableImage.tsx | 14 +- .../ImageMetadataViewer.tsx | 89 +++-- .../parameters/hooks/useParameters.ts | 151 -------- .../features/parameters/hooks/usePrompt.ts | 23 -- .../parameters/hooks/useRecallParameters.ts | 348 +++++++++++++++++ .../parameters/store/generationSlice.ts | 61 ++- .../parameters/store/parameterZodSchemas.ts | 156 ++++++++ .../store/setAllParametersReducer.ts | 77 ---- invokeai/frontend/web/yarn.lock | 5 + 15 files changed, 667 insertions(+), 764 deletions(-) delete mode 100644 invokeai/frontend/web/src/common/util/_parseMetadataZod.ts delete mode 100644 invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts delete mode 100644 invokeai/frontend/web/src/features/parameters/hooks/usePrompt.ts create mode 100644 invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts create mode 100644 invokeai/frontend/web/src/features/parameters/store/parameterZodSchemas.ts delete mode 100644 invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 13b8d78bf7..dd1c87effb 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -101,7 +101,8 @@ "serialize-error": "^11.0.0", "socket.io-client": "^4.6.0", "use-image": "^1.1.0", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "zod": "^3.21.4" }, "peerDependencies": { "@chakra-ui/cli": "^2.4.0", diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index de11352233..bf14dd5510 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -568,6 +568,8 @@ "canvasMerged": "Canvas Merged", "sentToImageToImage": "Sent To Image To Image", "sentToUnifiedCanvas": "Sent to Unified Canvas", + "parameterSet": "Parameter set", + "parameterNotSet": "Parameter not set", "parametersSet": "Parameters Set", "parametersNotSet": "Parameters Not Set", "parametersNotSetDesc": "No metadata found for this image.", diff --git a/invokeai/frontend/web/src/app/constants.ts b/invokeai/frontend/web/src/app/constants.ts index d312d725ba..6700a732b3 100644 --- a/invokeai/frontend/web/src/app/constants.ts +++ b/invokeai/frontend/web/src/app/constants.ts @@ -21,25 +21,11 @@ export const SCHEDULERS = [ export type Scheduler = (typeof SCHEDULERS)[number]; -export const isScheduler = (x: string): x is Scheduler => - SCHEDULERS.includes(x as Scheduler); - -// Valid image widths -export const WIDTHS: Array = Array.from(Array(64)).map( - (_x, i) => (i + 1) * 64 -); - -// Valid image heights -export const HEIGHTS: Array = Array.from(Array(64)).map( - (_x, i) => (i + 1) * 64 -); - // Valid upscaling levels export const UPSCALING_LEVELS: Array<{ key: string; value: number }> = [ { key: '2x', value: 2 }, { key: '4x', value: 4 }, ]; - export const NUMPY_RAND_MIN = 0; export const NUMPY_RAND_MAX = 2147483647; diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index 409a37ac36..8081ffa491 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -1,316 +1,82 @@ -/** - * Types for images, the things they are made of, and the things - * they make up. - * - * Generated images are txt2img and img2img images. They may have - * had additional postprocessing done on them when they were first - * generated. - * - * Postprocessed images are images which were not generated here - * but only postprocessed by the app. They only get postprocessing - * metadata and have a different image type, e.g. 'esrgan' or - * 'gfpgan'. - */ - -import { SelectedImage } from 'features/parameters/store/actions'; import { InvokeTabName } from 'features/ui/store/tabMap'; -import { IRect } from 'konva/lib/types'; -import { ImageResponseMetadata, ResourceOrigin } from 'services/api'; import { O } from 'ts-toolbelt'; -/** - * TODO: - * Once an image has been generated, if it is postprocessed again, - * additional postprocessing steps are added to its postprocessing - * array. - * - * TODO: Better documentation of types. - */ +// These are old types from the model management UI -export type PromptItem = { - prompt: string; - weight: number; -}; +// export type ModelStatus = 'active' | 'cached' | 'not loaded'; -// TECHDEBT: We need to retain compatibility with plain prompt strings and the structure Prompt type -export type Prompt = Array | string; - -export type SeedWeightPair = { - seed: number; - weight: number; -}; - -export type SeedWeights = Array; - -// All generated images contain these metadata. -export type CommonGeneratedImageMetadata = { - postprocessing: null | Array; - sampler: - | 'ddim' - | 'ddpm' - | 'deis' - | 'lms' - | 'pndm' - | 'heun' - | 'heun_k' - | 'euler' - | 'euler_k' - | 'euler_a' - | 'kdpm_2' - | 'kdpm_2_a' - | 'dpmpp_2s' - | 'dpmpp_2m' - | 'dpmpp_2m_k' - | 'unipc'; - prompt: Prompt; - seed: number; - variations: SeedWeights; - steps: number; - cfg_scale: number; - width: number; - height: number; - seamless: boolean; - hires_fix: boolean; - extra: null | Record; // Pending development of RFC #266 -}; - -// txt2img and img2img images have some unique attributes. -export type Txt2ImgMetadata = CommonGeneratedImageMetadata & { - type: 'txt2img'; -}; - -export type Img2ImgMetadata = CommonGeneratedImageMetadata & { - type: 'img2img'; - orig_hash: string; - strength: number; - fit: boolean; - init_image_path: string; - mask_image_path?: string; -}; - -// Superset of generated image metadata types. -export type GeneratedImageMetadata = Txt2ImgMetadata | Img2ImgMetadata; - -// All post processed images contain these metadata. -export type CommonPostProcessedImageMetadata = { - orig_path: string; - orig_hash: string; -}; - -// esrgan and gfpgan images have some unique attributes. -export type ESRGANMetadata = CommonPostProcessedImageMetadata & { - type: 'esrgan'; - scale: 2 | 4; - strength: number; - denoise_str: number; -}; - -export type FacetoolMetadata = CommonPostProcessedImageMetadata & { - type: 'gfpgan' | 'codeformer'; - strength: number; - fidelity?: number; -}; - -// Superset of all postprocessed image metadata types.. -export type PostProcessedImageMetadata = ESRGANMetadata | FacetoolMetadata; - -// Metadata includes the system config and image metadata. -// export type Metadata = SystemGenerationMetadata & { -// image: GeneratedImageMetadata | PostProcessedImageMetadata; +// export type Model = { +// status: ModelStatus; +// description: string; +// weights: string; +// config?: string; +// vae?: string; +// width?: number; +// height?: number; +// default?: boolean; +// format?: string; // }; -/** - * ResultImage - */ -// export ty`pe Image = { +// export type DiffusersModel = { +// status: ModelStatus; +// description: string; +// repo_id?: string; +// path?: string; +// vae?: { +// repo_id?: string; +// path?: string; +// }; +// format?: string; +// default?: boolean; +// }; + +// export type ModelList = Record; + +// export type FoundModel = { // name: string; -// type: image_origin; -// url: string; -// thumbnail: string; -// metadata: ImageResponseMetadata; +// location: string; // }; -// export const isInvokeAIImage = (obj: Image | SelectedImage): obj is Image => { -// if ('url' in obj && 'thumbnail' in obj) { -// return true; -// } - -// return false; +// export type InvokeModelConfigProps = { +// name: string | undefined; +// description: string | undefined; +// config: string | undefined; +// weights: string | undefined; +// vae: string | undefined; +// width: number | undefined; +// height: number | undefined; +// default: boolean | undefined; +// format: string | undefined; // }; -/** - * Types related to the system status. - */ - -// // This represents the processing status of the backend. -// export type SystemStatus = { -// isProcessing: boolean; -// currentStep: number; -// totalSteps: number; -// currentIteration: number; -// totalIterations: number; -// currentStatus: string; -// currentStatusHasSteps: boolean; -// hasError: boolean; +// export type InvokeDiffusersModelConfigProps = { +// name: string | undefined; +// description: string | undefined; +// repo_id: string | undefined; +// path: string | undefined; +// default: boolean | undefined; +// format: string | undefined; +// vae: { +// repo_id: string | undefined; +// path: string | undefined; +// }; // }; -// export type SystemGenerationMetadata = { -// model: string; -// model_weights?: string; -// model_id?: string; -// model_hash: string; -// app_id: string; -// app_version: string; +// export type InvokeModelConversionProps = { +// model_name: string; +// save_location: string; +// custom_location: string | null; // }; -// export type SystemConfig = SystemGenerationMetadata & { -// model_list: ModelList; -// infill_methods: string[]; +// export type InvokeModelMergingProps = { +// models_to_merge: string[]; +// alpha: number; +// interp: 'weighted_sum' | 'sigmoid' | 'inv_sigmoid' | 'add_difference'; +// force: boolean; +// merged_model_name: string; +// model_merge_save_path: string | null; // }; -export type ModelStatus = 'active' | 'cached' | 'not loaded'; - -export type Model = { - status: ModelStatus; - description: string; - weights: string; - config?: string; - vae?: string; - width?: number; - height?: number; - default?: boolean; - format?: string; -}; - -export type DiffusersModel = { - status: ModelStatus; - description: string; - repo_id?: string; - path?: string; - vae?: { - repo_id?: string; - path?: string; - }; - format?: string; - default?: boolean; -}; - -export type ModelList = Record; - -export type FoundModel = { - name: string; - location: string; -}; - -export type InvokeModelConfigProps = { - name: string | undefined; - description: string | undefined; - config: string | undefined; - weights: string | undefined; - vae: string | undefined; - width: number | undefined; - height: number | undefined; - default: boolean | undefined; - format: string | undefined; -}; - -export type InvokeDiffusersModelConfigProps = { - name: string | undefined; - description: string | undefined; - repo_id: string | undefined; - path: string | undefined; - default: boolean | undefined; - format: string | undefined; - vae: { - repo_id: string | undefined; - path: string | undefined; - }; -}; - -export type InvokeModelConversionProps = { - model_name: string; - save_location: string; - custom_location: string | null; -}; - -export type InvokeModelMergingProps = { - models_to_merge: string[]; - alpha: number; - interp: 'weighted_sum' | 'sigmoid' | 'inv_sigmoid' | 'add_difference'; - force: boolean; - merged_model_name: string; - model_merge_save_path: string | null; -}; - -/** - * These types type data received from the server via socketio. - */ - -export type ModelChangeResponse = { - model_name: string; - model_list: ModelList; -}; - -export type ModelConvertedResponse = { - converted_model_name: string; - model_list: ModelList; -}; - -export type ModelsMergedResponse = { - merged_models: string[]; - merged_model_name: string; - model_list: ModelList; -}; - -export type ModelAddedResponse = { - new_model_name: string; - model_list: ModelList; - update: boolean; -}; - -export type ModelDeletedResponse = { - deleted_model_name: string; - model_list: ModelList; -}; - -export type FoundModelResponse = { - search_folder: string; - found_models: FoundModel[]; -}; - -// export type SystemStatusResponse = SystemStatus; - -// export type SystemConfigResponse = SystemConfig; - -export type ImageResultResponse = Omit & { - boundingBox?: IRect; - generationMode: InvokeTabName; -}; - -export type ImageUploadResponse = { - // image: Omit; - url: string; - mtime: number; - width: number; - height: number; - thumbnail: string; - // bbox: [number, number, number, number]; -}; - -export type ErrorResponse = { - message: string; - additionalData?: string; -}; - -export type ImageUrlResponse = { - url: string; -}; - -export type UploadOutpaintingMergeImagePayload = { - dataURL: string; - name: string; -}; - /** * A disable-able application feature */ @@ -322,7 +88,8 @@ export type AppFeature = | 'githubLink' | 'discordLink' | 'bugLink' - | 'localization'; + | 'localization' + | 'consoleLogging'; /** * A disable-able Stable Diffusion feature diff --git a/invokeai/frontend/web/src/common/util/_parseMetadataZod.ts b/invokeai/frontend/web/src/common/util/_parseMetadataZod.ts deleted file mode 100644 index 584399233f..0000000000 --- a/invokeai/frontend/web/src/common/util/_parseMetadataZod.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * PARTIAL ZOD IMPLEMENTATION - * - * doesn't work well bc like most validators, zod is not built to skip invalid values. - * it mostly works but just seems clearer and simpler to manually parse for now. - * - * in the future it would be really nice if we could use zod for some things: - * - zodios (axios + zod): https://github.com/ecyrbe/zodios - * - openapi to zodios: https://github.com/astahmer/openapi-zod-client - */ - -// import { z } from 'zod'; - -// const zMetadataStringField = z.string(); -// export type MetadataStringField = z.infer; - -// const zMetadataIntegerField = z.number().int(); -// export type MetadataIntegerField = z.infer; - -// const zMetadataFloatField = z.number(); -// export type MetadataFloatField = z.infer; - -// const zMetadataBooleanField = z.boolean(); -// export type MetadataBooleanField = z.infer; - -// const zMetadataImageField = z.object({ -// image_type: z.union([ -// z.literal('results'), -// z.literal('uploads'), -// z.literal('intermediates'), -// ]), -// image_name: z.string().min(1), -// }); -// export type MetadataImageField = z.infer; - -// const zMetadataLatentsField = z.object({ -// latents_name: z.string().min(1), -// }); -// export type MetadataLatentsField = z.infer; - -// /** -// * zod Schema for any node field. Use a `transform()` to manually parse, skipping invalid values. -// */ -// const zAnyMetadataField = z.any().transform((val, ctx) => { -// // Grab the field name from the path -// const fieldName = String(ctx.path[ctx.path.length - 1]); - -// // `id` and `type` must be strings if they exist -// if (['id', 'type'].includes(fieldName)) { -// const reservedStringPropertyResult = zMetadataStringField.safeParse(val); -// if (reservedStringPropertyResult.success) { -// return reservedStringPropertyResult.data; -// } - -// return; -// } - -// // Parse the rest of the fields, only returning the data if the parsing is successful - -// const stringFieldResult = zMetadataStringField.safeParse(val); -// if (stringFieldResult.success) { -// return stringFieldResult.data; -// } - -// const integerFieldResult = zMetadataIntegerField.safeParse(val); -// if (integerFieldResult.success) { -// return integerFieldResult.data; -// } - -// const floatFieldResult = zMetadataFloatField.safeParse(val); -// if (floatFieldResult.success) { -// return floatFieldResult.data; -// } - -// const booleanFieldResult = zMetadataBooleanField.safeParse(val); -// if (booleanFieldResult.success) { -// return booleanFieldResult.data; -// } - -// const imageFieldResult = zMetadataImageField.safeParse(val); -// if (imageFieldResult.success) { -// return imageFieldResult.data; -// } - -// const latentsFieldResult = zMetadataImageField.safeParse(val); -// if (latentsFieldResult.success) { -// return latentsFieldResult.data; -// } -// }); - -// /** -// * The node metadata schema. -// */ -// const zNodeMetadata = z.object({ -// session_id: z.string().min(1).optional(), -// node: z.record(z.string().min(1), zAnyMetadataField).optional(), -// }); - -// export type NodeMetadata = z.infer; - -// const zMetadata = z.object({ -// invokeai: zNodeMetadata.optional(), -// 'sd-metadata': z.record(z.string().min(1), z.any()).optional(), -// }); -// export type Metadata = z.infer; - -// export const parseMetadata = ( -// metadata: Record -// ): Metadata | undefined => { -// const result = zMetadata.safeParse(metadata); -// if (!result.success) { -// console.log(result.error.issues); -// return; -// } - -// return result.data; -// }; - -export default {}; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx index 57d088d125..91bd1a0425 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx @@ -49,7 +49,7 @@ import { useCallback } from 'react'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; import { useGetUrl } from 'common/util/getUrl'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { useParameters } from 'features/parameters/hooks/useParameters'; +import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { initialImageSelected } from 'features/parameters/store/actions'; import { requestedImageDeletion, @@ -58,7 +58,6 @@ import { } from '../store/actions'; import FaceRestoreSettings from 'features/parameters/components/Parameters/FaceRestore/FaceRestoreSettings'; import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/UpscaleSettings'; -import { allParametersSet } from 'features/parameters/store/generationSlice'; import DeleteImageButton from './ImageActionButtons/DeleteImageButton'; import { useAppToaster } from 'app/components/Toaster'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; @@ -165,7 +164,8 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { const toaster = useAppToaster(); const { t } = useTranslation(); - const { recallPrompt, recallSeed, recallAllParameters } = useParameters(); + const { recallBothPrompts, recallSeed, recallAllParameters } = + useRecallParameters(); // const handleCopyImage = useCallback(async () => { // if (!image?.url) { @@ -250,11 +250,11 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { useHotkeys('s', handleUseSeed, [image]); const handleUsePrompt = useCallback(() => { - recallPrompt( + recallBothPrompts( image?.metadata?.positive_conditioning, image?.metadata?.negative_conditioning ); - }, [image, recallPrompt]); + }, [image, recallBothPrompts]); useHotkeys('p', handleUsePrompt, [image]); diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx index 94b653af1c..f652cebda2 100644 --- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx @@ -30,7 +30,7 @@ import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { isEqual } from 'lodash-es'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { useParameters } from 'features/parameters/hooks/useParameters'; +import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { initialImageSelected } from 'features/parameters/store/actions'; import { requestedImageDeletion, @@ -114,8 +114,8 @@ const HoverableImage = memo((props: HoverableImageProps) => { const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled; const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled; - const { recallSeed, recallPrompt, recallInitialImage, recallAllParameters } = - useParameters(); + const { recallBothPrompts, recallSeed, recallAllParameters } = + useRecallParameters(); const handleMouseOver = () => setIsHovered(true); const handleMouseOut = () => setIsHovered(false); @@ -154,11 +154,15 @@ const HoverableImage = memo((props: HoverableImageProps) => { // Recall parameters handlers const handleRecallPrompt = useCallback(() => { - recallPrompt( + recallBothPrompts( image.metadata?.positive_conditioning, image.metadata?.negative_conditioning ); - }, [image, recallPrompt]); + }, [ + image.metadata?.negative_conditioning, + image.metadata?.positive_conditioning, + recallBothPrompts, + ]); const handleRecallSeed = useCallback(() => { recallSeed(image.metadata?.seed); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx index df52a06c90..1619680ec5 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx @@ -31,6 +31,7 @@ import { IoArrowUndoCircleOutline } from 'react-icons/io5'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import { ImageDTO } from 'services/api'; import { Scheduler } from 'app/constants'; +import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; type MetadataItemProps = { isLink?: boolean; @@ -120,6 +121,21 @@ const memoEqualityCheck = ( */ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { const dispatch = useAppDispatch(); + const { + recallBothPrompts, + recallPositivePrompt, + recallNegativePrompt, + recallSeed, + recallInitialImage, + recallCfgScale, + recallModel, + recallScheduler, + recallSteps, + recallWidth, + recallHeight, + recallStrength, + recallAllParameters, + } = useRecallParameters(); useHotkeys('esc', () => { dispatch(setShouldShowImageDetails(false)); @@ -166,52 +182,53 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { {metadata.type && ( )} - {metadata.width && ( - dispatch(setWidth(Number(metadata.width)))} - /> - )} - {metadata.height && ( - dispatch(setHeight(Number(metadata.height)))} - /> - )} - {metadata.model && ( - - )} + {sessionId && } {metadata.positive_conditioning && ( + recallPositivePrompt(metadata.positive_conditioning) } - onClick={() => setPositivePrompt(metadata.positive_conditioning!)} /> )} {metadata.negative_conditioning && ( + recallNegativePrompt(metadata.negative_conditioning) } - onClick={() => setNegativePrompt(metadata.negative_conditioning!)} /> )} {metadata.seed !== undefined && ( dispatch(setSeed(Number(metadata.seed)))} + onClick={() => recallSeed(metadata.seed)} + /> + )} + {metadata.model !== undefined && ( + recallModel(metadata.model)} + /> + )} + {metadata.width && ( + recallWidth(metadata.width)} + /> + )} + {metadata.height && ( + recallHeight(metadata.height)} /> )} {/* {metadata.threshold !== undefined && ( @@ -232,23 +249,21 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { - dispatch(setScheduler(metadata.scheduler as Scheduler)) - } + onClick={() => recallScheduler(metadata.scheduler)} /> )} {metadata.steps && ( dispatch(setSteps(Number(metadata.steps)))} + onClick={() => recallSteps(metadata.steps)} /> )} {metadata.cfg_scale !== undefined && ( dispatch(setCfgScale(Number(metadata.cfg_scale)))} + onClick={() => recallCfgScale(metadata.cfg_scale)} /> )} {/* {metadata.variations && metadata.variations.length > 0 && ( @@ -289,9 +304,7 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { - dispatch(setImg2imgStrength(Number(metadata.strength))) - } + onClick={() => recallStrength(metadata.strength)} /> )} {/* {metadata.fit && ( diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts b/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts deleted file mode 100644 index ca9826693d..0000000000 --- a/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { useAppDispatch } from 'app/store/storeHooks'; -import { isFinite, isString } from 'lodash-es'; -import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import useSetBothPrompts from './usePrompt'; -import { allParametersSet, setSeed } from '../store/generationSlice'; -import { isImageField } from 'services/types/guards'; -import { NUMPY_RAND_MAX } from 'app/constants'; -import { initialImageSelected } from '../store/actions'; -import { setActiveTab } from 'features/ui/store/uiSlice'; -import { useAppToaster } from 'app/components/Toaster'; -import { ImageDTO } from 'services/api'; - -export const useParameters = () => { - const dispatch = useAppDispatch(); - const toaster = useAppToaster(); - const { t } = useTranslation(); - const setBothPrompts = useSetBothPrompts(); - - /** - * Sets prompt with toast - */ - const recallPrompt = useCallback( - (prompt: unknown, negativePrompt?: unknown) => { - if (!isString(prompt) || !isString(negativePrompt)) { - toaster({ - title: t('toast.promptNotSet'), - description: t('toast.promptNotSetDesc'), - status: 'warning', - duration: 2500, - isClosable: true, - }); - return; - } - - setBothPrompts(prompt, negativePrompt); - toaster({ - title: t('toast.promptSet'), - status: 'info', - duration: 2500, - isClosable: true, - }); - }, - [t, toaster, setBothPrompts] - ); - - /** - * Sets seed with toast - */ - const recallSeed = useCallback( - (seed: unknown) => { - const s = Number(seed); - if (!isFinite(s) || (isFinite(s) && !(s >= 0 && s <= NUMPY_RAND_MAX))) { - toaster({ - title: t('toast.seedNotSet'), - description: t('toast.seedNotSetDesc'), - status: 'warning', - duration: 2500, - isClosable: true, - }); - return; - } - - dispatch(setSeed(s)); - toaster({ - title: t('toast.seedSet'), - status: 'info', - duration: 2500, - isClosable: true, - }); - }, - [t, toaster, dispatch] - ); - - /** - * Sets initial image with toast - */ - const recallInitialImage = useCallback( - async (image: unknown) => { - if (!isImageField(image)) { - toaster({ - title: t('toast.initialImageNotSet'), - description: t('toast.initialImageNotSetDesc'), - status: 'warning', - duration: 2500, - isClosable: true, - }); - return; - } - - dispatch(initialImageSelected(image.image_name)); - toaster({ - title: t('toast.initialImageSet'), - status: 'info', - duration: 2500, - isClosable: true, - }); - }, - [t, toaster, dispatch] - ); - - /** - * Sets image as initial image with toast - */ - const sendToImageToImage = useCallback( - (image: ImageDTO) => { - dispatch(initialImageSelected(image)); - }, - [dispatch] - ); - - const recallAllParameters = useCallback( - (image: ImageDTO | undefined) => { - const type = image?.metadata?.type; - // not sure what this list should be - if (['t2l', 'l2l', 'inpaint'].includes(String(type))) { - dispatch(allParametersSet(image)); - - if (image?.metadata?.type === 'l2l') { - dispatch(setActiveTab('img2img')); - } else if (image?.metadata?.type === 't2l') { - dispatch(setActiveTab('txt2img')); - } - - toaster({ - title: t('toast.parametersSet'), - status: 'success', - duration: 2500, - isClosable: true, - }); - } else { - toaster({ - title: t('toast.parametersNotSet'), - description: t('toast.parametersNotSetDesc'), - status: 'error', - duration: 2500, - isClosable: true, - }); - } - }, - [t, toaster, dispatch] - ); - - return { - recallPrompt, - recallSeed, - recallInitialImage, - sendToImageToImage, - recallAllParameters, - }; -}; diff --git a/invokeai/frontend/web/src/features/parameters/hooks/usePrompt.ts b/invokeai/frontend/web/src/features/parameters/hooks/usePrompt.ts deleted file mode 100644 index 3fee0bcdd8..0000000000 --- a/invokeai/frontend/web/src/features/parameters/hooks/usePrompt.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { getPromptAndNegative } from 'common/util/getPromptAndNegative'; - -import * as InvokeAI from 'app/types/invokeai'; -import promptToString from 'common/util/promptToString'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { setNegativePrompt, setPositivePrompt } from '../store/generationSlice'; -import { useCallback } from 'react'; - -// TECHDEBT: We have two metadata prompt formats and need to handle recalling either of them. -// This hook provides a function to do that. -const useSetBothPrompts = () => { - const dispatch = useAppDispatch(); - - return useCallback( - (inputPrompt: InvokeAI.Prompt, negativePrompt: InvokeAI.Prompt) => { - dispatch(setPositivePrompt(inputPrompt)); - dispatch(setNegativePrompt(negativePrompt)); - }, - [dispatch] - ); -}; - -export default useSetBothPrompts; diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts b/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts new file mode 100644 index 0000000000..7b7a405867 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts @@ -0,0 +1,348 @@ +import { useAppDispatch } from 'app/store/storeHooks'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + modelSelected, + setCfgScale, + setHeight, + setImg2imgStrength, + setNegativePrompt, + setPositivePrompt, + setScheduler, + setSeed, + setSteps, + setWidth, +} from '../store/generationSlice'; +import { isImageField } from 'services/types/guards'; +import { initialImageSelected } from '../store/actions'; +import { useAppToaster } from 'app/components/Toaster'; +import { ImageDTO } from 'services/api'; +import { + isValidCfgScale, + isValidHeight, + isValidModel, + isValidNegativePrompt, + isValidPositivePrompt, + isValidScheduler, + isValidSeed, + isValidSteps, + isValidStrength, + isValidWidth, +} from '../store/parameterZodSchemas'; + +export const useRecallParameters = () => { + const dispatch = useAppDispatch(); + const toaster = useAppToaster(); + const { t } = useTranslation(); + + const parameterSetToast = useCallback(() => { + toaster({ + title: t('toast.parameterSet'), + status: 'info', + duration: 2500, + isClosable: true, + }); + }, [t, toaster]); + + const parameterNotSetToast = useCallback(() => { + toaster({ + title: t('toast.parameterNotSet'), + status: 'warning', + duration: 2500, + isClosable: true, + }); + }, [t, toaster]); + + const allParameterSetToast = useCallback(() => { + toaster({ + title: t('toast.parametersSet'), + status: 'info', + duration: 2500, + isClosable: true, + }); + }, [t, toaster]); + + const allParameterNotSetToast = useCallback(() => { + toaster({ + title: t('toast.parametersNotSet'), + status: 'warning', + duration: 2500, + isClosable: true, + }); + }, [t, toaster]); + + /** + * Recall both prompts with toast + */ + const recallBothPrompts = useCallback( + (positivePrompt: unknown, negativePrompt: unknown) => { + if ( + isValidPositivePrompt(positivePrompt) || + isValidNegativePrompt(negativePrompt) + ) { + if (isValidPositivePrompt(positivePrompt)) { + dispatch(setPositivePrompt(positivePrompt)); + } + if (isValidNegativePrompt(negativePrompt)) { + dispatch(setNegativePrompt(negativePrompt)); + } + parameterSetToast(); + return; + } + parameterNotSetToast(); + }, + [dispatch, parameterSetToast, parameterNotSetToast] + ); + + /** + * Recall positive prompt with toast + */ + const recallPositivePrompt = useCallback( + (positivePrompt: unknown) => { + if (!isValidPositivePrompt(positivePrompt)) { + parameterNotSetToast(); + return; + } + dispatch(setPositivePrompt(positivePrompt)); + parameterSetToast(); + }, + [dispatch, parameterSetToast, parameterNotSetToast] + ); + + /** + * Recall negative prompt with toast + */ + const recallNegativePrompt = useCallback( + (negativePrompt: unknown) => { + if (!isValidNegativePrompt(negativePrompt)) { + parameterNotSetToast(); + return; + } + dispatch(setNegativePrompt(negativePrompt)); + parameterSetToast(); + }, + [dispatch, parameterSetToast, parameterNotSetToast] + ); + + /** + * Recall seed with toast + */ + const recallSeed = useCallback( + (seed: unknown) => { + if (!isValidSeed(seed)) { + parameterNotSetToast(); + return; + } + dispatch(setSeed(seed)); + parameterSetToast(); + }, + [dispatch, parameterSetToast, parameterNotSetToast] + ); + + /** + * Recall CFG scale with toast + */ + const recallCfgScale = useCallback( + (cfgScale: unknown) => { + if (!isValidCfgScale(cfgScale)) { + parameterNotSetToast(); + return; + } + dispatch(setCfgScale(cfgScale)); + parameterSetToast(); + }, + [dispatch, parameterSetToast, parameterNotSetToast] + ); + + /** + * Recall model with toast + */ + const recallModel = useCallback( + (model: unknown) => { + if (!isValidModel(model)) { + parameterNotSetToast(); + return; + } + dispatch(modelSelected(model)); + parameterSetToast(); + }, + [dispatch, parameterSetToast, parameterNotSetToast] + ); + + /** + * Recall scheduler with toast + */ + const recallScheduler = useCallback( + (scheduler: unknown) => { + if (!isValidScheduler(scheduler)) { + parameterNotSetToast(); + return; + } + dispatch(setScheduler(scheduler)); + parameterSetToast(); + }, + [dispatch, parameterSetToast, parameterNotSetToast] + ); + + /** + * Recall steps with toast + */ + const recallSteps = useCallback( + (steps: unknown) => { + if (!isValidSteps(steps)) { + parameterNotSetToast(); + return; + } + dispatch(setSteps(steps)); + parameterSetToast(); + }, + [dispatch, parameterSetToast, parameterNotSetToast] + ); + + /** + * Recall width with toast + */ + const recallWidth = useCallback( + (width: unknown) => { + if (!isValidWidth(width)) { + parameterNotSetToast(); + return; + } + dispatch(setWidth(width)); + parameterSetToast(); + }, + [dispatch, parameterSetToast, parameterNotSetToast] + ); + + /** + * Recall height with toast + */ + const recallHeight = useCallback( + (height: unknown) => { + if (!isValidHeight(height)) { + parameterNotSetToast(); + return; + } + dispatch(setHeight(height)); + parameterSetToast(); + }, + [dispatch, parameterSetToast, parameterNotSetToast] + ); + + /** + * Recall strength with toast + */ + const recallStrength = useCallback( + (strength: unknown) => { + if (!isValidStrength(strength)) { + parameterNotSetToast(); + return; + } + dispatch(setImg2imgStrength(strength)); + parameterSetToast(); + }, + [dispatch, parameterSetToast, parameterNotSetToast] + ); + + /** + * Sets initial image with toast + */ + const recallInitialImage = useCallback( + async (image: unknown) => { + if (!isImageField(image)) { + parameterNotSetToast(); + return; + } + dispatch(initialImageSelected(image.image_name)); + parameterSetToast(); + }, + [dispatch, parameterSetToast, parameterNotSetToast] + ); + + /** + * Sets image as initial image with toast + */ + const sendToImageToImage = useCallback( + (image: ImageDTO) => { + dispatch(initialImageSelected(image)); + }, + [dispatch] + ); + + const recallAllParameters = useCallback( + (image: ImageDTO | undefined) => { + if (!image || !image.metadata) { + allParameterNotSetToast(); + return; + } + const { + cfg_scale, + height, + model, + positive_conditioning, + negative_conditioning, + scheduler, + seed, + steps, + width, + strength, + clip, + extra, + latents, + unet, + vae, + } = image.metadata; + + if (isValidCfgScale(cfg_scale)) { + dispatch(setCfgScale(cfg_scale)); + } + if (isValidModel(model)) { + dispatch(modelSelected(model)); + } + if (isValidPositivePrompt(positive_conditioning)) { + dispatch(setPositivePrompt(positive_conditioning)); + } + if (isValidNegativePrompt(negative_conditioning)) { + dispatch(setNegativePrompt(negative_conditioning)); + } + if (isValidScheduler(scheduler)) { + dispatch(setScheduler(scheduler)); + } + if (isValidSeed(seed)) { + dispatch(setSeed(seed)); + } + if (isValidSteps(steps)) { + dispatch(setSteps(steps)); + } + if (isValidWidth(width)) { + dispatch(setWidth(width)); + } + if (isValidHeight(height)) { + dispatch(setHeight(height)); + } + if (isValidStrength(strength)) { + dispatch(setImg2imgStrength(strength)); + } + + allParameterSetToast(); + }, + [allParameterNotSetToast, allParameterSetToast, dispatch] + ); + + return { + recallBothPrompts, + recallPositivePrompt, + recallNegativePrompt, + recallSeed, + recallInitialImage, + recallCfgScale, + recallModel, + recallScheduler, + recallSteps, + recallWidth, + recallHeight, + recallStrength, + recallAllParameters, + sendToImageToImage, + }; +}; diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index 75e724ed4c..6420950e4a 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -1,44 +1,53 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; -import * as InvokeAI from 'app/types/invokeai'; -import promptToString from 'common/util/promptToString'; import { clamp, sortBy } from 'lodash-es'; -import { setAllParametersReducer } from './setAllParametersReducer'; import { receivedModels } from 'services/thunks/model'; import { Scheduler } from 'app/constants'; import { ImageDTO } from 'services/api'; import { configChanged } from 'features/system/store/configSlice'; +import { + CfgScaleParam, + HeightParam, + ModelParam, + NegativePromptParam, + PositivePromptParam, + SchedulerParam, + SeedParam, + StepsParam, + StrengthParam, + WidthParam, +} from './parameterZodSchemas'; export interface GenerationState { - cfgScale: number; - height: number; - img2imgStrength: number; + cfgScale: CfgScaleParam; + height: HeightParam; + img2imgStrength: StrengthParam; infillMethod: string; initialImage?: ImageDTO; iterations: number; perlin: number; - positivePrompt: string; - negativePrompt: string; - scheduler: Scheduler; + positivePrompt: PositivePromptParam; + negativePrompt: NegativePromptParam; + scheduler: SchedulerParam; seamBlur: number; seamSize: number; seamSteps: number; seamStrength: number; - seed: number; + seed: SeedParam; seedWeights: string; shouldFitToWidthHeight: boolean; shouldGenerateVariations: boolean; shouldRandomizeSeed: boolean; shouldUseNoiseSettings: boolean; - steps: number; + steps: StepsParam; threshold: number; tileSize: number; variationAmount: number; - width: number; + width: WidthParam; shouldUseSymmetry: boolean; horizontalSymmetrySteps: number; verticalSymmetrySteps: number; - model: string; + model: ModelParam; shouldUseSeamless: boolean; seamlessXAxis: boolean; seamlessYAxis: boolean; @@ -84,27 +93,11 @@ export const generationSlice = createSlice({ name: 'generation', initialState, reducers: { - setPositivePrompt: ( - state, - action: PayloadAction - ) => { - const newPrompt = action.payload; - if (typeof newPrompt === 'string') { - state.positivePrompt = newPrompt; - } else { - state.positivePrompt = promptToString(newPrompt); - } + setPositivePrompt: (state, action: PayloadAction) => { + state.positivePrompt = action.payload; }, - setNegativePrompt: ( - state, - action: PayloadAction - ) => { - const newPrompt = action.payload; - if (typeof newPrompt === 'string') { - state.negativePrompt = newPrompt; - } else { - state.negativePrompt = promptToString(newPrompt); - } + setNegativePrompt: (state, action: PayloadAction) => { + state.negativePrompt = action.payload; }, setIterations: (state, action: PayloadAction) => { state.iterations = action.payload; @@ -175,7 +168,6 @@ export const generationSlice = createSlice({ state.shouldGenerateVariations = true; state.variationAmount = 0; }, - allParametersSet: setAllParametersReducer, resetParametersState: (state) => { return { ...state, @@ -279,7 +271,6 @@ export const { setSeamless, setSeamlessXAxis, setSeamlessYAxis, - allParametersSet, } = generationSlice.actions; export default generationSlice.reducer; diff --git a/invokeai/frontend/web/src/features/parameters/store/parameterZodSchemas.ts b/invokeai/frontend/web/src/features/parameters/store/parameterZodSchemas.ts new file mode 100644 index 0000000000..b99e57bfbb --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/store/parameterZodSchemas.ts @@ -0,0 +1,156 @@ +import { NUMPY_RAND_MAX, SCHEDULERS } from 'app/constants'; +import { z } from 'zod'; + +/** + * These zod schemas should match the pydantic node schemas. + * + * Parameters only need schemas if we want to recall them from metadata. + * + * Each parameter needs: + * - a zod schema + * - a type alias, inferred from the zod schema + * - a combo validation/type guard function, which returns true if the value is valid + */ + +/** + * Zod schema for positive prompt parameter + */ +export const zPositivePrompt = z.string(); +/** + * Type alias for positive prompt parameter, inferred from its zod schema + */ +export type PositivePromptParam = z.infer; +/** + * Validates/type-guards a value as a positive prompt parameter + */ +export const isValidPositivePrompt = ( + val: unknown +): val is PositivePromptParam => zPositivePrompt.safeParse(val).success; + +/** + * Zod schema for negative prompt parameter + */ +export const zNegativePrompt = z.string(); +/** + * Type alias for negative prompt parameter, inferred from its zod schema + */ +export type NegativePromptParam = z.infer; +/** + * Validates/type-guards a value as a negative prompt parameter + */ +export const isValidNegativePrompt = ( + val: unknown +): val is NegativePromptParam => zNegativePrompt.safeParse(val).success; + +/** + * Zod schema for steps parameter + */ +export const zSteps = z.number().int().min(1); +/** + * Type alias for steps parameter, inferred from its zod schema + */ +export type StepsParam = z.infer; +/** + * Validates/type-guards a value as a steps parameter + */ +export const isValidSteps = (val: unknown): val is StepsParam => + zSteps.safeParse(val).success; + +/** + * Zod schema for CFG scale parameter + */ +export const zCfgScale = z.number().min(1); +/** + * Type alias for CFG scale parameter, inferred from its zod schema + */ +export type CfgScaleParam = z.infer; +/** + * Validates/type-guards a value as a CFG scale parameter + */ +export const isValidCfgScale = (val: unknown): val is CfgScaleParam => + zCfgScale.safeParse(val).success; + +/** + * Zod schema for scheduler parameter + */ +export const zScheduler = z.enum(SCHEDULERS); +/** + * Type alias for scheduler parameter, inferred from its zod schema + */ +export type SchedulerParam = z.infer; +/** + * Validates/type-guards a value as a scheduler parameter + */ +export const isValidScheduler = (val: unknown): val is SchedulerParam => + zScheduler.safeParse(val).success; + +/** + * Zod schema for seed parameter + */ +export const zSeed = z.number().int().min(0).max(NUMPY_RAND_MAX); +/** + * Type alias for seed parameter, inferred from its zod schema + */ +export type SeedParam = z.infer; +/** + * Validates/type-guards a value as a seed parameter + */ +export const isValidSeed = (val: unknown): val is SeedParam => + zSeed.safeParse(val).success; + +/** + * Zod schema for width parameter + */ +export const zWidth = z.number().multipleOf(8).min(64); +/** + * Type alias for width parameter, inferred from its zod schema + */ +export type WidthParam = z.infer; +/** + * Validates/type-guards a value as a width parameter + */ +export const isValidWidth = (val: unknown): val is WidthParam => + zWidth.safeParse(val).success; + +/** + * Zod schema for height parameter + */ +export const zHeight = z.number().multipleOf(8).min(64); +/** + * Type alias for height parameter, inferred from its zod schema + */ +export type HeightParam = z.infer; +/** + * Validates/type-guards a value as a height parameter + */ +export const isValidHeight = (val: unknown): val is HeightParam => + zHeight.safeParse(val).success; + +/** + * Zod schema for model parameter + * TODO: Make this a dynamically generated enum? + */ +export const zModel = z.string(); +/** + * Type alias for model parameter, inferred from its zod schema + */ +export type ModelParam = z.infer; +/** + * Validates/type-guards a value as a model parameter + */ +export const isValidModel = (val: unknown): val is ModelParam => + zModel.safeParse(val).success; + +/** + * Zod schema for l2l strength parameter + */ +export const zStrength = z.number().min(0).max(1); +/** + * Type alias for l2l strength parameter, inferred from its zod schema + */ +export type StrengthParam = z.infer; +/** + * Validates/type-guards a value as a l2l strength parameter + */ +export const isValidStrength = (val: unknown): val is StrengthParam => + zStrength.safeParse(val).success; diff --git a/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts b/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts deleted file mode 100644 index 8f06c7d0ef..0000000000 --- a/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Draft, PayloadAction } from '@reduxjs/toolkit'; -import { GenerationState } from './generationSlice'; -import { ImageDTO, ImageToImageInvocation } from 'services/api'; -import { isScheduler } from 'app/constants'; - -export const setAllParametersReducer = ( - state: Draft, - action: PayloadAction -) => { - const metadata = action.payload?.metadata; - - if (!metadata) { - return; - } - - // not sure what this list should be - if ( - metadata.type === 't2l' || - metadata.type === 'l2l' || - metadata.type === 'inpaint' - ) { - const { - cfg_scale, - height, - model, - positive_conditioning, - negative_conditioning, - scheduler, - seed, - steps, - width, - } = metadata; - - if (cfg_scale !== undefined) { - state.cfgScale = Number(cfg_scale); - } - if (height !== undefined) { - state.height = Number(height); - } - if (model !== undefined) { - state.model = String(model); - } - if (positive_conditioning !== undefined) { - state.positivePrompt = String(positive_conditioning); - } - if (negative_conditioning !== undefined) { - state.negativePrompt = String(negative_conditioning); - } - if (scheduler !== undefined) { - const schedulerString = String(scheduler); - if (isScheduler(schedulerString)) { - state.scheduler = schedulerString; - } - } - if (seed !== undefined) { - state.seed = Number(seed); - state.shouldRandomizeSeed = false; - } - if (steps !== undefined) { - state.steps = Number(steps); - } - if (width !== undefined) { - state.width = Number(width); - } - } - - if (metadata.type === 'l2l') { - const { fit, image } = metadata as ImageToImageInvocation; - - if (fit !== undefined) { - state.shouldFitToWidthHeight = Boolean(fit); - } - // if (image !== undefined) { - // state.initialImage = image; - // } - } -}; diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock index 0df6d3c9a3..356f7466fe 100644 --- a/invokeai/frontend/web/yarn.lock +++ b/invokeai/frontend/web/yarn.lock @@ -6877,6 +6877,11 @@ z-schema@~5.0.2: optionalDependencies: commander "^10.0.0" +zod@^3.21.4: + version "3.21.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" + integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== + zustand@^4.3.1: version "4.3.7" resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.7.tgz#501b1f0393a7f1d103332e45ab574be5747fedce" From e06ba40795d97dcbfe40465ad4a65a59670a7d79 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 31 May 2023 21:07:36 +1000 Subject: [PATCH 21/29] fix(ui): do not allow `dpmpp_2s` to be used ever it doesn't work for the img2img pipelines, but the implemented conditional display could break the scheduler selection dropdown. simple fix until diffusers merges the fix - never use this scheduler. --- .../Parameters/Core/ParamScheduler.tsx | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamScheduler.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamScheduler.tsx index 5b6b07803a..f4413c4cf6 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamScheduler.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamScheduler.tsx @@ -5,21 +5,18 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAICustomSelect from 'common/components/IAICustomSelect'; import { generationSelector } from 'features/parameters/store/generationSelectors'; import { setScheduler } from 'features/parameters/store/generationSlice'; -import { - activeTabNameSelector, - uiSelector, -} from 'features/ui/store/uiSelectors'; +import { uiSelector } from 'features/ui/store/uiSelectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const selector = createSelector( - [uiSelector, generationSelector, activeTabNameSelector], - (ui, generation, activeTabName) => { - const allSchedulers = ['img2img', 'unifiedCanvas'].includes(activeTabName) - ? ui.schedulers.filter((scheduler) => { - return !['dpmpp_2s'].includes(scheduler); - }) - : ui.schedulers; + [uiSelector, generationSelector], + (ui, generation) => { + // TODO: DPMSolverSinglestepScheduler is fixed in https://github.com/huggingface/diffusers/pull/3413 + // but we need to wait for the next release before removing this special handling. + const allSchedulers = ui.schedulers.filter((scheduler) => { + return !['dpmpp_2s'].includes(scheduler); + }); return { scheduler: generation.scheduler, From c9e621093e7d3c9fa303816905901fae7c3e66d1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 31 May 2023 23:00:46 +1000 Subject: [PATCH 22/29] fix(ui): fix looping gallery images fetch The gallery could get in a state where it thought it had just reached the end of the list and endlessly fetches more images, if there are no more images to fetch (weird I know). Add some logic to remove the `end reached` handler when there are no more images to load. --- .../components/ImageGalleryContent.tsx | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx index ce7eb00404..77f42a11a6 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx @@ -1,8 +1,6 @@ import { Box, ButtonGroup, - Checkbox, - CheckboxGroup, Flex, FlexProps, Grid, @@ -32,18 +30,13 @@ import { memo, useCallback, useEffect, + useMemo, useRef, useState, } from 'react'; import { useTranslation } from 'react-i18next'; import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs'; -import { - FaFilter, - FaImage, - FaImages, - FaServer, - FaWrench, -} from 'react-icons/fa'; +import { FaImage, FaServer, FaWrench } from 'react-icons/fa'; import { MdPhotoLibrary } from 'react-icons/md'; import HoverableImage from './HoverableImage'; @@ -53,7 +46,6 @@ import { RootState } from 'app/store/store'; import { Virtuoso, VirtuosoGrid } from 'react-virtuoso'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { uiSelector } from 'features/ui/store/uiSelectors'; -import { ImageCategory } from 'services/api'; import { ASSETS_CATEGORIES, IMAGE_CATEGORIES, @@ -61,7 +53,6 @@ import { selectImagesAll, } from '../store/imagesSlice'; import { receivedPageOfImages } from 'services/thunks/image'; -import { capitalize } from 'lodash-es'; const categorySelector = createSelector( [(state: RootState) => state], @@ -144,6 +135,13 @@ const ImageGalleryContent = () => { dispatch(receivedPageOfImages()); }, [dispatch]); + const handleEndReached = useMemo(() => { + if (areMoreImagesAvailable && !isLoading) { + return handleLoadMoreImages; + } + return undefined; + }, [areMoreImagesAvailable, handleLoadMoreImages, isLoading]); + const handleChangeGalleryImageMinimumWidth = (v: number) => { dispatch(setGalleryImageMinimumWidth(v)); }; @@ -172,17 +170,6 @@ const ImageGalleryContent = () => { } }, []); - const handleEndReached = useCallback(() => { - handleLoadMoreImages(); - }, [handleLoadMoreImages]); - - const handleCategoriesChanged = useCallback( - (newCategories: ImageCategory[]) => { - dispatch(imageCategoriesChanged(newCategories)); - }, - [dispatch] - ); - const handleClickImagesCategory = useCallback(() => { dispatch(imageCategoriesChanged(IMAGE_CATEGORIES)); }, [dispatch]); From d66979073bc0fe4ee2639b0cdc935782aacb6a16 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Thu, 1 Jun 2023 10:14:39 -0400 Subject: [PATCH 23/29] add optional config for settings modal --- .../SettingsModal/SettingsModal.tsx | 102 ++++++++++++------ 1 file changed, 67 insertions(+), 35 deletions(-) diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx index 54556124c9..4cfe35081b 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -35,7 +35,13 @@ import { } from 'features/ui/store/uiSlice'; import { UIState } from 'features/ui/store/uiTypes'; import { isEqual } from 'lodash-es'; -import { ChangeEvent, cloneElement, ReactElement, useCallback } from 'react'; +import { + ChangeEvent, + cloneElement, + ReactElement, + useCallback, + useEffect, +} from 'react'; import { useTranslation } from 'react-i18next'; import { VALID_LOG_LEVELS } from 'app/logging/useLogger'; import { LogLevelName } from 'roarr'; @@ -85,15 +91,33 @@ const modalSectionStyles: ChakraProps['sx'] = { borderRadius: 'base', }; +type ConfigOptions = { + shouldShowDeveloperSettings: boolean; + shouldShowResetWebUiText: boolean; + shouldShowBetaLayout: boolean; +}; + type SettingsModalProps = { /* The button to open the Settings Modal */ children: ReactElement; + config?: ConfigOptions; }; -const SettingsModal = ({ children }: SettingsModalProps) => { +const SettingsModal = ({ children, config }: SettingsModalProps) => { const dispatch = useAppDispatch(); const { t } = useTranslation(); + const shouldShowBetaLayout = config?.shouldShowBetaLayout ?? true; + const shouldShowDeveloperSettings = + config?.shouldShowDeveloperSettings ?? true; + const shouldShowResetWebUiText = config?.shouldShowResetWebUiText ?? true; + + useEffect(() => { + if (!shouldShowDeveloperSettings) { + dispatch(shouldLogToConsoleChanged(false)); + } + }, [shouldShowDeveloperSettings, dispatch]); + const { isOpen: isSettingsModalOpen, onOpen: onSettingsModalOpen, @@ -189,13 +213,15 @@ const SettingsModal = ({ children }: SettingsModalProps) => { dispatch(setShouldDisplayGuides(e.target.checked)) } /> - ) => - dispatch(setShouldUseCanvasBetaLayout(e.target.checked)) - } - /> + {shouldShowBetaLayout && ( + ) => + dispatch(setShouldUseCanvasBetaLayout(e.target.checked)) + } + /> + )} { /> - - {t('settings.developer')} - - - ) => - dispatch(setEnableImageDebugging(e.target.checked)) - } - /> - + {shouldShowDeveloperSettings && ( + + {t('settings.developer')} + + + ) => + dispatch(setEnableImageDebugging(e.target.checked)) + } + /> + + )} {t('settings.resetWebUI')} {t('settings.resetWebUI')} - {t('settings.resetWebUIDesc1')} - {t('settings.resetWebUIDesc2')} + {shouldShowResetWebUiText && ( + <> + {t('settings.resetWebUIDesc1')} + {t('settings.resetWebUIDesc2')} + + )} From 8ae1eaaccc7edc81c264448069206b070249ebbb Mon Sep 17 00:00:00 2001 From: mickr777 <115216705+mickr777@users.noreply.github.com> Date: Fri, 2 Jun 2023 14:19:02 +1000 Subject: [PATCH 24/29] Add Progress bar under invoke Button Find on some screens the progress bar at top of screen gets cut off --- .../parameters/components/ProcessButtons/InvokeButton.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx index 7042bc8c41..9efebf7ef2 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx @@ -12,6 +12,7 @@ import { useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { FaPlay } from 'react-icons/fa'; +import ProgressBar from 'features/system/components/ProgressBar'; interface InvokeButton extends Omit { @@ -43,7 +44,7 @@ export default function InvokeButton(props: InvokeButton) { ); return ( - + {iconButton ? ( )} + {!isReady && } ); } From 2bdb65537595a47d9b313d243edba27282af3cf4 Mon Sep 17 00:00:00 2001 From: mickr777 <115216705+mickr777@users.noreply.github.com> Date: Fri, 2 Jun 2023 14:59:10 +1000 Subject: [PATCH 25/29] Change to absolute --- .../ProcessButtons/InvokeButton.tsx | 78 +++++++++++-------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx index 9efebf7ef2..963581d2fe 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx @@ -45,38 +45,52 @@ export default function InvokeButton(props: InvokeButton) { return ( - {iconButton ? ( - } - isDisabled={!isReady} - onClick={handleInvoke} - flexGrow={1} - w="100%" - tooltip={t('parameters.invoke')} - tooltipProps={{ placement: 'bottom' }} - colorScheme="accent" - id="invoke-button" - {...rest} - /> - ) : ( - - Invoke - - )} - {!isReady && } + + {iconButton ? ( + } + isDisabled={!isReady} + onClick={handleInvoke} + flexGrow={1} + w="100%" + tooltip={t('parameters.invoke')} + tooltipProps={{ placement: 'bottom' }} + colorScheme="accent" + id="invoke-button" + {...rest} + /> + ) : ( + + Invoke + + )} + {!isReady && ( + + + + )} + ); } From f143fb7254f697a884a0f4d84b20d7aa7652a737 Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Fri, 2 Jun 2023 17:24:40 +1200 Subject: [PATCH 26/29] feat: Make Invoke Button also the progress bar --- .../frontend/web/src/app/components/App.tsx | 34 +++++++++---------- .../ProcessButtons/InvokeButton.tsx | 7 +++- .../system/components/ProgressBar.tsx | 3 +- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 40554356b1..33fa57f0b3 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -1,27 +1,26 @@ -import ImageUploader from 'common/components/ImageUploader'; -import SiteHeader from 'features/system/components/SiteHeader'; -import ProgressBar from 'features/system/components/ProgressBar'; -import InvokeTabs from 'features/ui/components/InvokeTabs'; -import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton'; -import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons'; import { Box, Flex, Grid, Portal } from '@chakra-ui/react'; -import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants'; +import { useLogger } from 'app/logging/useLogger'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { PartialAppConfig } from 'app/types/invokeai'; +import ImageUploader from 'common/components/ImageUploader'; +import Loading from 'common/components/Loading/Loading'; import GalleryDrawer from 'features/gallery/components/GalleryPanel'; import Lightbox from 'features/lightbox/components/Lightbox'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { memo, ReactNode, useCallback, useEffect, useState } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import Loading from 'common/components/Loading/Loading'; -import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady'; -import { PartialAppConfig } from 'app/types/invokeai'; -import { configChanged } from 'features/system/store/configSlice'; +import SiteHeader from 'features/system/components/SiteHeader'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { useLogger } from 'app/logging/useLogger'; -import ParametersDrawer from 'features/ui/components/ParametersDrawer'; +import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady'; +import { configChanged } from 'features/system/store/configSlice'; import { languageSelector } from 'features/system/store/systemSelectors'; +import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton'; +import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons'; +import InvokeTabs from 'features/ui/components/InvokeTabs'; +import ParametersDrawer from 'features/ui/components/ParametersDrawer'; +import { AnimatePresence, motion } from 'framer-motion'; import i18n from 'i18n'; -import Toaster from './Toaster'; +import { ReactNode, memo, useCallback, useEffect, useState } from 'react'; +import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants'; import GlobalHotkeys from './GlobalHotkeys'; +import Toaster from './Toaster'; const DEFAULT_CONFIG = {}; @@ -76,7 +75,6 @@ const App = ({ {isLightboxEnabled && } - { @@ -59,6 +59,7 @@ export default function InvokeButton(props: InvokeButton) { tooltipProps={{ placement: 'bottom' }} colorScheme="accent" id="invoke-button" + zIndex={2} {...rest} /> ) : ( @@ -72,6 +73,7 @@ export default function InvokeButton(props: InvokeButton) { colorScheme="accent" id="invoke-button" fontWeight={700} + zIndex={2} {...rest} > Invoke @@ -84,7 +86,10 @@ export default function InvokeButton(props: InvokeButton) { bottom: '0', left: '0', right: '0', + height: '100%', zIndex: 1, + borderRadius: 4, + overflow: 'clip', }} > diff --git a/invokeai/frontend/web/src/features/system/components/ProgressBar.tsx b/invokeai/frontend/web/src/features/system/components/ProgressBar.tsx index 4584bee644..140a8b5978 100644 --- a/invokeai/frontend/web/src/features/system/components/ProgressBar.tsx +++ b/invokeai/frontend/web/src/features/system/components/ProgressBar.tsx @@ -5,7 +5,6 @@ import { SystemState } from 'features/system/store/systemSlice'; import { isEqual } from 'lodash-es'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -import { PROGRESS_BAR_THICKNESS } from 'theme/util/constants'; import { systemSelector } from '../store/systemSelectors'; const progressBarSelector = createSelector( @@ -35,7 +34,7 @@ const ProgressBar = () => { value={value} aria-label={t('accessibility.invokeProgressBar')} isIndeterminate={isProcessing && !currentStatusHasSteps} - height={PROGRESS_BAR_THICKNESS} + height="full" /> ); }; From 47301e6f85e70a396fd89efaf2767cef93819b50 Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Fri, 2 Jun 2023 17:33:38 +1200 Subject: [PATCH 27/29] fix: Do the same without zIndex --- .../ProcessButtons/InvokeButton.tsx | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx index af88228be3..34bac3600d 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx @@ -46,6 +46,20 @@ export default function InvokeButton(props: InvokeButton) { return ( + {!isReady && ( + + + + )} {iconButton ? ( ) : ( @@ -73,28 +86,12 @@ export default function InvokeButton(props: InvokeButton) { colorScheme="accent" id="invoke-button" fontWeight={700} - zIndex={2} + background={!isReady ? 'none' : 'accent.700'} {...rest} > Invoke )} - {!isReady && ( - - - - )} ); From ea9cf0476570da2387648b33e1cc34c50906ff5c Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Fri, 2 Jun 2023 17:36:14 +1200 Subject: [PATCH 28/29] fix: Remove progress bg instead of altering button bg --- .../parameters/components/ProcessButtons/InvokeButton.tsx | 2 +- invokeai/frontend/web/src/theme/components/progress.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx index 34bac3600d..4ada615628 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx @@ -55,6 +55,7 @@ export default function InvokeButton(props: InvokeButton) { right: '0', height: '100%', overflow: 'clip', + borderRadius: 4, }} > @@ -86,7 +87,6 @@ export default function InvokeButton(props: InvokeButton) { colorScheme="accent" id="invoke-button" fontWeight={700} - background={!isReady ? 'none' : 'accent.700'} {...rest} > Invoke diff --git a/invokeai/frontend/web/src/theme/components/progress.ts b/invokeai/frontend/web/src/theme/components/progress.ts index fa6b5b57c5..87b6b7af01 100644 --- a/invokeai/frontend/web/src/theme/components/progress.ts +++ b/invokeai/frontend/web/src/theme/components/progress.ts @@ -20,7 +20,7 @@ const invokeAIFilledTrack = defineStyle((_props) => ({ const invokeAITrack = defineStyle((_props) => { return { - bg: 'base.800', + bg: 'none', }; }); From 7620bacc01e7381c6720eb1cff19f610e01d2956 Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Fri, 2 Jun 2023 17:55:15 +1200 Subject: [PATCH 29/29] feat: Add temporary NodeInvokeButton --- .../components/panels/TopCenterPanel.tsx | 10 +- .../nodes/components/ui/NodeInvokeButton.tsx | 96 +++++++++++++++++++ 2 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/components/ui/NodeInvokeButton.tsx diff --git a/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx index b97bf423e1..b961a9f403 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx @@ -1,18 +1,14 @@ import { HStack } from '@chakra-ui/react'; -import { userInvoked } from 'app/store/actions'; import { useAppDispatch } from 'app/store/storeHooks'; import IAIButton from 'common/components/IAIButton'; import { memo, useCallback } from 'react'; import { Panel } from 'reactflow'; import { receivedOpenAPISchema } from 'services/thunks/schema'; +import NodeInvokeButton from '../ui/NodeInvokeButton'; const TopCenterPanel = () => { const dispatch = useAppDispatch(); - const handleInvoke = useCallback(() => { - dispatch(userInvoked('nodes')); - }, [dispatch]); - const handleReloadSchema = useCallback(() => { dispatch(receivedOpenAPISchema()); }, [dispatch]); @@ -20,9 +16,7 @@ const TopCenterPanel = () => { return ( - - Will it blend? - + Reload Schema diff --git a/invokeai/frontend/web/src/features/nodes/components/ui/NodeInvokeButton.tsx b/invokeai/frontend/web/src/features/nodes/components/ui/NodeInvokeButton.tsx new file mode 100644 index 0000000000..4b916abd2e --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/ui/NodeInvokeButton.tsx @@ -0,0 +1,96 @@ +import { Box } from '@chakra-ui/react'; +import { readinessSelector } from 'app/selectors/readinessSelector'; +import { userInvoked } from 'app/store/actions'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAIButton, { IAIButtonProps } from 'common/components/IAIButton'; +import IAIIconButton, { + IAIIconButtonProps, +} from 'common/components/IAIIconButton'; +import ProgressBar from 'features/system/components/ProgressBar'; +import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; +import { useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { FaPlay } from 'react-icons/fa'; + +interface InvokeButton + extends Omit { + iconButton?: boolean; +} + +export default function NodeInvokeButton(props: InvokeButton) { + const { iconButton = false, ...rest } = props; + const dispatch = useAppDispatch(); + const { isReady } = useAppSelector(readinessSelector); + const activeTabName = useAppSelector(activeTabNameSelector); + + const handleInvoke = useCallback(() => { + dispatch(userInvoked('nodes')); + }, [dispatch]); + + const { t } = useTranslation(); + + useHotkeys( + ['ctrl+enter', 'meta+enter'], + handleInvoke, + { + enabled: () => isReady, + preventDefault: true, + enableOnFormTags: ['input', 'textarea', 'select'], + }, + [isReady, activeTabName] + ); + + return ( + + + {!isReady && ( + + + + )} + {iconButton ? ( + } + isDisabled={!isReady} + onClick={handleInvoke} + flexGrow={1} + w="100%" + tooltip={t('parameters.invoke')} + tooltipProps={{ placement: 'bottom' }} + colorScheme="accent" + id="invoke-button" + {...rest} + /> + ) : ( + + Invoke + + )} + + + ); +}