Merge branch 'main' into lstein/config-management-fixes

This commit is contained in:
Lincoln Stein 2023-06-02 22:56:43 -04:00 committed by GitHub
commit ff63433591
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1692 additions and 1231 deletions

View File

@ -173,7 +173,7 @@ class TextToLatentsInvocation(BaseInvocation):
negative_conditioning: Optional[ConditioningField] = Field(description="Negative conditioning for generation") negative_conditioning: Optional[ConditioningField] = Field(description="Negative conditioning for generation")
noise: Optional[LatentsField] = Field(description="The noise to use") 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") 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" ) scheduler: SAMPLER_NAME_VALUES = Field(default="euler", description="The scheduler to use" )
model: str = Field(default="", description="The model to use (currently ignored)") model: str = Field(default="", description="The model to use (currently ignored)")
control: Union[ControlField, list[ControlField]] = Field(default=None, description="The control to use") control: Union[ControlField, list[ControlField]] = Field(default=None, description="The control to use")
@ -366,7 +366,7 @@ class LatentsToLatentsInvocation(TextToLatentsInvocation):
# Inputs # Inputs
latents: Optional[LatentsField] = Field(description="The latents to use as a base image") 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 # Schema customisation
class Config(InvocationConfig): class Config(InvocationConfig):

View File

@ -75,10 +75,10 @@ class AddsMaskLatents:
initial_image_latents: torch.Tensor initial_image_latents: torch.Tensor
def __call__( 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: ) -> torch.Tensor:
model_input = self.add_mask_channels(latents) 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): def add_mask_channels(self, latents):
batch_size = latents.size(0) batch_size = latents.size(0)

View File

@ -101,7 +101,8 @@
"serialize-error": "^11.0.0", "serialize-error": "^11.0.0",
"socket.io-client": "^4.6.0", "socket.io-client": "^4.6.0",
"use-image": "^1.1.0", "use-image": "^1.1.0",
"uuid": "^9.0.0" "uuid": "^9.0.0",
"zod": "^3.21.4"
}, },
"peerDependencies": { "peerDependencies": {
"@chakra-ui/cli": "^2.4.0", "@chakra-ui/cli": "^2.4.0",

View File

@ -454,6 +454,8 @@
"height": "Height", "height": "Height",
"scheduler": "Scheduler", "scheduler": "Scheduler",
"seed": "Seed", "seed": "Seed",
"boundingBoxWidth": "Bounding Box Width",
"boundingBoxHeight": "Bounding Box Height",
"imageToImage": "Image to Image", "imageToImage": "Image to Image",
"randomizeSeed": "Randomize Seed", "randomizeSeed": "Randomize Seed",
"shuffle": "Shuffle Seed", "shuffle": "Shuffle Seed",
@ -566,6 +568,8 @@
"canvasMerged": "Canvas Merged", "canvasMerged": "Canvas Merged",
"sentToImageToImage": "Sent To Image To Image", "sentToImageToImage": "Sent To Image To Image",
"sentToUnifiedCanvas": "Sent to Unified Canvas", "sentToUnifiedCanvas": "Sent to Unified Canvas",
"parameterSet": "Parameter set",
"parameterNotSet": "Parameter not set",
"parametersSet": "Parameters Set", "parametersSet": "Parameters Set",
"parametersNotSet": "Parameters Not Set", "parametersNotSet": "Parameters Not Set",
"parametersNotSetDesc": "No metadata found for this image.", "parametersNotSetDesc": "No metadata found for this image.",

View File

@ -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 { 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 GalleryDrawer from 'features/gallery/components/GalleryPanel';
import Lightbox from 'features/lightbox/components/Lightbox'; import Lightbox from 'features/lightbox/components/Lightbox';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import SiteHeader from 'features/system/components/SiteHeader';
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 { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useLogger } from 'app/logging/useLogger'; import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady';
import ParametersDrawer from 'features/ui/components/ParametersDrawer'; import { configChanged } from 'features/system/store/configSlice';
import { languageSelector } from 'features/system/store/systemSelectors'; 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 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 GlobalHotkeys from './GlobalHotkeys';
import Toaster from './Toaster';
const DEFAULT_CONFIG = {}; const DEFAULT_CONFIG = {};
@ -76,7 +75,6 @@ const App = ({
<Grid w="100vw" h="100vh" position="relative" overflow="hidden"> <Grid w="100vw" h="100vh" position="relative" overflow="hidden">
{isLightboxEnabled && <Lightbox />} {isLightboxEnabled && <Lightbox />}
<ImageUploader> <ImageUploader>
<ProgressBar />
<Grid <Grid
gap={4} gap={4}
p={4} p={4}

View File

@ -21,25 +21,11 @@ export const SCHEDULERS = [
export type Scheduler = (typeof SCHEDULERS)[number]; 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<number> = Array.from(Array(64)).map(
(_x, i) => (i + 1) * 64
);
// Valid image heights
export const HEIGHTS: Array<number> = Array.from(Array(64)).map(
(_x, i) => (i + 1) * 64
);
// Valid upscaling levels // Valid upscaling levels
export const UPSCALING_LEVELS: Array<{ key: string; value: number }> = [ export const UPSCALING_LEVELS: Array<{ key: string; value: number }> = [
{ key: '2x', value: 2 }, { key: '2x', value: 2 },
{ key: '4x', value: 4 }, { key: '4x', value: 4 },
]; ];
export const NUMPY_RAND_MIN = 0; export const NUMPY_RAND_MIN = 0;
export const NUMPY_RAND_MAX = 2147483647; export const NUMPY_RAND_MAX = 2147483647;

View File

@ -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 { InvokeTabName } from 'features/ui/store/tabMap';
import { IRect } from 'konva/lib/types';
import { ImageResponseMetadata, ResourceOrigin } from 'services/api';
import { O } from 'ts-toolbelt'; import { O } from 'ts-toolbelt';
/** // These are old types from the model management UI
* 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.
*/
export type PromptItem = { // export type ModelStatus = 'active' | 'cached' | 'not loaded';
prompt: string;
weight: number;
};
// TECHDEBT: We need to retain compatibility with plain prompt strings and the structure Prompt type // export type Model = {
export type Prompt = Array<PromptItem> | string; // status: ModelStatus;
// description: string;
export type SeedWeightPair = { // weights: string;
seed: number; // config?: string;
weight: number; // vae?: string;
}; // width?: number;
// height?: number;
export type SeedWeights = Array<SeedWeightPair>; // default?: boolean;
// format?: string;
// All generated images contain these metadata.
export type CommonGeneratedImageMetadata = {
postprocessing: null | Array<ESRGANMetadata | FacetoolMetadata>;
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<string, never>; // 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 DiffusersModel = {
* ResultImage // status: ModelStatus;
*/ // description: string;
// export ty`pe Image = { // repo_id?: string;
// path?: string;
// vae?: {
// repo_id?: string;
// path?: string;
// };
// format?: string;
// default?: boolean;
// };
// export type ModelList = Record<string, Model & DiffusersModel>;
// export type FoundModel = {
// name: string; // name: string;
// type: image_origin; // location: string;
// url: string;
// thumbnail: string;
// metadata: ImageResponseMetadata;
// }; // };
// export const isInvokeAIImage = (obj: Image | SelectedImage): obj is Image => { // export type InvokeModelConfigProps = {
// if ('url' in obj && 'thumbnail' in obj) { // name: string | undefined;
// return true; // description: string | undefined;
// } // config: string | undefined;
// weights: string | undefined;
// return false; // vae: string | undefined;
// width: number | undefined;
// height: number | undefined;
// default: boolean | undefined;
// format: string | undefined;
// }; // };
/** // export type InvokeDiffusersModelConfigProps = {
* Types related to the system status. // name: string | undefined;
*/ // description: string | undefined;
// repo_id: string | undefined;
// // This represents the processing status of the backend. // path: string | undefined;
// export type SystemStatus = { // default: boolean | undefined;
// isProcessing: boolean; // format: string | undefined;
// currentStep: number; // vae: {
// totalSteps: number; // repo_id: string | undefined;
// currentIteration: number; // path: string | undefined;
// totalIterations: number; // };
// currentStatus: string;
// currentStatusHasSteps: boolean;
// hasError: boolean;
// }; // };
// export type SystemGenerationMetadata = { // export type InvokeModelConversionProps = {
// model: string; // model_name: string;
// model_weights?: string; // save_location: string;
// model_id?: string; // custom_location: string | null;
// model_hash: string;
// app_id: string;
// app_version: string;
// }; // };
// export type SystemConfig = SystemGenerationMetadata & { // export type InvokeModelMergingProps = {
// model_list: ModelList; // models_to_merge: string[];
// infill_methods: 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<string, Model & DiffusersModel>;
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<Image, 'uuid'> & {
boundingBox?: IRect;
generationMode: InvokeTabName;
};
export type ImageUploadResponse = {
// image: Omit<Image, 'uuid' | 'metadata' | 'category'>;
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 * A disable-able application feature
*/ */
@ -322,7 +88,8 @@ export type AppFeature =
| 'githubLink' | 'githubLink'
| 'discordLink' | 'discordLink'
| 'bugLink' | 'bugLink'
| 'localization'; | 'localization'
| 'consoleLogging';
/** /**
* A disable-able Stable Diffusion feature * A disable-able Stable Diffusion feature
@ -351,6 +118,7 @@ export type AppConfig = {
disabledSDFeatures: SDFeature[]; disabledSDFeatures: SDFeature[];
canRestoreDeletedImagesFromBin: boolean; canRestoreDeletedImagesFromBin: boolean;
sd: { sd: {
defaultModel?: string;
iterations: { iterations: {
initial: number; initial: number;
min: number; min: number;

View File

@ -21,9 +21,12 @@ import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { memo } from 'react'; import { memo } from 'react';
export type ItemTooltips = { [key: string]: string };
type IAICustomSelectProps = { type IAICustomSelectProps = {
label?: string; label?: string;
items: string[]; items: string[];
itemTooltips?: ItemTooltips;
selectedItem: string; selectedItem: string;
setSelectedItem: (v: string | null | undefined) => void; setSelectedItem: (v: string | null | undefined) => void;
withCheckIcon?: boolean; withCheckIcon?: boolean;
@ -37,6 +40,7 @@ const IAICustomSelect = (props: IAICustomSelectProps) => {
const { const {
label, label,
items, items,
itemTooltips,
setSelectedItem, setSelectedItem,
selectedItem, selectedItem,
withCheckIcon, withCheckIcon,
@ -118,6 +122,13 @@ const IAICustomSelect = (props: IAICustomSelectProps) => {
> >
<OverlayScrollbarsComponent> <OverlayScrollbarsComponent>
{items.map((item, index) => ( {items.map((item, index) => (
<Tooltip
isDisabled={!itemTooltips}
key={`${item}${index}`}
label={itemTooltips?.[item]}
hasArrow
placement="right"
>
<ListItem <ListItem
sx={{ sx={{
bg: highlightedIndex === index ? 'base.700' : undefined, bg: highlightedIndex === index ? 'base.700' : undefined,
@ -160,6 +171,7 @@ const IAICustomSelect = (props: IAICustomSelectProps) => {
</Text> </Text>
)} )}
</ListItem> </ListItem>
</Tooltip>
))} ))}
</OverlayScrollbarsComponent> </OverlayScrollbarsComponent>
</List> </List>

View File

@ -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<typeof zMetadataStringField>;
// const zMetadataIntegerField = z.number().int();
// export type MetadataIntegerField = z.infer<typeof zMetadataIntegerField>;
// const zMetadataFloatField = z.number();
// export type MetadataFloatField = z.infer<typeof zMetadataFloatField>;
// const zMetadataBooleanField = z.boolean();
// export type MetadataBooleanField = z.infer<typeof zMetadataBooleanField>;
// 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<typeof zMetadataImageField>;
// const zMetadataLatentsField = z.object({
// latents_name: z.string().min(1),
// });
// export type MetadataLatentsField = z.infer<typeof zMetadataLatentsField>;
// /**
// * 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<typeof zNodeMetadata>;
// const zMetadata = z.object({
// invokeai: zNodeMetadata.optional(),
// 'sd-metadata': z.record(z.string().min(1), z.any()).optional(),
// });
// export type Metadata = z.infer<typeof zMetadata>;
// export const parseMetadata = (
// metadata: Record<string, any>
// ): Metadata | undefined => {
// const result = zMetadata.safeParse(metadata);
// if (!result.success) {
// console.log(result.error.issues);
// return;
// }
// return result.data;
// };
export default {};

View File

@ -29,6 +29,7 @@ import {
isCanvasMaskLine, isCanvasMaskLine,
} from './canvasTypes'; } from './canvasTypes';
import { ImageDTO } from 'services/api'; import { ImageDTO } from 'services/api';
import { sessionCanceled } from 'services/thunks/session';
export const initialLayerState: CanvasLayerState = { export const initialLayerState: CanvasLayerState = {
objects: [], objects: [],
@ -844,6 +845,13 @@ export const canvasSlice = createSlice({
state.isTransformingBoundingBox = false; state.isTransformingBoundingBox = false;
}, },
}, },
extraReducers: (builder) => {
builder.addCase(sessionCanceled.pending, (state) => {
if (!state.layerState.stagingArea.images.length) {
state.layerState.stagingArea = initialLayerState.stagingArea;
}
});
},
}); });
export const { export const {

View File

@ -9,7 +9,8 @@ import { IRect } from 'konva/lib/types';
*/ */
const createMaskStage = async ( const createMaskStage = async (
lines: CanvasMaskLine[], lines: CanvasMaskLine[],
boundingBox: IRect boundingBox: IRect,
shouldInvertMask: boolean
): Promise<Konva.Stage> => { ): Promise<Konva.Stage> => {
// create an offscreen canvas and add the mask to it // create an offscreen canvas and add the mask to it
const { width, height } = boundingBox; const { width, height } = boundingBox;
@ -29,7 +30,7 @@ const createMaskStage = async (
baseLayer.add( baseLayer.add(
new Konva.Rect({ new Konva.Rect({
...boundingBox, ...boundingBox,
fill: 'white', fill: shouldInvertMask ? 'black' : 'white',
}) })
); );
@ -37,7 +38,7 @@ const createMaskStage = async (
maskLayer.add( maskLayer.add(
new Konva.Line({ new Konva.Line({
points: line.points, points: line.points,
stroke: 'black', stroke: shouldInvertMask ? 'white' : 'black',
strokeWidth: line.strokeWidth * 2, strokeWidth: line.strokeWidth * 2,
tension: 0, tension: 0,
lineCap: 'round', lineCap: 'round',

View File

@ -25,6 +25,7 @@ export const getCanvasData = async (state: RootState) => {
boundingBoxCoordinates, boundingBoxCoordinates,
boundingBoxDimensions, boundingBoxDimensions,
isMaskEnabled, isMaskEnabled,
shouldPreserveMaskedArea,
} = state.canvas; } = state.canvas;
const boundingBox = { const boundingBox = {
@ -58,7 +59,8 @@ export const getCanvasData = async (state: RootState) => {
// For the mask layer, use the normal boundingBox // For the mask layer, use the normal boundingBox
const maskStage = await createMaskStage( const maskStage = await createMaskStage(
isMaskEnabled ? objects.filter(isCanvasMaskLine) : [], // only include mask lines, and only if mask is enabled isMaskEnabled ? objects.filter(isCanvasMaskLine) : [], // only include mask lines, and only if mask is enabled
boundingBox boundingBox,
shouldPreserveMaskedArea
); );
const maskBlob = await konvaNodeToBlob(maskStage, boundingBox); const maskBlob = await konvaNodeToBlob(maskStage, boundingBox);
const maskImageData = await konvaNodeToImageData(maskStage, boundingBox); const maskImageData = await konvaNodeToImageData(maskStage, boundingBox);

View File

@ -49,7 +49,7 @@ import { useCallback } from 'react';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import { useGetUrl } from 'common/util/getUrl'; import { useGetUrl } from 'common/util/getUrl';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; 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 { initialImageSelected } from 'features/parameters/store/actions';
import { import {
requestedImageDeletion, requestedImageDeletion,
@ -58,7 +58,6 @@ import {
} from '../store/actions'; } from '../store/actions';
import FaceRestoreSettings from 'features/parameters/components/Parameters/FaceRestore/FaceRestoreSettings'; import FaceRestoreSettings from 'features/parameters/components/Parameters/FaceRestore/FaceRestoreSettings';
import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/UpscaleSettings'; import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/UpscaleSettings';
import { allParametersSet } from 'features/parameters/store/generationSlice';
import DeleteImageButton from './ImageActionButtons/DeleteImageButton'; import DeleteImageButton from './ImageActionButtons/DeleteImageButton';
import { useAppToaster } from 'app/components/Toaster'; import { useAppToaster } from 'app/components/Toaster';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
@ -165,7 +164,8 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
const toaster = useAppToaster(); const toaster = useAppToaster();
const { t } = useTranslation(); const { t } = useTranslation();
const { recallPrompt, recallSeed, recallAllParameters } = useParameters(); const { recallBothPrompts, recallSeed, recallAllParameters } =
useRecallParameters();
// const handleCopyImage = useCallback(async () => { // const handleCopyImage = useCallback(async () => {
// if (!image?.url) { // if (!image?.url) {
@ -250,11 +250,11 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
useHotkeys('s', handleUseSeed, [image]); useHotkeys('s', handleUseSeed, [image]);
const handleUsePrompt = useCallback(() => { const handleUsePrompt = useCallback(() => {
recallPrompt( recallBothPrompts(
image?.metadata?.positive_conditioning, image?.metadata?.positive_conditioning,
image?.metadata?.negative_conditioning image?.metadata?.negative_conditioning
); );
}, [image, recallPrompt]); }, [image, recallBothPrompts]);
useHotkeys('p', handleUsePrompt, [image]); useHotkeys('p', handleUsePrompt, [image]);
@ -461,7 +461,11 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
{t('parameters.copyImageToLink')} {t('parameters.copyImageToLink')}
</IAIButton> </IAIButton>
<Link download={true} href={getUrl(image?.image_url ?? '')}> <Link
download={true}
href={getUrl(image?.image_url ?? '')}
target="_blank"
>
<IAIButton leftIcon={<FaDownload />} size="sm" w="100%"> <IAIButton leftIcon={<FaDownload />} size="sm" w="100%">
{t('parameters.downloadImage')} {t('parameters.downloadImage')}
</IAIButton> </IAIButton>

View File

@ -30,7 +30,7 @@ import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; 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 { initialImageSelected } from 'features/parameters/store/actions';
import { import {
requestedImageDeletion, requestedImageDeletion,
@ -114,8 +114,8 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled; const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled; const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
const { recallSeed, recallPrompt, recallInitialImage, recallAllParameters } = const { recallBothPrompts, recallSeed, recallAllParameters } =
useParameters(); useRecallParameters();
const handleMouseOver = () => setIsHovered(true); const handleMouseOver = () => setIsHovered(true);
const handleMouseOut = () => setIsHovered(false); const handleMouseOut = () => setIsHovered(false);
@ -154,11 +154,15 @@ const HoverableImage = memo((props: HoverableImageProps) => {
// Recall parameters handlers // Recall parameters handlers
const handleRecallPrompt = useCallback(() => { const handleRecallPrompt = useCallback(() => {
recallPrompt( recallBothPrompts(
image.metadata?.positive_conditioning, image.metadata?.positive_conditioning,
image.metadata?.negative_conditioning image.metadata?.negative_conditioning
); );
}, [image, recallPrompt]); }, [
image.metadata?.negative_conditioning,
image.metadata?.positive_conditioning,
recallBothPrompts,
]);
const handleRecallSeed = useCallback(() => { const handleRecallSeed = useCallback(() => {
recallSeed(image.metadata?.seed); recallSeed(image.metadata?.seed);

View File

@ -1,8 +1,6 @@
import { import {
Box, Box,
ButtonGroup, ButtonGroup,
Checkbox,
CheckboxGroup,
Flex, Flex,
FlexProps, FlexProps,
Grid, Grid,
@ -32,18 +30,13 @@ import {
memo, memo,
useCallback, useCallback,
useEffect, useEffect,
useMemo,
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs'; import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
import { import { FaImage, FaServer, FaWrench } from 'react-icons/fa';
FaFilter,
FaImage,
FaImages,
FaServer,
FaWrench,
} from 'react-icons/fa';
import { MdPhotoLibrary } from 'react-icons/md'; import { MdPhotoLibrary } from 'react-icons/md';
import HoverableImage from './HoverableImage'; import HoverableImage from './HoverableImage';
@ -53,7 +46,6 @@ import { RootState } from 'app/store/store';
import { Virtuoso, VirtuosoGrid } from 'react-virtuoso'; import { Virtuoso, VirtuosoGrid } from 'react-virtuoso';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { uiSelector } from 'features/ui/store/uiSelectors'; import { uiSelector } from 'features/ui/store/uiSelectors';
import { ImageCategory } from 'services/api';
import { import {
ASSETS_CATEGORIES, ASSETS_CATEGORIES,
IMAGE_CATEGORIES, IMAGE_CATEGORIES,
@ -61,7 +53,6 @@ import {
selectImagesAll, selectImagesAll,
} from '../store/imagesSlice'; } from '../store/imagesSlice';
import { receivedPageOfImages } from 'services/thunks/image'; import { receivedPageOfImages } from 'services/thunks/image';
import { capitalize } from 'lodash-es';
const categorySelector = createSelector( const categorySelector = createSelector(
[(state: RootState) => state], [(state: RootState) => state],
@ -144,6 +135,13 @@ const ImageGalleryContent = () => {
dispatch(receivedPageOfImages()); dispatch(receivedPageOfImages());
}, [dispatch]); }, [dispatch]);
const handleEndReached = useMemo(() => {
if (areMoreImagesAvailable && !isLoading) {
return handleLoadMoreImages;
}
return undefined;
}, [areMoreImagesAvailable, handleLoadMoreImages, isLoading]);
const handleChangeGalleryImageMinimumWidth = (v: number) => { const handleChangeGalleryImageMinimumWidth = (v: number) => {
dispatch(setGalleryImageMinimumWidth(v)); 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(() => { const handleClickImagesCategory = useCallback(() => {
dispatch(imageCategoriesChanged(IMAGE_CATEGORIES)); dispatch(imageCategoriesChanged(IMAGE_CATEGORIES));
}, [dispatch]); }, [dispatch]);

View File

@ -31,6 +31,7 @@ import { IoArrowUndoCircleOutline } from 'react-icons/io5';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { ImageDTO } from 'services/api'; import { ImageDTO } from 'services/api';
import { Scheduler } from 'app/constants'; import { Scheduler } from 'app/constants';
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
type MetadataItemProps = { type MetadataItemProps = {
isLink?: boolean; isLink?: boolean;
@ -120,6 +121,21 @@ const memoEqualityCheck = (
*/ */
const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const {
recallBothPrompts,
recallPositivePrompt,
recallNegativePrompt,
recallSeed,
recallInitialImage,
recallCfgScale,
recallModel,
recallScheduler,
recallSteps,
recallWidth,
recallHeight,
recallStrength,
recallAllParameters,
} = useRecallParameters();
useHotkeys('esc', () => { useHotkeys('esc', () => {
dispatch(setShouldShowImageDetails(false)); dispatch(setShouldShowImageDetails(false));
@ -166,52 +182,53 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
{metadata.type && ( {metadata.type && (
<MetadataItem label="Invocation type" value={metadata.type} /> <MetadataItem label="Invocation type" value={metadata.type} />
)} )}
{metadata.width && ( {sessionId && <MetadataItem label="Session ID" value={sessionId} />}
<MetadataItem
label="Width"
value={metadata.width}
onClick={() => dispatch(setWidth(Number(metadata.width)))}
/>
)}
{metadata.height && (
<MetadataItem
label="Height"
value={metadata.height}
onClick={() => dispatch(setHeight(Number(metadata.height)))}
/>
)}
{metadata.model && (
<MetadataItem label="Model" value={metadata.model} />
)}
{metadata.positive_conditioning && ( {metadata.positive_conditioning && (
<MetadataItem <MetadataItem
label="Prompt" label="Positive Prompt"
labelPosition="top" labelPosition="top"
value={ value={metadata.positive_conditioning}
typeof metadata.positive_conditioning === 'string' onClick={() =>
? metadata.positive_conditioning recallPositivePrompt(metadata.positive_conditioning)
: promptToString(metadata.positive_conditioning)
} }
onClick={() => setPositivePrompt(metadata.positive_conditioning!)}
/> />
)} )}
{metadata.negative_conditioning && ( {metadata.negative_conditioning && (
<MetadataItem <MetadataItem
label="Prompt" label="Negative Prompt"
labelPosition="top" labelPosition="top"
value={ value={metadata.negative_conditioning}
typeof metadata.negative_conditioning === 'string' onClick={() =>
? metadata.negative_conditioning recallNegativePrompt(metadata.negative_conditioning)
: promptToString(metadata.negative_conditioning)
} }
onClick={() => setNegativePrompt(metadata.negative_conditioning!)}
/> />
)} )}
{metadata.seed !== undefined && ( {metadata.seed !== undefined && (
<MetadataItem <MetadataItem
label="Seed" label="Seed"
value={metadata.seed} value={metadata.seed}
onClick={() => dispatch(setSeed(Number(metadata.seed)))} onClick={() => recallSeed(metadata.seed)}
/>
)}
{metadata.model !== undefined && (
<MetadataItem
label="Model"
value={metadata.model}
onClick={() => recallModel(metadata.model)}
/>
)}
{metadata.width && (
<MetadataItem
label="Width"
value={metadata.width}
onClick={() => recallWidth(metadata.width)}
/>
)}
{metadata.height && (
<MetadataItem
label="Height"
value={metadata.height}
onClick={() => recallHeight(metadata.height)}
/> />
)} )}
{/* {metadata.threshold !== undefined && ( {/* {metadata.threshold !== undefined && (
@ -232,23 +249,21 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
<MetadataItem <MetadataItem
label="Scheduler" label="Scheduler"
value={metadata.scheduler} value={metadata.scheduler}
onClick={() => onClick={() => recallScheduler(metadata.scheduler)}
dispatch(setScheduler(metadata.scheduler as Scheduler))
}
/> />
)} )}
{metadata.steps && ( {metadata.steps && (
<MetadataItem <MetadataItem
label="Steps" label="Steps"
value={metadata.steps} value={metadata.steps}
onClick={() => dispatch(setSteps(Number(metadata.steps)))} onClick={() => recallSteps(metadata.steps)}
/> />
)} )}
{metadata.cfg_scale !== undefined && ( {metadata.cfg_scale !== undefined && (
<MetadataItem <MetadataItem
label="CFG scale" label="CFG scale"
value={metadata.cfg_scale} value={metadata.cfg_scale}
onClick={() => dispatch(setCfgScale(Number(metadata.cfg_scale)))} onClick={() => recallCfgScale(metadata.cfg_scale)}
/> />
)} )}
{/* {metadata.variations && metadata.variations.length > 0 && ( {/* {metadata.variations && metadata.variations.length > 0 && (
@ -289,9 +304,7 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
<MetadataItem <MetadataItem
label="Image to image strength" label="Image to image strength"
value={metadata.strength} value={metadata.strength}
onClick={() => onClick={() => recallStrength(metadata.strength)}
dispatch(setImg2imgStrength(Number(metadata.strength)))
}
/> />
)} )}
{/* {metadata.fit && ( {/* {metadata.fit && (

View File

@ -1,18 +1,14 @@
import { HStack } from '@chakra-ui/react'; import { HStack } from '@chakra-ui/react';
import { userInvoked } from 'app/store/actions';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton'; import IAIButton from 'common/components/IAIButton';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { Panel } from 'reactflow'; import { Panel } from 'reactflow';
import { receivedOpenAPISchema } from 'services/thunks/schema'; import { receivedOpenAPISchema } from 'services/thunks/schema';
import NodeInvokeButton from '../ui/NodeInvokeButton';
const TopCenterPanel = () => { const TopCenterPanel = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const handleInvoke = useCallback(() => {
dispatch(userInvoked('nodes'));
}, [dispatch]);
const handleReloadSchema = useCallback(() => { const handleReloadSchema = useCallback(() => {
dispatch(receivedOpenAPISchema()); dispatch(receivedOpenAPISchema());
}, [dispatch]); }, [dispatch]);
@ -20,9 +16,7 @@ const TopCenterPanel = () => {
return ( return (
<Panel position="top-center"> <Panel position="top-center">
<HStack> <HStack>
<IAIButton colorScheme="accent" onClick={handleInvoke}> <NodeInvokeButton />
Will it blend?
</IAIButton>
<IAIButton onClick={handleReloadSchema}>Reload Schema</IAIButton> <IAIButton onClick={handleReloadSchema}>Reload Schema</IAIButton>
</HStack> </HStack>
</Panel> </Panel>

View File

@ -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<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
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 (
<Box style={{ flexGrow: 4 }} position="relative">
<Box style={{ position: 'relative' }}>
{!isReady && (
<Box
style={{
position: 'absolute',
bottom: '0',
left: '0',
right: '0',
height: '100%',
overflow: 'clip',
borderRadius: 4,
}}
>
<ProgressBar />
</Box>
)}
{iconButton ? (
<IAIIconButton
aria-label={t('parameters.invoke')}
type="submit"
icon={<FaPlay />}
isDisabled={!isReady}
onClick={handleInvoke}
flexGrow={1}
w="100%"
tooltip={t('parameters.invoke')}
tooltipProps={{ placement: 'bottom' }}
colorScheme="accent"
id="invoke-button"
{...rest}
/>
) : (
<IAIButton
aria-label={t('parameters.invoke')}
type="submit"
isDisabled={!isReady}
onClick={handleInvoke}
flexGrow={1}
w="100%"
colorScheme="accent"
id="invoke-button"
fontWeight={700}
{...rest}
>
Invoke
</IAIButton>
)}
</Box>
</Box>
);
}

View File

@ -16,7 +16,7 @@ import { buildEdges } from '../edgeBuilders/buildEdges';
import { log } from 'app/logging/useLogger'; import { log } from 'app/logging/useLogger';
import { buildInpaintNode } from '../nodeBuilders/buildInpaintNode'; import { buildInpaintNode } from '../nodeBuilders/buildInpaintNode';
const moduleLog = log.child({ namespace: 'buildCanvasGraph' }); const moduleLog = log.child({ namespace: 'nodes' });
const buildBaseNode = ( const buildBaseNode = (
nodeType: 'txt2img' | 'img2img' | 'inpaint' | 'outpaint', nodeType: 'txt2img' | 'img2img' | 'inpaint' | 'outpaint',
@ -80,18 +80,23 @@ export const buildCanvasGraphComponents = async (
infillMethod, infillMethod,
} = state.generation; } = state.generation;
// generationParameters.invert_mask = shouldPreserveMaskedArea; const { scaledBoundingBoxDimensions, boundingBoxScaleMethod } =
// if (boundingBoxScale !== 'none') { state.canvas;
// generationParameters.inpaint_width = scaledBoundingBoxDimensions.width;
// generationParameters.inpaint_height = scaledBoundingBoxDimensions.height; if (boundingBoxScaleMethod !== 'none') {
// } baseNode.inpaint_width = scaledBoundingBoxDimensions.width;
baseNode.inpaint_height = scaledBoundingBoxDimensions.height;
}
baseNode.seam_size = seamSize; baseNode.seam_size = seamSize;
baseNode.seam_blur = seamBlur; baseNode.seam_blur = seamBlur;
baseNode.seam_strength = seamStrength; baseNode.seam_strength = seamStrength;
baseNode.seam_steps = seamSteps; baseNode.seam_steps = seamSteps;
baseNode.tile_size = tileSize;
baseNode.infill_method = infillMethod as InpaintInvocation['infill_method']; 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 // We always range and iterate nodes, no matter the iteration count

View File

@ -2,21 +2,31 @@ import { RootState } from 'app/store/store';
import { import {
CompelInvocation, CompelInvocation,
Graph, Graph,
ImageResizeInvocation,
ImageToLatentsInvocation, ImageToLatentsInvocation,
IterateInvocation,
LatentsToImageInvocation, LatentsToImageInvocation,
LatentsToLatentsInvocation, LatentsToLatentsInvocation,
NoiseInvocation,
RandomIntInvocation,
RangeOfSizeInvocation,
} from 'services/api'; } from 'services/api';
import { NonNullableGraph } from 'features/nodes/types/types'; import { NonNullableGraph } from 'features/nodes/types/types';
import { addNoiseNodes } from '../nodeBuilders/addNoiseNodes';
import { log } from 'app/logging/useLogger'; 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 POSITIVE_CONDITIONING = 'positive_conditioning';
const NEGATIVE_CONDITIONING = 'negative_conditioning'; const NEGATIVE_CONDITIONING = 'negative_conditioning';
const IMAGE_TO_LATENTS = 'image_to_latents'; const IMAGE_TO_LATENTS = 'image_to_latents';
const LATENTS_TO_LATENTS = 'latents_to_latents'; const LATENTS_TO_LATENTS = 'latents_to_latents';
const LATENTS_TO_IMAGE = 'latents_to_image'; 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. * Builds the Image to Image tab graph.
@ -31,6 +41,12 @@ export const buildImageToImageGraph = (state: RootState): Graph => {
steps, steps,
initialImage, initialImage,
img2imgStrength: strength, img2imgStrength: strength,
shouldFitToWidthHeight,
width,
height,
iterations,
seed,
shouldRandomizeSeed,
} = state.generation; } = state.generation;
if (!initialImage) { if (!initialImage) {
@ -38,12 +54,12 @@ export const buildImageToImageGraph = (state: RootState): Graph => {
throw new Error('No initial image found in state'); throw new Error('No initial image found in state');
} }
let graph: NonNullableGraph = { const graph: NonNullableGraph = {
nodes: {}, nodes: {},
edges: [], edges: [],
}; };
// Create the conditioning, t2l and l2i nodes // Create the positive conditioning (prompt) node
const positiveConditioningNode: CompelInvocation = { const positiveConditioningNode: CompelInvocation = {
id: POSITIVE_CONDITIONING, id: POSITIVE_CONDITIONING,
type: 'compel', type: 'compel',
@ -51,6 +67,7 @@ export const buildImageToImageGraph = (state: RootState): Graph => {
model, model,
}; };
// Negative conditioning
const negativeConditioningNode: CompelInvocation = { const negativeConditioningNode: CompelInvocation = {
id: NEGATIVE_CONDITIONING, id: NEGATIVE_CONDITIONING,
type: 'compel', type: 'compel',
@ -58,16 +75,15 @@ export const buildImageToImageGraph = (state: RootState): Graph => {
model, 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 = { const imageToLatentsNode: ImageToLatentsInvocation = {
id: IMAGE_TO_LATENTS, id: IMAGE_TO_LATENTS,
type: 'i2l', type: 'i2l',
model, model,
image: {
image_name: initialImage?.image_name,
image_origin: initialImage?.image_origin,
},
}; };
// This does the actual img2img inference
const latentsToLatentsNode: LatentsToLatentsInvocation = { const latentsToLatentsNode: LatentsToLatentsInvocation = {
id: LATENTS_TO_LATENTS, id: LATENTS_TO_LATENTS,
type: 'l2l', type: 'l2l',
@ -78,20 +94,21 @@ export const buildImageToImageGraph = (state: RootState): Graph => {
strength, strength,
}; };
// Finally we decode the latents back to an image
const latentsToImageNode: LatentsToImageInvocation = { const latentsToImageNode: LatentsToImageInvocation = {
id: LATENTS_TO_IMAGE, id: LATENTS_TO_IMAGE,
type: 'l2i', type: 'l2i',
model, model,
}; };
// Add to the graph // Add all those nodes to the graph
graph.nodes[POSITIVE_CONDITIONING] = positiveConditioningNode; graph.nodes[POSITIVE_CONDITIONING] = positiveConditioningNode;
graph.nodes[NEGATIVE_CONDITIONING] = negativeConditioningNode; graph.nodes[NEGATIVE_CONDITIONING] = negativeConditioningNode;
graph.nodes[IMAGE_TO_LATENTS] = imageToLatentsNode; graph.nodes[IMAGE_TO_LATENTS] = imageToLatentsNode;
graph.nodes[LATENTS_TO_LATENTS] = latentsToLatentsNode; graph.nodes[LATENTS_TO_LATENTS] = latentsToLatentsNode;
graph.nodes[LATENTS_TO_IMAGE] = latentsToImageNode; graph.nodes[LATENTS_TO_IMAGE] = latentsToImageNode;
// Connect them // Connect the prompt nodes to the imageToLatents node
graph.edges.push({ graph.edges.push({
source: { node_id: POSITIVE_CONDITIONING, field: 'conditioning' }, source: { node_id: POSITIVE_CONDITIONING, field: 'conditioning' },
destination: { destination: {
@ -99,7 +116,6 @@ export const buildImageToImageGraph = (state: RootState): Graph => {
field: 'positive_conditioning', field: 'positive_conditioning',
}, },
}); });
graph.edges.push({ graph.edges.push({
source: { node_id: NEGATIVE_CONDITIONING, field: 'conditioning' }, source: { node_id: NEGATIVE_CONDITIONING, field: 'conditioning' },
destination: { destination: {
@ -108,6 +124,7 @@ export const buildImageToImageGraph = (state: RootState): Graph => {
}, },
}); });
// Connect the image-encoding node
graph.edges.push({ graph.edges.push({
source: { node_id: IMAGE_TO_LATENTS, field: 'latents' }, source: { node_id: IMAGE_TO_LATENTS, field: 'latents' },
destination: { destination: {
@ -116,6 +133,7 @@ export const buildImageToImageGraph = (state: RootState): Graph => {
}, },
}); });
// Connect the image-decoding node
graph.edges.push({ graph.edges.push({
source: { node_id: LATENTS_TO_LATENTS, field: 'latents' }, source: { node_id: LATENTS_TO_LATENTS, field: 'latents' },
destination: { destination: {
@ -124,8 +142,271 @@ 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,
},
is_intermediate: true,
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 `IMAGE_TO_LATENTS` node explicitly
set(graph.nodes[IMAGE_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; return graph;
}; };

View File

@ -1,8 +1,9 @@
import { Graph } from 'services/api'; import { Graph } from 'services/api';
import { v4 as uuidv4 } from 'uuid'; 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 { RootState } from 'app/store/store';
import { InputFieldValue } from 'features/nodes/types/types'; import { InputFieldValue } from 'features/nodes/types/types';
import { AnyInvocation } from 'services/events/types';
/** /**
* We need to do special handling for some fields * 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! // Assemble!
const graph = { const graph = {
id: uuidv4(), id: uuidv4(),

View File

@ -2,16 +2,23 @@ import { RootState } from 'app/store/store';
import { import {
CompelInvocation, CompelInvocation,
Graph, Graph,
IterateInvocation,
LatentsToImageInvocation, LatentsToImageInvocation,
NoiseInvocation,
RandomIntInvocation,
RangeOfSizeInvocation,
TextToLatentsInvocation, TextToLatentsInvocation,
} from 'services/api'; } from 'services/api';
import { NonNullableGraph } from 'features/nodes/types/types'; import { NonNullableGraph } from 'features/nodes/types/types';
import { addNoiseNodes } from '../nodeBuilders/addNoiseNodes';
const POSITIVE_CONDITIONING = 'positive_conditioning'; const POSITIVE_CONDITIONING = 'positive_conditioning';
const NEGATIVE_CONDITIONING = 'negative_conditioning'; const NEGATIVE_CONDITIONING = 'negative_conditioning';
const TEXT_TO_LATENTS = 'text_to_latents'; const TEXT_TO_LATENTS = 'text_to_latents';
const LATENTS_TO_IMAGE = 'latents_to_image'; 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. * Builds the Text to Image tab graph.
@ -24,9 +31,14 @@ export const buildTextToImageGraph = (state: RootState): Graph => {
cfgScale: cfg_scale, cfgScale: cfg_scale,
scheduler, scheduler,
steps, steps,
width,
height,
iterations,
seed,
shouldRandomizeSeed,
} = state.generation; } = state.generation;
let graph: NonNullableGraph = { const graph: NonNullableGraph = {
nodes: {}, nodes: {},
edges: [], edges: [],
}; };
@ -92,8 +104,209 @@ 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,
width,
height,
};
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',
width,
height,
};
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',
width,
height,
};
// 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; return graph;
}; };

View File

@ -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;
};

View File

@ -2,15 +2,12 @@ import { v4 as uuidv4 } from 'uuid';
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { InpaintInvocation } from 'services/api'; import { InpaintInvocation } from 'services/api';
import { O } from 'ts-toolbelt'; import { O } from 'ts-toolbelt';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
export const buildInpaintNode = ( export const buildInpaintNode = (
state: RootState, state: RootState,
overrides: O.Partial<InpaintInvocation, 'deep'> = {} overrides: O.Partial<InpaintInvocation, 'deep'> = {}
): InpaintInvocation => { ): InpaintInvocation => {
const nodeId = uuidv4(); const nodeId = uuidv4();
const { generation } = state;
const activeTabName = activeTabNameSelector(state);
const { const {
positivePrompt: prompt, positivePrompt: prompt,
@ -25,8 +22,7 @@ export const buildInpaintNode = (
img2imgStrength: strength, img2imgStrength: strength,
shouldFitToWidthHeight: fit, shouldFitToWidthHeight: fit,
shouldRandomizeSeed, shouldRandomizeSeed,
initialImage, } = state.generation;
} = generation;
const inpaintNode: InpaintInvocation = { const inpaintNode: InpaintInvocation = {
id: nodeId, id: nodeId,
@ -42,19 +38,6 @@ export const buildInpaintNode = (
fit, 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) { if (!shouldRandomizeSeed) {
inpaintNode.seed = seed; inpaintNode.seed = seed;
} }

View File

@ -2,18 +2,22 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAISlider from 'common/components/IAISlider'; 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 { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
canvasSelector, [canvasSelector, isStagingSelector],
(canvas) => { (canvas, isStaging) => {
const { boundingBoxDimensions } = canvas; const { boundingBoxDimensions } = canvas;
return { return {
boundingBoxDimensions, boundingBoxDimensions,
isStaging,
}; };
}, },
defaultSelectorOptions defaultSelectorOptions
@ -21,7 +25,7 @@ const selector = createSelector(
const ParamBoundingBoxWidth = () => { const ParamBoundingBoxWidth = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { boundingBoxDimensions } = useAppSelector(selector); const { boundingBoxDimensions, isStaging } = useAppSelector(selector);
const { t } = useTranslation(); const { t } = useTranslation();
@ -45,12 +49,13 @@ const ParamBoundingBoxWidth = () => {
return ( return (
<IAISlider <IAISlider
label={t('parameters.height')} label={t('parameters.boundingBoxHeight')}
min={64} min={64}
max={1024} max={1024}
step={64} step={64}
value={boundingBoxDimensions.height} value={boundingBoxDimensions.height}
onChange={handleChangeHeight} onChange={handleChangeHeight}
isDisabled={isStaging}
sliderNumberInputProps={{ max: 4096 }} sliderNumberInputProps={{ max: 4096 }}
withSliderMarks withSliderMarks
withInput withInput

View File

@ -2,18 +2,22 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAISlider from 'common/components/IAISlider'; 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 { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
canvasSelector, [canvasSelector, isStagingSelector],
(canvas) => { (canvas, isStaging) => {
const { boundingBoxDimensions } = canvas; const { boundingBoxDimensions } = canvas;
return { return {
boundingBoxDimensions, boundingBoxDimensions,
isStaging,
}; };
}, },
defaultSelectorOptions defaultSelectorOptions
@ -21,7 +25,7 @@ const selector = createSelector(
const ParamBoundingBoxWidth = () => { const ParamBoundingBoxWidth = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { boundingBoxDimensions } = useAppSelector(selector); const { boundingBoxDimensions, isStaging } = useAppSelector(selector);
const { t } = useTranslation(); const { t } = useTranslation();
@ -45,12 +49,13 @@ const ParamBoundingBoxWidth = () => {
return ( return (
<IAISlider <IAISlider
label={t('parameters.width')} label={t('parameters.boundingBoxWidth')}
min={64} min={64}
max={1024} max={1024}
step={64} step={64}
value={boundingBoxDimensions.width} value={boundingBoxDimensions.width}
onChange={handleChangeWidth} onChange={handleChangeWidth}
isDisabled={isStaging}
sliderNumberInputProps={{ max: 4096 }} sliderNumberInputProps={{ max: 4096 }}
withSliderMarks withSliderMarks
withInput withInput

View File

@ -1,25 +1,34 @@
import { createSelector } from '@reduxjs/toolkit';
import { Scheduler } from 'app/constants'; import { Scheduler } from 'app/constants';
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAICustomSelect from 'common/components/IAICustomSelect'; import IAICustomSelect from 'common/components/IAICustomSelect';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import { setScheduler } from 'features/parameters/store/generationSlice'; import { setScheduler } from 'features/parameters/store/generationSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { uiSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const ParamScheduler = () => { const selector = createSelector(
const scheduler = useAppSelector( [uiSelector, generationSelector],
(state: RootState) => state.generation.scheduler (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 activeTabName = useAppSelector(activeTabNameSelector); const allSchedulers = ui.schedulers.filter((scheduler) => {
const schedulers = useAppSelector((state: RootState) => state.ui.schedulers);
const img2imgSchedulers = schedulers.filter((scheduler) => {
return !['dpmpp_2s'].includes(scheduler); return !['dpmpp_2s'].includes(scheduler);
}); });
return {
scheduler: generation.scheduler,
allSchedulers,
};
},
defaultSelectorOptions
);
const ParamScheduler = () => {
const { allSchedulers, scheduler } = useAppSelector(selector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
@ -38,11 +47,7 @@ const ParamScheduler = () => {
label={t('parameters.scheduler')} label={t('parameters.scheduler')}
selectedItem={scheduler} selectedItem={scheduler}
setSelectedItem={handleChange} setSelectedItem={handleChange}
items={ items={allSchedulers}
['img2img', 'unifiedCanvas'].includes(activeTabName)
? img2imgSchedulers
: schedulers
}
withCheckIcon withCheckIcon
/> />
); );

View File

@ -7,6 +7,7 @@ import IAIIconButton, {
IAIIconButtonProps, IAIIconButtonProps,
} from 'common/components/IAIIconButton'; } from 'common/components/IAIIconButton';
import { clampSymmetrySteps } from 'features/parameters/store/generationSlice'; import { clampSymmetrySteps } from 'features/parameters/store/generationSlice';
import ProgressBar from 'features/system/components/ProgressBar';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
@ -43,7 +44,23 @@ export default function InvokeButton(props: InvokeButton) {
); );
return ( return (
<Box style={{ flexGrow: 4 }}> <Box style={{ flexGrow: 4 }} position="relative">
<Box style={{ position: 'relative' }}>
{!isReady && (
<Box
style={{
position: 'absolute',
bottom: '0',
left: '0',
right: '0',
height: '100%',
overflow: 'clip',
borderRadius: 4,
}}
>
<ProgressBar />
</Box>
)}
{iconButton ? ( {iconButton ? (
<IAIIconButton <IAIIconButton
aria-label={t('parameters.invoke')} aria-label={t('parameters.invoke')}
@ -76,5 +93,6 @@ export default function InvokeButton(props: InvokeButton) {
</IAIButton> </IAIButton>
)} )}
</Box> </Box>
</Box>
); );
} }

View File

@ -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,
};
};

View File

@ -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;

View File

@ -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,
};
};

View File

@ -1,43 +1,53 @@
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import * as InvokeAI from 'app/types/invokeai'; import { clamp, sortBy } from 'lodash-es';
import promptToString from 'common/util/promptToString';
import { clamp, sample } from 'lodash-es';
import { setAllParametersReducer } from './setAllParametersReducer';
import { receivedModels } from 'services/thunks/model'; import { receivedModels } from 'services/thunks/model';
import { Scheduler } from 'app/constants'; import { Scheduler } from 'app/constants';
import { ImageDTO } from 'services/api'; 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 { export interface GenerationState {
cfgScale: number; cfgScale: CfgScaleParam;
height: number; height: HeightParam;
img2imgStrength: number; img2imgStrength: StrengthParam;
infillMethod: string; infillMethod: string;
initialImage?: ImageDTO; initialImage?: ImageDTO;
iterations: number; iterations: number;
perlin: number; perlin: number;
positivePrompt: string; positivePrompt: PositivePromptParam;
negativePrompt: string; negativePrompt: NegativePromptParam;
scheduler: Scheduler; scheduler: SchedulerParam;
seamBlur: number; seamBlur: number;
seamSize: number; seamSize: number;
seamSteps: number; seamSteps: number;
seamStrength: number; seamStrength: number;
seed: number; seed: SeedParam;
seedWeights: string; seedWeights: string;
shouldFitToWidthHeight: boolean; shouldFitToWidthHeight: boolean;
shouldGenerateVariations: boolean; shouldGenerateVariations: boolean;
shouldRandomizeSeed: boolean; shouldRandomizeSeed: boolean;
shouldUseNoiseSettings: boolean; shouldUseNoiseSettings: boolean;
steps: number; steps: StepsParam;
threshold: number; threshold: number;
tileSize: number; tileSize: number;
variationAmount: number; variationAmount: number;
width: number; width: WidthParam;
shouldUseSymmetry: boolean; shouldUseSymmetry: boolean;
horizontalSymmetrySteps: number; horizontalSymmetrySteps: number;
verticalSymmetrySteps: number; verticalSymmetrySteps: number;
model: string; model: ModelParam;
shouldUseSeamless: boolean; shouldUseSeamless: boolean;
seamlessXAxis: boolean; seamlessXAxis: boolean;
seamlessYAxis: boolean; seamlessYAxis: boolean;
@ -83,27 +93,11 @@ export const generationSlice = createSlice({
name: 'generation', name: 'generation',
initialState, initialState,
reducers: { reducers: {
setPositivePrompt: ( setPositivePrompt: (state, action: PayloadAction<string>) => {
state, state.positivePrompt = action.payload;
action: PayloadAction<string | InvokeAI.Prompt>
) => {
const newPrompt = action.payload;
if (typeof newPrompt === 'string') {
state.positivePrompt = newPrompt;
} else {
state.positivePrompt = promptToString(newPrompt);
}
}, },
setNegativePrompt: ( setNegativePrompt: (state, action: PayloadAction<string>) => {
state, state.negativePrompt = action.payload;
action: PayloadAction<string | InvokeAI.Prompt>
) => {
const newPrompt = action.payload;
if (typeof newPrompt === 'string') {
state.negativePrompt = newPrompt;
} else {
state.negativePrompt = promptToString(newPrompt);
}
}, },
setIterations: (state, action: PayloadAction<number>) => { setIterations: (state, action: PayloadAction<number>) => {
state.iterations = action.payload; state.iterations = action.payload;
@ -174,7 +168,6 @@ export const generationSlice = createSlice({
state.shouldGenerateVariations = true; state.shouldGenerateVariations = true;
state.variationAmount = 0; state.variationAmount = 0;
}, },
allParametersSet: setAllParametersReducer,
resetParametersState: (state) => { resetParametersState: (state) => {
return { return {
...state, ...state,
@ -227,10 +220,15 @@ export const generationSlice = createSlice({
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(receivedModels.fulfilled, (state, action) => { builder.addCase(receivedModels.fulfilled, (state, action) => {
if (!state.model) { if (!state.model) {
const randomModel = sample(action.payload); const firstModel = sortBy(action.payload, 'name')[0];
if (randomModel) { state.model = firstModel.name;
state.model = randomModel.name;
} }
});
builder.addCase(configChanged, (state, action) => {
const defaultModel = action.payload.sd?.defaultModel;
if (defaultModel && !state.model) {
state.model = defaultModel;
} }
}); });
}, },
@ -273,7 +271,6 @@ export const {
setSeamless, setSeamless,
setSeamlessXAxis, setSeamlessXAxis,
setSeamlessYAxis, setSeamlessYAxis,
allParametersSet,
} = generationSlice.actions; } = generationSlice.actions;
export default generationSlice.reducer; export default generationSlice.reducer;

View File

@ -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<typeof zPositivePrompt>;
/**
* 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<typeof zNegativePrompt>;
/**
* 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<typeof zSteps>;
/**
* 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<typeof zCfgScale>;
/**
* 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<typeof zScheduler>;
/**
* 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<typeof zSeed>;
/**
* 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<typeof zWidth>;
/**
* 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<typeof zHeight>;
/**
* 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<typeof zModel>;
/**
* 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<typeof zStrength>;
/**
* Validates/type-guards a value as a l2l strength parameter
*/
export const isValidStrength = (val: unknown): val is StrengthParam =>
zStrength.safeParse(val).success;

View File

@ -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<GenerationState>,
action: PayloadAction<ImageDTO | undefined>
) => {
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;
// }
}
};

View File

@ -4,19 +4,33 @@ import { isEqual } from 'lodash-es';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; 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 { RootState } from 'app/store/store';
import { modelSelected } from 'features/parameters/store/generationSlice'; import { modelSelected } from 'features/parameters/store/generationSlice';
import { generationSelector } from 'features/parameters/store/generationSelectors'; import { generationSelector } from 'features/parameters/store/generationSelectors';
import IAICustomSelect from 'common/components/IAICustomSelect'; import IAICustomSelect, {
ItemTooltips,
} from 'common/components/IAICustomSelect';
const selector = createSelector( const selector = createSelector(
[(state: RootState) => state, generationSelector], [(state: RootState) => state, generationSelector],
(state, generation) => { (state, generation) => {
const selectedModel = selectModelsById(state, generation.model); const selectedModel = selectModelsById(state, generation.model);
const allModelNames = selectModelsIds(state).map((id) => String(id)); 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 { return {
allModelNames, allModelNames,
allModelTooltips,
selectedModel, selectedModel,
}; };
}, },
@ -30,7 +44,8 @@ const selector = createSelector(
const ModelSelect = () => { const ModelSelect = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const { allModelNames, selectedModel } = useAppSelector(selector); const { allModelNames, allModelTooltips, selectedModel } =
useAppSelector(selector);
const handleChangeModel = useCallback( const handleChangeModel = useCallback(
(v: string | null | undefined) => { (v: string | null | undefined) => {
if (!v) { if (!v) {
@ -46,6 +61,7 @@ const ModelSelect = () => {
label={t('modelManager.model')} label={t('modelManager.model')}
tooltip={selectedModel?.description} tooltip={selectedModel?.description}
items={allModelNames} items={allModelNames}
itemTooltips={allModelTooltips}
selectedItem={selectedModel?.name ?? ''} selectedItem={selectedModel?.name ?? ''}
setSelectedItem={handleChangeModel} setSelectedItem={handleChangeModel}
withCheckIcon={true} withCheckIcon={true}

View File

@ -5,7 +5,6 @@ import { SystemState } from 'features/system/store/systemSlice';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PROGRESS_BAR_THICKNESS } from 'theme/util/constants';
import { systemSelector } from '../store/systemSelectors'; import { systemSelector } from '../store/systemSelectors';
const progressBarSelector = createSelector( const progressBarSelector = createSelector(
@ -35,7 +34,7 @@ const ProgressBar = () => {
value={value} value={value}
aria-label={t('accessibility.invokeProgressBar')} aria-label={t('accessibility.invokeProgressBar')}
isIndeterminate={isProcessing && !currentStatusHasSteps} isIndeterminate={isProcessing && !currentStatusHasSteps}
height={PROGRESS_BAR_THICKNESS} height="full"
/> />
); );
}; };

View File

@ -35,7 +35,13 @@ import {
} from 'features/ui/store/uiSlice'; } from 'features/ui/store/uiSlice';
import { UIState } from 'features/ui/store/uiTypes'; import { UIState } from 'features/ui/store/uiTypes';
import { isEqual } from 'lodash-es'; 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 { useTranslation } from 'react-i18next';
import { VALID_LOG_LEVELS } from 'app/logging/useLogger'; import { VALID_LOG_LEVELS } from 'app/logging/useLogger';
import { LogLevelName } from 'roarr'; import { LogLevelName } from 'roarr';
@ -85,15 +91,33 @@ const modalSectionStyles: ChakraProps['sx'] = {
borderRadius: 'base', borderRadius: 'base',
}; };
type ConfigOptions = {
shouldShowDeveloperSettings: boolean;
shouldShowResetWebUiText: boolean;
shouldShowBetaLayout: boolean;
};
type SettingsModalProps = { type SettingsModalProps = {
/* The button to open the Settings Modal */ /* The button to open the Settings Modal */
children: ReactElement; children: ReactElement;
config?: ConfigOptions;
}; };
const SettingsModal = ({ children }: SettingsModalProps) => { const SettingsModal = ({ children, config }: SettingsModalProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); 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 { const {
isOpen: isSettingsModalOpen, isOpen: isSettingsModalOpen,
onOpen: onSettingsModalOpen, onOpen: onSettingsModalOpen,
@ -189,6 +213,7 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
dispatch(setShouldDisplayGuides(e.target.checked)) dispatch(setShouldDisplayGuides(e.target.checked))
} }
/> />
{shouldShowBetaLayout && (
<IAISwitch <IAISwitch
label={t('settings.useCanvasBeta')} label={t('settings.useCanvasBeta')}
isChecked={shouldUseCanvasBetaLayout} isChecked={shouldUseCanvasBetaLayout}
@ -196,6 +221,7 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
dispatch(setShouldUseCanvasBetaLayout(e.target.checked)) dispatch(setShouldUseCanvasBetaLayout(e.target.checked))
} }
/> />
)}
<IAISwitch <IAISwitch
label={t('settings.useSlidersForAll')} label={t('settings.useSlidersForAll')}
isChecked={shouldUseSliders} isChecked={shouldUseSliders}
@ -221,6 +247,7 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
/> />
</Flex> </Flex>
{shouldShowDeveloperSettings && (
<Flex sx={modalSectionStyles}> <Flex sx={modalSectionStyles}>
<Heading size="sm">{t('settings.developer')}</Heading> <Heading size="sm">{t('settings.developer')}</Heading>
<IAISwitch <IAISwitch
@ -245,14 +272,19 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
} }
/> />
</Flex> </Flex>
)}
<Flex sx={modalSectionStyles}> <Flex sx={modalSectionStyles}>
<Heading size="sm">{t('settings.resetWebUI')}</Heading> <Heading size="sm">{t('settings.resetWebUI')}</Heading>
<IAIButton colorScheme="error" onClick={handleClickResetWebUI}> <IAIButton colorScheme="error" onClick={handleClickResetWebUI}>
{t('settings.resetWebUI')} {t('settings.resetWebUI')}
</IAIButton> </IAIButton>
{shouldShowResetWebUiText && (
<>
<Text>{t('settings.resetWebUIDesc1')}</Text> <Text>{t('settings.resetWebUIDesc1')}</Text>
<Text>{t('settings.resetWebUIDesc2')}</Text> <Text>{t('settings.resetWebUIDesc2')}</Text>
</>
)}
</Flex> </Flex>
</Flex> </Flex>
</ModalBody> </ModalBody>

View File

@ -7,11 +7,11 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations'; import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations';
import ParamSteps from 'features/parameters/components/Parameters/Core/ParamSteps'; import ParamSteps from 'features/parameters/components/Parameters/Core/ParamSteps';
import ParamCFGScale from 'features/parameters/components/Parameters/Core/ParamCFGScale'; import ParamCFGScale from 'features/parameters/components/Parameters/Core/ParamCFGScale';
import ParamWidth from 'features/parameters/components/Parameters/Core/ParamWidth';
import ParamHeight from 'features/parameters/components/Parameters/Core/ParamHeight';
import ImageToImageStrength from 'features/parameters/components/Parameters/ImageToImage/ImageToImageStrength'; import ImageToImageStrength from 'features/parameters/components/Parameters/ImageToImage/ImageToImageStrength';
import ImageToImageFit from 'features/parameters/components/Parameters/ImageToImage/ImageToImageFit'; import ImageToImageFit from 'features/parameters/components/Parameters/ImageToImage/ImageToImageFit';
import ParamSchedulerAndModel from 'features/parameters/components/Parameters/Core/ParamSchedulerAndModel'; import ParamSchedulerAndModel from 'features/parameters/components/Parameters/Core/ParamSchedulerAndModel';
import ParamBoundingBoxWidth from 'features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxWidth';
import ParamBoundingBoxHeight from 'features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxHeight';
const selector = createSelector( const selector = createSelector(
uiSelector, uiSelector,
@ -41,8 +41,8 @@ const UnifiedCanvasCoreParameters = () => {
<ParamIterations /> <ParamIterations />
<ParamSteps /> <ParamSteps />
<ParamCFGScale /> <ParamCFGScale />
<ParamWidth /> <ParamBoundingBoxWidth />
<ParamHeight /> <ParamBoundingBoxHeight />
<ImageToImageStrength /> <ImageToImageStrength />
<ImageToImageFit /> <ImageToImageFit />
<ParamSchedulerAndModel /> <ParamSchedulerAndModel />
@ -55,8 +55,8 @@ const UnifiedCanvasCoreParameters = () => {
<ParamCFGScale /> <ParamCFGScale />
</Flex> </Flex>
<ParamSchedulerAndModel /> <ParamSchedulerAndModel />
<ParamWidth /> <ParamBoundingBoxWidth />
<ParamHeight /> <ParamBoundingBoxHeight />
<ImageToImageStrength /> <ImageToImageStrength />
</Flex> </Flex>
)} )}

View File

@ -2,7 +2,6 @@ import ProcessButtons from 'features/parameters/components/ProcessButtons/Proces
import ParamSeedCollapse from 'features/parameters/components/Parameters/Seed/ParamSeedCollapse'; import ParamSeedCollapse from 'features/parameters/components/Parameters/Seed/ParamSeedCollapse';
import ParamVariationCollapse from 'features/parameters/components/Parameters/Variations/ParamVariationCollapse'; import ParamVariationCollapse from 'features/parameters/components/Parameters/Variations/ParamVariationCollapse';
import ParamSymmetryCollapse from 'features/parameters/components/Parameters/Symmetry/ParamSymmetryCollapse'; 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 ParamInfillAndScalingCollapse from 'features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamInfillAndScalingCollapse';
import ParamSeamCorrectionCollapse from 'features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamCorrectionCollapse'; import ParamSeamCorrectionCollapse from 'features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamCorrectionCollapse';
import UnifiedCanvasCoreParameters from './UnifiedCanvasCoreParameters'; import UnifiedCanvasCoreParameters from './UnifiedCanvasCoreParameters';
@ -20,7 +19,6 @@ const UnifiedCanvasParameters = () => {
<ParamSeedCollapse /> <ParamSeedCollapse />
<ParamVariationCollapse /> <ParamVariationCollapse />
<ParamSymmetryCollapse /> <ParamSymmetryCollapse />
<ParamBoundingBoxCollapse />
<ParamSeamCorrectionCollapse /> <ParamSeamCorrectionCollapse />
<ParamInfillAndScalingCollapse /> <ParamInfillAndScalingCollapse />
</> </>

View File

@ -19,7 +19,7 @@ export const initialUIState: UIState = {
shouldPinGallery: true, shouldPinGallery: true,
shouldShowGallery: true, shouldShowGallery: true,
shouldHidePreview: false, shouldHidePreview: false,
shouldShowProgressInViewer: false, shouldShowProgressInViewer: true,
schedulers: SCHEDULERS, schedulers: SCHEDULERS,
}; };

View File

@ -49,6 +49,8 @@ export type { ImageOutput } from './models/ImageOutput';
export type { ImagePasteInvocation } from './models/ImagePasteInvocation'; export type { ImagePasteInvocation } from './models/ImagePasteInvocation';
export type { ImageProcessorInvocation } from './models/ImageProcessorInvocation'; export type { ImageProcessorInvocation } from './models/ImageProcessorInvocation';
export type { ImageRecordChanges } from './models/ImageRecordChanges'; 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 { ImageToImageInvocation } from './models/ImageToImageInvocation';
export type { ImageToLatentsInvocation } from './models/ImageToLatentsInvocation'; export type { ImageToLatentsInvocation } from './models/ImageToLatentsInvocation';
export type { ImageUrlsDTO } from './models/ImageUrlsDTO'; export type { ImageUrlsDTO } from './models/ImageUrlsDTO';

View File

@ -22,6 +22,8 @@ import type { ImageLerpInvocation } from './ImageLerpInvocation';
import type { ImageMultiplyInvocation } from './ImageMultiplyInvocation'; import type { ImageMultiplyInvocation } from './ImageMultiplyInvocation';
import type { ImagePasteInvocation } from './ImagePasteInvocation'; import type { ImagePasteInvocation } from './ImagePasteInvocation';
import type { ImageProcessorInvocation } from './ImageProcessorInvocation'; import type { ImageProcessorInvocation } from './ImageProcessorInvocation';
import type { ImageResizeInvocation } from './ImageResizeInvocation';
import type { ImageScaleInvocation } from './ImageScaleInvocation';
import type { ImageToImageInvocation } from './ImageToImageInvocation'; import type { ImageToImageInvocation } from './ImageToImageInvocation';
import type { ImageToLatentsInvocation } from './ImageToLatentsInvocation'; import type { ImageToLatentsInvocation } from './ImageToLatentsInvocation';
import type { InfillColorInvocation } from './InfillColorInvocation'; import type { InfillColorInvocation } from './InfillColorInvocation';
@ -67,7 +69,7 @@ export type Graph = {
/** /**
* The nodes in this graph * The nodes in this graph
*/ */
nodes?: Record<string, (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)>; nodes?: Record<string, (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)>;
/** /**
* The connections between nodes and their fields in this graph * The connections between nodes and their fields in this graph
*/ */

View File

@ -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';
};

View File

@ -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';
};

View File

@ -23,6 +23,8 @@ import type { ImageLerpInvocation } from '../models/ImageLerpInvocation';
import type { ImageMultiplyInvocation } from '../models/ImageMultiplyInvocation'; import type { ImageMultiplyInvocation } from '../models/ImageMultiplyInvocation';
import type { ImagePasteInvocation } from '../models/ImagePasteInvocation'; import type { ImagePasteInvocation } from '../models/ImagePasteInvocation';
import type { ImageProcessorInvocation } from '../models/ImageProcessorInvocation'; 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 { ImageToImageInvocation } from '../models/ImageToImageInvocation';
import type { ImageToLatentsInvocation } from '../models/ImageToLatentsInvocation'; import type { ImageToLatentsInvocation } from '../models/ImageToLatentsInvocation';
import type { InfillColorInvocation } from '../models/InfillColorInvocation'; import type { InfillColorInvocation } from '../models/InfillColorInvocation';
@ -169,7 +171,7 @@ export class SessionsService {
* The id of the session * The id of the session
*/ */
sessionId: string, 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<string> { }): CancelablePromise<string> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'POST', method: 'POST',
@ -206,7 +208,7 @@ export class SessionsService {
* The path to the node in the graph * The path to the node in the graph
*/ */
nodePath: string, 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<GraphExecutionState> { }): CancelablePromise<GraphExecutionState> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'PUT', method: 'PUT',

View File

@ -20,7 +20,7 @@ const invokeAIFilledTrack = defineStyle((_props) => ({
const invokeAITrack = defineStyle((_props) => { const invokeAITrack = defineStyle((_props) => {
return { return {
bg: 'base.800', bg: 'none',
}; };
}); });

View File

@ -6877,6 +6877,11 @@ z-schema@~5.0.2:
optionalDependencies: optionalDependencies:
commander "^10.0.0" 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: zustand@^4.3.1:
version "4.3.7" version "4.3.7"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.7.tgz#501b1f0393a7f1d103332e45ab574be5747fedce" resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.7.tgz#501b1f0393a7f1d103332e45ab574be5747fedce"