mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'main' into release/make-web-dist-startable
This commit is contained in:
commit
4bbe3b0d00
@ -19,31 +19,56 @@ An invocation looks like this:
|
||||
```py
|
||||
class UpscaleInvocation(BaseInvocation):
|
||||
"""Upscales an image."""
|
||||
type: Literal['upscale'] = 'upscale'
|
||||
|
||||
# fmt: off
|
||||
type: Literal["upscale"] = "upscale"
|
||||
|
||||
# Inputs
|
||||
image: Union[ImageField,None] = Field(description="The input image")
|
||||
strength: float = Field(default=0.75, gt=0, le=1, description="The strength")
|
||||
level: Literal[2,4] = Field(default=2, description = "The upscale level")
|
||||
image: Union[ImageField, None] = Field(description="The input image", default=None)
|
||||
strength: float = Field(default=0.75, gt=0, le=1, description="The strength")
|
||||
level: Literal[2, 4] = Field(default=2, description="The upscale level")
|
||||
# fmt: on
|
||||
|
||||
# Schema customisation
|
||||
class Config(InvocationConfig):
|
||||
schema_extra = {
|
||||
"ui": {
|
||||
"tags": ["upscaling", "image"],
|
||||
},
|
||||
}
|
||||
|
||||
def invoke(self, context: InvocationContext) -> ImageOutput:
|
||||
image = context.services.images.get(self.image.image_type, self.image.image_name)
|
||||
results = context.services.generate.upscale_and_reconstruct(
|
||||
image_list = [[image, 0]],
|
||||
upscale = (self.level, self.strength),
|
||||
strength = 0.0, # GFPGAN strength
|
||||
save_original = False,
|
||||
image_callback = None,
|
||||
image = context.services.images.get_pil_image(
|
||||
self.image.image_origin, self.image.image_name
|
||||
)
|
||||
results = context.services.restoration.upscale_and_reconstruct(
|
||||
image_list=[[image, 0]],
|
||||
upscale=(self.level, self.strength),
|
||||
strength=0.0, # GFPGAN strength
|
||||
save_original=False,
|
||||
image_callback=None,
|
||||
)
|
||||
|
||||
# Results are image and seed, unwrap for now
|
||||
# TODO: can this return multiple results?
|
||||
image_type = ImageType.RESULT
|
||||
image_name = context.services.images.create_name(context.graph_execution_state_id, self.id)
|
||||
context.services.images.save(image_type, image_name, results[0][0])
|
||||
return ImageOutput(
|
||||
image = ImageField(image_type = image_type, image_name = image_name)
|
||||
image_dto = context.services.images.create(
|
||||
image=results[0][0],
|
||||
image_origin=ResourceOrigin.INTERNAL,
|
||||
image_category=ImageCategory.GENERAL,
|
||||
node_id=self.id,
|
||||
session_id=context.graph_execution_state_id,
|
||||
is_intermediate=self.is_intermediate,
|
||||
)
|
||||
|
||||
return ImageOutput(
|
||||
image=ImageField(
|
||||
image_name=image_dto.image_name,
|
||||
image_origin=image_dto.image_origin,
|
||||
),
|
||||
width=image_dto.width,
|
||||
height=image_dto.height,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
Each portion is important to implement correctly.
|
||||
@ -95,25 +120,67 @@ Finally, note that for all linking, the `type` of the linked fields must match.
|
||||
If the `name` also matches, then the field can be **automatically linked** to a
|
||||
previous invocation by name and matching.
|
||||
|
||||
### Config
|
||||
|
||||
```py
|
||||
# Schema customisation
|
||||
class Config(InvocationConfig):
|
||||
schema_extra = {
|
||||
"ui": {
|
||||
"tags": ["upscaling", "image"],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
This is an optional configuration for the invocation. It inherits from
|
||||
pydantic's model `Config` class, and it used primarily to customize the
|
||||
autogenerated OpenAPI schema.
|
||||
|
||||
The UI relies on the OpenAPI schema in two ways:
|
||||
|
||||
- An API client & Typescript types are generated from it. This happens at build
|
||||
time.
|
||||
- The node editor parses the schema into a template used by the UI to create the
|
||||
node editor UI. This parsing happens at runtime.
|
||||
|
||||
In this example, a `ui` key has been added to the `schema_extra` dict to provide
|
||||
some tags for the UI, to facilitate filtering nodes.
|
||||
|
||||
See the Schema Generation section below for more information.
|
||||
|
||||
### Invoke Function
|
||||
|
||||
```py
|
||||
def invoke(self, context: InvocationContext) -> ImageOutput:
|
||||
image = context.services.images.get(self.image.image_type, self.image.image_name)
|
||||
results = context.services.generate.upscale_and_reconstruct(
|
||||
image_list = [[image, 0]],
|
||||
upscale = (self.level, self.strength),
|
||||
strength = 0.0, # GFPGAN strength
|
||||
save_original = False,
|
||||
image_callback = None,
|
||||
image = context.services.images.get_pil_image(
|
||||
self.image.image_origin, self.image.image_name
|
||||
)
|
||||
results = context.services.restoration.upscale_and_reconstruct(
|
||||
image_list=[[image, 0]],
|
||||
upscale=(self.level, self.strength),
|
||||
strength=0.0, # GFPGAN strength
|
||||
save_original=False,
|
||||
image_callback=None,
|
||||
)
|
||||
|
||||
# Results are image and seed, unwrap for now
|
||||
image_type = ImageType.RESULT
|
||||
image_name = context.services.images.create_name(context.graph_execution_state_id, self.id)
|
||||
context.services.images.save(image_type, image_name, results[0][0])
|
||||
# TODO: can this return multiple results?
|
||||
image_dto = context.services.images.create(
|
||||
image=results[0][0],
|
||||
image_origin=ResourceOrigin.INTERNAL,
|
||||
image_category=ImageCategory.GENERAL,
|
||||
node_id=self.id,
|
||||
session_id=context.graph_execution_state_id,
|
||||
is_intermediate=self.is_intermediate,
|
||||
)
|
||||
|
||||
return ImageOutput(
|
||||
image = ImageField(image_type = image_type, image_name = image_name)
|
||||
image=ImageField(
|
||||
image_name=image_dto.image_name,
|
||||
image_origin=image_dto.image_origin,
|
||||
),
|
||||
width=image_dto.width,
|
||||
height=image_dto.height,
|
||||
)
|
||||
```
|
||||
|
||||
@ -135,9 +202,16 @@ scenarios. If you need functionality, please provide it as a service in the
|
||||
```py
|
||||
class ImageOutput(BaseInvocationOutput):
|
||||
"""Base class for invocations that output an image"""
|
||||
type: Literal['image'] = 'image'
|
||||
|
||||
image: ImageField = Field(default=None, description="The output image")
|
||||
# fmt: off
|
||||
type: Literal["image_output"] = "image_output"
|
||||
image: ImageField = Field(default=None, description="The output image")
|
||||
width: int = Field(description="The width of the image in pixels")
|
||||
height: int = Field(description="The height of the image in pixels")
|
||||
# fmt: on
|
||||
|
||||
class Config:
|
||||
schema_extra = {"required": ["type", "image", "width", "height"]}
|
||||
```
|
||||
|
||||
Output classes look like an invocation class without the invoke method. Prefer
|
||||
@ -168,35 +242,36 @@ Here's that `ImageOutput` class, without the needed schema customisation:
|
||||
class ImageOutput(BaseInvocationOutput):
|
||||
"""Base class for invocations that output an image"""
|
||||
|
||||
type: Literal["image"] = "image"
|
||||
# fmt: off
|
||||
type: Literal["image_output"] = "image_output"
|
||||
image: ImageField = Field(default=None, description="The output image")
|
||||
width: int = Field(description="The width of the image in pixels")
|
||||
height: int = Field(description="The height of the image in pixels")
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
The generated OpenAPI schema, and all clients/types generated from it, will have
|
||||
the `type` and `image` properties marked as optional, even though we know they
|
||||
will always have a value by the time we can interact with them via the API.
|
||||
|
||||
Here's the same class, but with the schema customisation added:
|
||||
The OpenAPI schema that results from this `ImageOutput` will have the `type`,
|
||||
`image`, `width` and `height` properties marked as optional, even though we know
|
||||
they will always have a value.
|
||||
|
||||
```python
|
||||
class ImageOutput(BaseInvocationOutput):
|
||||
"""Base class for invocations that output an image"""
|
||||
|
||||
type: Literal["image"] = "image"
|
||||
# fmt: off
|
||||
type: Literal["image_output"] = "image_output"
|
||||
image: ImageField = Field(default=None, description="The output image")
|
||||
width: int = Field(description="The width of the image in pixels")
|
||||
height: int = Field(description="The height of the image in pixels")
|
||||
# fmt: on
|
||||
|
||||
# Add schema customization
|
||||
class Config:
|
||||
schema_extra = {
|
||||
'required': [
|
||||
'type',
|
||||
'image',
|
||||
]
|
||||
}
|
||||
schema_extra = {"required": ["type", "image", "width", "height"]}
|
||||
```
|
||||
|
||||
The resultant schema (and any API client or types generated from it) will now
|
||||
have see `type` as string literal `"image"` and `image` as an `ImageField`
|
||||
object.
|
||||
With the customization in place, the schema will now show these properties as
|
||||
required, obviating the need for extensive null checks in client code.
|
||||
|
||||
See this `pydantic` issue for discussion on this solution:
|
||||
<https://github.com/pydantic/pydantic/discussions/4577>
|
||||
|
@ -506,8 +506,8 @@
|
||||
"isScheduled": "Canceling",
|
||||
"setType": "Set cancel type"
|
||||
},
|
||||
"promptPlaceholder": "Type prompt here. [negative tokens], (upweight)++, (downweight)--, swap and blend are available (see docs)",
|
||||
"negativePrompts": "Negative Prompts",
|
||||
"positivePromptPlaceholder": "Positive Prompt",
|
||||
"negativePromptPlaceholder": "Negative Prompt",
|
||||
"sendTo": "Send to",
|
||||
"sendToImg2Img": "Send to Image to Image",
|
||||
"sendToUnifiedCanvas": "Send To Unified Canvas",
|
||||
|
@ -3,7 +3,6 @@ import {
|
||||
DragEndEvent,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
KeyboardSensor,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
pointerWithin,
|
||||
@ -15,6 +14,7 @@ import OverlayDragImage from './OverlayDragImage';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { isImageDTO } from 'services/types/guards';
|
||||
import { snapCenterToCursor } from '@dnd-kit/modifiers';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
||||
type ImageDndContextProps = PropsWithChildren;
|
||||
|
||||
@ -40,11 +40,11 @@ const ImageDndContext = (props: ImageDndContextProps) => {
|
||||
);
|
||||
|
||||
const mouseSensor = useSensor(MouseSensor, {
|
||||
activationConstraint: { distance: 15 },
|
||||
activationConstraint: { delay: 250, tolerance: 5 },
|
||||
});
|
||||
|
||||
const touchSensor = useSensor(TouchSensor, {
|
||||
activationConstraint: { distance: 15 },
|
||||
activationConstraint: { delay: 250, tolerance: 5 },
|
||||
});
|
||||
// TODO: Use KeyboardSensor - needs composition of multiple collisionDetection algos
|
||||
// Alternatively, fix `rectIntersection` collection detection to work with the drag overlay
|
||||
@ -62,7 +62,25 @@ const ImageDndContext = (props: ImageDndContextProps) => {
|
||||
>
|
||||
{props.children}
|
||||
<DragOverlay dropAnimation={null} modifiers={[snapCenterToCursor]}>
|
||||
{draggedImage && <OverlayDragImage image={draggedImage} />}
|
||||
<AnimatePresence>
|
||||
{draggedImage && (
|
||||
<motion.div
|
||||
layout
|
||||
key="overlay-drag-image"
|
||||
initial={{
|
||||
opacity: 0,
|
||||
scale: 0.7,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: { duration: 0.1 },
|
||||
}}
|
||||
>
|
||||
<OverlayDragImage image={draggedImage} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
);
|
||||
|
@ -8,7 +8,6 @@ import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
||||
import { getFullBaseLayerBlob } from 'features/canvas/util/getFullBaseLayerBlob';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'canvasCopiedToClipboardListener' });
|
||||
export const MERGED_CANVAS_FILENAME = 'mergedCanvas.png';
|
||||
|
||||
export const addCanvasMergedListener = () => {
|
||||
startAppListening({
|
||||
@ -49,12 +48,15 @@ export const addCanvasMergedListener = () => {
|
||||
const imageUploadedRequest = dispatch(
|
||||
imageUploaded({
|
||||
formData: {
|
||||
file: new File([blob], MERGED_CANVAS_FILENAME, {
|
||||
file: new File([blob], 'mergedCanvas.png', {
|
||||
type: 'image/png',
|
||||
}),
|
||||
},
|
||||
imageCategory: 'general',
|
||||
isIntermediate: true,
|
||||
postUploadAction: {
|
||||
type: 'TOAST_CANVAS_MERGED',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -6,8 +6,6 @@ import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { imageUpserted } from 'features/gallery/store/imagesSlice';
|
||||
|
||||
export const SAVED_CANVAS_FILENAME = 'savedCanvas.png';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
|
||||
|
||||
export const addCanvasSavedToGalleryListener = () => {
|
||||
@ -33,12 +31,15 @@ export const addCanvasSavedToGalleryListener = () => {
|
||||
const imageUploadedRequest = dispatch(
|
||||
imageUploaded({
|
||||
formData: {
|
||||
file: new File([blob], SAVED_CANVAS_FILENAME, {
|
||||
file: new File([blob], 'savedCanvas.png', {
|
||||
type: 'image/png',
|
||||
}),
|
||||
},
|
||||
imageCategory: 'general',
|
||||
isIntermediate: false,
|
||||
postUploadAction: {
|
||||
type: 'TOAST_CANVAS_SAVED_TO_GALLERY',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { AnyAction } from '@reduxjs/toolkit';
|
||||
import { AnyListenerPredicate } from '@reduxjs/toolkit';
|
||||
import { startAppListening } from '..';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { controlNetImageProcessed } from 'features/controlNet/store/actions';
|
||||
import {
|
||||
controlNetAutoConfigToggled,
|
||||
controlNetImageChanged,
|
||||
controlNetModelChanged,
|
||||
controlNetProcessorParamsChanged,
|
||||
controlNetProcessorTypeChanged,
|
||||
} from 'features/controlNet/store/controlNetSlice';
|
||||
@ -11,19 +13,26 @@ import { RootState } from 'app/store/store';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'controlNet' });
|
||||
|
||||
const predicate = (action: AnyAction, state: RootState) => {
|
||||
const predicate: AnyListenerPredicate<RootState> = (action, state) => {
|
||||
const isActionMatched =
|
||||
controlNetProcessorParamsChanged.match(action) ||
|
||||
controlNetModelChanged.match(action) ||
|
||||
controlNetImageChanged.match(action) ||
|
||||
controlNetProcessorTypeChanged.match(action);
|
||||
controlNetProcessorTypeChanged.match(action) ||
|
||||
controlNetAutoConfigToggled.match(action);
|
||||
|
||||
if (!isActionMatched) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { controlImage, processorType } =
|
||||
const { controlImage, processorType, shouldAutoConfig } =
|
||||
state.controlNet.controlNets[action.payload.controlNetId];
|
||||
|
||||
if (controlNetModelChanged.match(action) && !shouldAutoConfig) {
|
||||
// do not process if the action is a model change but the processor settings are dirty
|
||||
return false;
|
||||
}
|
||||
|
||||
const isProcessorSelected = processorType !== 'none';
|
||||
|
||||
const isBusy = state.system.isProcessing;
|
||||
@ -49,7 +58,10 @@ export const addControlNetAutoProcessListener = () => {
|
||||
|
||||
// Cancel any in-progress instances of this listener
|
||||
cancelActiveListeners();
|
||||
|
||||
moduleLog.trace(
|
||||
{ data: action.payload },
|
||||
'ControlNet auto-process triggered'
|
||||
);
|
||||
// Delay before starting actual work
|
||||
await delay(300);
|
||||
|
||||
|
@ -3,8 +3,10 @@ import { imageUploaded } from 'services/thunks/image';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { imageUpserted } from 'features/gallery/store/imagesSlice';
|
||||
import { SAVED_CANVAS_FILENAME } from './canvasSavedToGallery';
|
||||
import { MERGED_CANVAS_FILENAME } from './canvasMerged';
|
||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||
import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
|
||||
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
||||
import { fieldValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'image' });
|
||||
|
||||
@ -21,23 +23,48 @@ export const addImageUploadedFulfilledListener = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalFileName = action.meta.arg.formData.file.name;
|
||||
|
||||
dispatch(imageUpserted(image));
|
||||
|
||||
if (originalFileName === SAVED_CANVAS_FILENAME) {
|
||||
const { postUploadAction } = action.meta.arg;
|
||||
|
||||
if (postUploadAction?.type === 'TOAST_CANVAS_SAVED_TO_GALLERY') {
|
||||
dispatch(
|
||||
addToast({ title: 'Canvas Saved to Gallery', status: 'success' })
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (originalFileName === MERGED_CANVAS_FILENAME) {
|
||||
if (postUploadAction?.type === 'TOAST_CANVAS_MERGED') {
|
||||
dispatch(addToast({ title: 'Canvas Merged', status: 'success' }));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
|
||||
if (postUploadAction?.type === 'SET_CANVAS_INITIAL_IMAGE') {
|
||||
dispatch(setInitialCanvasImage(image));
|
||||
return;
|
||||
}
|
||||
|
||||
if (postUploadAction?.type === 'SET_CONTROLNET_IMAGE') {
|
||||
const { controlNetId } = postUploadAction;
|
||||
dispatch(controlNetImageChanged({ controlNetId, controlImage: image }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (postUploadAction?.type === 'SET_INITIAL_IMAGE') {
|
||||
dispatch(initialImageChanged(image));
|
||||
return;
|
||||
}
|
||||
|
||||
if (postUploadAction?.type === 'SET_NODES_IMAGE') {
|
||||
const { nodeId, fieldName } = postUploadAction;
|
||||
dispatch(fieldValueChanged({ nodeId, fieldName, value: image }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (postUploadAction?.type === 'TOAST_UPLOADED') {
|
||||
dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
|
||||
return;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,16 +1,26 @@
|
||||
import { Box, Flex, Icon, IconButtonProps, Image } from '@chakra-ui/react';
|
||||
import {
|
||||
Box,
|
||||
ChakraProps,
|
||||
Flex,
|
||||
Icon,
|
||||
IconButtonProps,
|
||||
Image,
|
||||
} from '@chakra-ui/react';
|
||||
import { useDraggable, useDroppable } from '@dnd-kit/core';
|
||||
import { useCombinedRefs } from '@dnd-kit/utilities';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { IAIImageFallback } from 'common/components/IAIImageFallback';
|
||||
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { ReactElement, SyntheticEvent } from 'react';
|
||||
import { ReactElement, SyntheticEvent, useCallback } from 'react';
|
||||
import { memo, useRef } from 'react';
|
||||
import { FaImage, FaTimes } from 'react-icons/fa';
|
||||
import { FaImage, FaTimes, FaUpload } from 'react-icons/fa';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import IAIDropOverlay from './IAIDropOverlay';
|
||||
import { PostUploadAction, imageUploaded } from 'services/thunks/image';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
|
||||
type IAIDndImageProps = {
|
||||
image: ImageDTO | null | undefined;
|
||||
@ -23,9 +33,12 @@ type IAIDndImageProps = {
|
||||
withMetadataOverlay?: boolean;
|
||||
isDragDisabled?: boolean;
|
||||
isDropDisabled?: boolean;
|
||||
isUploadDisabled?: boolean;
|
||||
fallback?: ReactElement;
|
||||
payloadImage?: ImageDTO | null | undefined;
|
||||
minSize?: number;
|
||||
postUploadAction?: PostUploadAction;
|
||||
imageSx?: ChakraProps['sx'];
|
||||
};
|
||||
|
||||
const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
@ -39,15 +52,20 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
withMetadataOverlay = false,
|
||||
isDropDisabled = false,
|
||||
isDragDisabled = false,
|
||||
isUploadDisabled = false,
|
||||
fallback = <IAIImageFallback />,
|
||||
payloadImage,
|
||||
minSize = 24,
|
||||
postUploadAction,
|
||||
imageSx,
|
||||
} = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const dndId = useRef(uuidv4());
|
||||
|
||||
const {
|
||||
isOver,
|
||||
setNodeRef: setDroppableRef,
|
||||
active,
|
||||
active: isDropActive,
|
||||
} = useDroppable({
|
||||
id: dndId.current,
|
||||
disabled: isDropDisabled,
|
||||
@ -60,16 +78,55 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef: setDraggableRef,
|
||||
isDragging,
|
||||
} = useDraggable({
|
||||
id: dndId.current,
|
||||
data: {
|
||||
image: payloadImage ? payloadImage : image,
|
||||
},
|
||||
disabled: isDragDisabled,
|
||||
disabled: isDragDisabled || !image,
|
||||
});
|
||||
|
||||
const handleOnDropAccepted = useCallback(
|
||||
(files: Array<File>) => {
|
||||
const file = files[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
imageUploaded({
|
||||
formData: { file },
|
||||
imageCategory: 'user',
|
||||
isIntermediate: false,
|
||||
postUploadAction,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, postUploadAction]
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },
|
||||
onDropAccepted: handleOnDropAccepted,
|
||||
noDrag: true,
|
||||
multiple: false,
|
||||
disabled: isUploadDisabled,
|
||||
});
|
||||
|
||||
const setNodeRef = useCombinedRefs(setDroppableRef, setDraggableRef);
|
||||
|
||||
const uploadButtonStyles = isUploadDisabled
|
||||
? {}
|
||||
: {
|
||||
cursor: 'pointer',
|
||||
bg: 'base.800',
|
||||
_hover: {
|
||||
bg: 'base.750',
|
||||
color: 'base.300',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
@ -81,7 +138,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
minW: minSize,
|
||||
minH: minSize,
|
||||
userSelect: 'none',
|
||||
cursor: 'grab',
|
||||
cursor: isDragDisabled || !image ? 'auto' : 'grab',
|
||||
}}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
@ -92,7 +149,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
sx={{
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
position: 'relative',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
@ -108,6 +164,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
maxW: 'full',
|
||||
maxH: 'full',
|
||||
borderRadius: 'base',
|
||||
...imageSx,
|
||||
}}
|
||||
/>
|
||||
{withMetadataOverlay && <ImageMetadataOverlay image={image} />}
|
||||
@ -130,7 +187,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
</Box>
|
||||
)}
|
||||
<AnimatePresence>
|
||||
{active && <IAIDropOverlay isOver={isOver} />}
|
||||
{isDropActive && <IAIDropOverlay isOver={isOver} />}
|
||||
</AnimatePresence>
|
||||
</Flex>
|
||||
)}
|
||||
@ -139,24 +196,28 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
<Flex
|
||||
sx={{
|
||||
minH: minSize,
|
||||
bg: 'base.850',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 'base',
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: '0.1s',
|
||||
color: 'base.500',
|
||||
...uploadButtonStyles,
|
||||
}}
|
||||
{...getRootProps()}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<Icon
|
||||
as={FaImage}
|
||||
as={isUploadDisabled ? FaImage : FaUpload}
|
||||
sx={{
|
||||
boxSize: 24,
|
||||
color: 'base.500',
|
||||
boxSize: 12,
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<AnimatePresence>
|
||||
{active && <IAIDropOverlay isOver={isOver} />}
|
||||
{isDropActive && <IAIDropOverlay isOver={isOver} />}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)}
|
||||
|
@ -30,7 +30,7 @@ export const IAIDropOverlay = (props: Props) => {
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
insetInlineStart: 0,
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
}}
|
||||
@ -39,7 +39,7 @@ export const IAIDropOverlay = (props: Props) => {
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
insetInlineStart: 0,
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
bg: 'base.900',
|
||||
@ -56,7 +56,7 @@ export const IAIDropOverlay = (props: Props) => {
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
insetInlineStart: 0,
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
opacity: 1,
|
||||
|
@ -1,17 +1,13 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import useImageUploader from 'common/hooks/useImageUploader';
|
||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import { ResourceKey } from 'i18next';
|
||||
import {
|
||||
KeyboardEvent,
|
||||
memo,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||
@ -19,10 +15,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { imageUploaded } from 'services/thunks/image';
|
||||
import ImageUploadOverlay from './ImageUploadOverlay';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { filter, map, some } from 'lodash-es';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import { ErrorCode } from 'react-dropzone';
|
||||
|
||||
const selector = createSelector(
|
||||
[systemSelector, activeTabNameSelector],
|
||||
@ -71,6 +65,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
||||
formData: { file },
|
||||
imageCategory: 'user',
|
||||
isIntermediate: false,
|
||||
postUploadAction: { type: 'TOAST_UPLOADED' },
|
||||
})
|
||||
);
|
||||
},
|
||||
|
@ -9,25 +9,14 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
const readinessSelector = createSelector(
|
||||
[generationSelector, systemSelector, activeTabNameSelector],
|
||||
(generation, system, activeTabName) => {
|
||||
const {
|
||||
positivePrompt: prompt,
|
||||
shouldGenerateVariations,
|
||||
seedWeights,
|
||||
initialImage,
|
||||
seed,
|
||||
} = generation;
|
||||
const { shouldGenerateVariations, seedWeights, initialImage, seed } =
|
||||
generation;
|
||||
|
||||
const { isProcessing, isConnected } = system;
|
||||
|
||||
let isReady = true;
|
||||
const reasonsWhyNotReady: string[] = [];
|
||||
|
||||
// Cannot generate without a prompt
|
||||
if (!prompt || Boolean(prompt.match(/^[\s\r\n]+$/))) {
|
||||
isReady = false;
|
||||
reasonsWhyNotReady.push('Missing prompt');
|
||||
}
|
||||
|
||||
if (activeTabName === 'img2img' && !initialImage) {
|
||||
isReady = false;
|
||||
reasonsWhyNotReady.push('No initial image selected');
|
||||
|
@ -8,20 +8,8 @@ import {
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import ParamControlNetModel from './parameters/ParamControlNetModel';
|
||||
import ParamControlNetWeight from './parameters/ParamControlNetWeight';
|
||||
import {
|
||||
Checkbox,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
HStack,
|
||||
TabList,
|
||||
TabPanels,
|
||||
Tabs,
|
||||
Tab,
|
||||
TabPanel,
|
||||
Box,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaCopy, FaPlus, FaTrash, FaWrench } from 'react-icons/fa';
|
||||
import { Flex, Box, ChakraProps } from '@chakra-ui/react';
|
||||
import { FaCopy, FaTrash } from 'react-icons/fa';
|
||||
|
||||
import ParamControlNetBeginEnd from './parameters/ParamControlNetBeginEnd';
|
||||
import ControlNetImagePreview from './ControlNetImagePreview';
|
||||
@ -30,10 +18,11 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import { useToggle } from 'react-use';
|
||||
import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect';
|
||||
import ControlNetProcessorComponent from './ControlNetProcessorComponent';
|
||||
import ControlNetPreprocessButton from './ControlNetPreprocessButton';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import IAISwitch from 'common/components/IAISwitch';
|
||||
import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons';
|
||||
import { ChevronUpIcon } from '@chakra-ui/icons';
|
||||
import ParamControlNetShouldAutoConfig from './ParamControlNetShouldAutoConfig';
|
||||
|
||||
const expandedControlImageSx: ChakraProps['sx'] = { maxH: 96 };
|
||||
|
||||
type ControlNetProps = {
|
||||
controlNet: ControlNetConfig;
|
||||
@ -51,9 +40,10 @@ const ControlNet = (props: ControlNetProps) => {
|
||||
processedControlImage,
|
||||
processorNode,
|
||||
processorType,
|
||||
shouldAutoConfig,
|
||||
} = props.controlNet;
|
||||
const dispatch = useAppDispatch();
|
||||
const [shouldShowAdvanced, onToggleAdvanced] = useToggle(false);
|
||||
const [isExpanded, toggleIsExpanded] = useToggle(false);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
dispatch(controlNetRemoved({ controlNetId }));
|
||||
@ -77,6 +67,7 @@ const ControlNet = (props: ControlNetProps) => {
|
||||
p: 3,
|
||||
bg: 'base.850',
|
||||
borderRadius: 'base',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ gap: 2 }}>
|
||||
@ -115,27 +106,38 @@ const ControlNet = (props: ControlNetProps) => {
|
||||
/>
|
||||
<IAIIconButton
|
||||
size="sm"
|
||||
aria-label="Expand"
|
||||
onClick={onToggleAdvanced}
|
||||
aria-label="Show All Options"
|
||||
onClick={toggleIsExpanded}
|
||||
variant="link"
|
||||
icon={
|
||||
<ChevronUpIcon
|
||||
sx={{
|
||||
boxSize: 4,
|
||||
color: 'base.300',
|
||||
transform: shouldShowAdvanced
|
||||
? 'rotate(0deg)'
|
||||
: 'rotate(180deg)',
|
||||
transform: isExpanded ? 'rotate(0deg)' : 'rotate(180deg)',
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: 'normal',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{!shouldAutoConfig && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
w: 1.5,
|
||||
h: 1.5,
|
||||
borderRadius: 'full',
|
||||
bg: 'error.200',
|
||||
top: 4,
|
||||
insetInlineEnd: 4,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
{isEnabled && (
|
||||
<>
|
||||
<Flex sx={{ gap: 4 }}>
|
||||
<Flex sx={{ gap: 4, w: 'full' }}>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDir: 'column',
|
||||
@ -143,7 +145,7 @@ const ControlNet = (props: ControlNetProps) => {
|
||||
w: 'full',
|
||||
h: 24,
|
||||
paddingInlineStart: 1,
|
||||
paddingInlineEnd: shouldShowAdvanced ? 1 : 0,
|
||||
paddingInlineEnd: isExpanded ? 1 : 0,
|
||||
pb: 2,
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
@ -160,7 +162,7 @@ const ControlNet = (props: ControlNetProps) => {
|
||||
mini
|
||||
/>
|
||||
</Flex>
|
||||
{!shouldShowAdvanced && (
|
||||
{!isExpanded && (
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
@ -174,10 +176,13 @@ const ControlNet = (props: ControlNetProps) => {
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
{shouldShowAdvanced && (
|
||||
{isExpanded && (
|
||||
<>
|
||||
<Box pt={2}>
|
||||
<ControlNetImagePreview controlNet={props.controlNet} />
|
||||
<Box mt={2}>
|
||||
<ControlNetImagePreview
|
||||
controlNet={props.controlNet}
|
||||
imageSx={expandedControlImageSx}
|
||||
/>
|
||||
</Box>
|
||||
<ParamControlNetProcessorSelect
|
||||
controlNetId={controlNetId}
|
||||
@ -187,72 +192,16 @@ const ControlNet = (props: ControlNetProps) => {
|
||||
controlNetId={controlNetId}
|
||||
processorNode={processorNode}
|
||||
/>
|
||||
<ParamControlNetShouldAutoConfig
|
||||
controlNetId={controlNetId}
|
||||
shouldAutoConfig={shouldAutoConfig}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex sx={{ flexDir: 'column', gap: 3 }}>
|
||||
<ControlNetImagePreview controlNet={props.controlNet} />
|
||||
<ParamControlNetModel controlNetId={controlNetId} model={model} />
|
||||
<Tabs
|
||||
isFitted
|
||||
orientation="horizontal"
|
||||
variant="enclosed"
|
||||
size="sm"
|
||||
colorScheme="accent"
|
||||
>
|
||||
<TabList>
|
||||
<Tab
|
||||
sx={{ 'button&': { _selected: { borderBottomColor: 'base.800' } } }}
|
||||
>
|
||||
Model Config
|
||||
</Tab>
|
||||
<Tab
|
||||
sx={{ 'button&': { _selected: { borderBottomColor: 'base.800' } } }}
|
||||
>
|
||||
Preprocess
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels sx={{ pt: 2 }}>
|
||||
<TabPanel sx={{ p: 0 }}>
|
||||
<ParamControlNetWeight
|
||||
controlNetId={controlNetId}
|
||||
weight={weight}
|
||||
/>
|
||||
<ParamControlNetBeginEnd
|
||||
controlNetId={controlNetId}
|
||||
beginStepPct={beginStepPct}
|
||||
endStepPct={endStepPct}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel sx={{ p: 0 }}>
|
||||
<ParamControlNetProcessorSelect
|
||||
controlNetId={controlNetId}
|
||||
processorNode={processorNode}
|
||||
/>
|
||||
<ControlNetProcessorComponent
|
||||
controlNetId={controlNetId}
|
||||
processorNode={processorNode}
|
||||
/>
|
||||
<ControlNetPreprocessButton controlNet={props.controlNet} />
|
||||
{/* <IAIButton
|
||||
size="sm"
|
||||
leftIcon={<FaUndo />}
|
||||
onClick={handleReset}
|
||||
isDisabled={Boolean(!processedControlImage)}
|
||||
>
|
||||
Reset Processing
|
||||
</IAIButton> */}
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<IAIButton onClick={handleDelete}>Remove ControlNet</IAIButton>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ControlNet);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import {
|
||||
ControlNetConfig,
|
||||
@ -6,41 +6,44 @@ import {
|
||||
controlNetSelector,
|
||||
} from '../store/controlNetSlice';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { Box, ChakraProps, Flex } from '@chakra-ui/react';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { IAIImageFallback } from 'common/components/IAIImageFallback';
|
||||
import { useHoverDirty } from 'react-use';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { FaUndo } from 'react-icons/fa';
|
||||
|
||||
const selector = createSelector(
|
||||
controlNetSelector,
|
||||
(controlNet) => {
|
||||
const { isProcessingControlImage } = controlNet;
|
||||
return { isProcessingControlImage };
|
||||
const { pendingControlImages } = controlNet;
|
||||
return { pendingControlImages };
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
type Props = {
|
||||
controlNet: ControlNetConfig;
|
||||
imageSx?: ChakraProps['sx'];
|
||||
};
|
||||
|
||||
const ControlNetImagePreview = (props: Props) => {
|
||||
const { imageSx } = props;
|
||||
const { controlNetId, controlImage, processedControlImage, processorType } =
|
||||
props.controlNet;
|
||||
const dispatch = useAppDispatch();
|
||||
const { isProcessingControlImage } = useAppSelector(selector);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { pendingControlImages } = useAppSelector(selector);
|
||||
|
||||
const isMouseOverImage = useHoverDirty(containerRef);
|
||||
const [isMouseOverImage, setIsMouseOverImage] = useState(false);
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(droppedImage: ImageDTO) => {
|
||||
if (controlImage?.image_name === droppedImage.image_name) {
|
||||
return;
|
||||
}
|
||||
setIsMouseOverImage(false);
|
||||
dispatch(
|
||||
controlNetImageChanged({ controlNetId, controlImage: droppedImage })
|
||||
);
|
||||
@ -48,6 +51,17 @@ const ControlNetImagePreview = (props: Props) => {
|
||||
[controlImage, controlNetId, dispatch]
|
||||
);
|
||||
|
||||
const handleResetControlImage = useCallback(() => {
|
||||
dispatch(controlNetImageChanged({ controlNetId, controlImage: null }));
|
||||
}, [controlNetId, dispatch]);
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
setIsMouseOverImage(true);
|
||||
}, []);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
setIsMouseOverImage(false);
|
||||
}, []);
|
||||
|
||||
const shouldShowProcessedImageBackdrop =
|
||||
Number(controlImage?.width) > Number(processedControlImage?.width) ||
|
||||
Number(controlImage?.height) > Number(processedControlImage?.height);
|
||||
@ -56,13 +70,14 @@ const ControlNetImagePreview = (props: Props) => {
|
||||
controlImage &&
|
||||
processedControlImage &&
|
||||
!isMouseOverImage &&
|
||||
!isProcessingControlImage &&
|
||||
!pendingControlImages.includes(controlNetId) &&
|
||||
processorType !== 'none';
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={containerRef}
|
||||
sx={{ position: 'relative', w: 'full', h: 'full', aspectRatio: '1/1' }}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
sx={{ position: 'relative', w: 'full', h: 'full' }}
|
||||
>
|
||||
<IAIDndImage
|
||||
image={controlImage}
|
||||
@ -70,10 +85,14 @@ const ControlNetImagePreview = (props: Props) => {
|
||||
isDropDisabled={Boolean(
|
||||
processedControlImage && processorType !== 'none'
|
||||
)}
|
||||
isUploadDisabled={Boolean(controlImage)}
|
||||
postUploadAction={{ type: 'SET_CONTROLNET_IMAGE', controlNetId }}
|
||||
imageSx={imageSx}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{shouldShowProcessedImage && (
|
||||
<motion.div
|
||||
style={{ width: '100%' }}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
@ -86,18 +105,13 @@ const ControlNetImagePreview = (props: Props) => {
|
||||
transition: { duration: 0.1 },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
}}
|
||||
>
|
||||
<>
|
||||
{shouldShowProcessedImageBackdrop && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
bg: 'base.900',
|
||||
@ -118,13 +132,15 @@ const ControlNetImagePreview = (props: Props) => {
|
||||
image={processedControlImage}
|
||||
onDrop={handleDrop}
|
||||
payloadImage={controlImage}
|
||||
isUploadDisabled={true}
|
||||
imageSx={imageSx}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{isProcessingControlImage && (
|
||||
{pendingControlImages.includes(controlNetId) && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
@ -137,6 +153,22 @@ const ControlNetImagePreview = (props: Props) => {
|
||||
<IAIImageFallback />
|
||||
</Box>
|
||||
)}
|
||||
{controlImage && (
|
||||
<Flex sx={{ position: 'absolute', top: 0, insetInlineEnd: 0 }}>
|
||||
<IAIIconButton
|
||||
aria-label="Reset Control Image"
|
||||
tooltip="Reset Control Image"
|
||||
size="sm"
|
||||
onClick={handleResetControlImage}
|
||||
icon={<FaUndo />}
|
||||
variant="link"
|
||||
sx={{
|
||||
p: 2,
|
||||
color: 'base.50',
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,29 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import IAISwitch from 'common/components/IAISwitch';
|
||||
import { controlNetAutoConfigToggled } from 'features/controlNet/store/controlNetSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
type Props = {
|
||||
controlNetId: string;
|
||||
shouldAutoConfig: boolean;
|
||||
};
|
||||
|
||||
const ParamControlNetShouldAutoConfig = (props: Props) => {
|
||||
const { controlNetId, shouldAutoConfig } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleShouldAutoConfigChanged = useCallback(() => {
|
||||
dispatch(controlNetAutoConfigToggled({ controlNetId }));
|
||||
}, [controlNetId, dispatch]);
|
||||
|
||||
return (
|
||||
<IAISwitch
|
||||
label="Auto configure processor"
|
||||
aria-label="Auto configure processor"
|
||||
isChecked={shouldAutoConfig}
|
||||
onChange={handleShouldAutoConfigChanged}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ParamControlNetShouldAutoConfig);
|
@ -7,12 +7,12 @@ import {
|
||||
import { controlNetModelChanged } from 'features/controlNet/store/controlNetSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
type ParamIsControlNetModelProps = {
|
||||
type ParamControlNetModelProps = {
|
||||
controlNetId: string;
|
||||
model: ControlNetModel;
|
||||
};
|
||||
|
||||
const ParamIsControlNetModel = (props: ParamIsControlNetModelProps) => {
|
||||
const ParamControlNetModel = (props: ParamControlNetModelProps) => {
|
||||
const { controlNetId, model } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@ -38,4 +38,4 @@ const ParamIsControlNetModel = (props: ParamIsControlNetModelProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ParamIsControlNetModel);
|
||||
export default memo(ParamControlNetModel);
|
||||
|
@ -4,5 +4,5 @@ import { ControlNetState } from './controlNetSlice';
|
||||
* ControlNet slice persist denylist
|
||||
*/
|
||||
export const controlNetDenylist: (keyof ControlNetState)[] = [
|
||||
'isProcessingControlImage',
|
||||
'pendingControlImages',
|
||||
];
|
||||
|
@ -9,12 +9,15 @@ import {
|
||||
} from './types';
|
||||
import {
|
||||
CONTROLNET_MODELS,
|
||||
CONTROLNET_MODEL_MAP,
|
||||
CONTROLNET_PROCESSORS,
|
||||
ControlNetModel,
|
||||
} from './constants';
|
||||
import { controlNetImageProcessed } from './actions';
|
||||
import { imageDeleted, imageUrlsReceived } from 'services/thunks/image';
|
||||
import { forEach } from 'lodash-es';
|
||||
import { isAnySessionRejected } from 'services/thunks/session';
|
||||
import { appSocketInvocationError } from 'services/events/actions';
|
||||
|
||||
export const initialControlNet: Omit<ControlNetConfig, 'controlNetId'> = {
|
||||
isEnabled: true,
|
||||
@ -27,6 +30,7 @@ export const initialControlNet: Omit<ControlNetConfig, 'controlNetId'> = {
|
||||
processorType: 'canny_image_processor',
|
||||
processorNode: CONTROLNET_PROCESSORS.canny_image_processor
|
||||
.default as RequiredCannyImageProcessorInvocation,
|
||||
shouldAutoConfig: true,
|
||||
};
|
||||
|
||||
export type ControlNetConfig = {
|
||||
@ -40,18 +44,19 @@ export type ControlNetConfig = {
|
||||
processedControlImage: ImageDTO | null;
|
||||
processorType: ControlNetProcessorType;
|
||||
processorNode: RequiredControlNetProcessorNode;
|
||||
shouldAutoConfig: boolean;
|
||||
};
|
||||
|
||||
export type ControlNetState = {
|
||||
controlNets: Record<string, ControlNetConfig>;
|
||||
isEnabled: boolean;
|
||||
isProcessingControlImage: boolean;
|
||||
pendingControlImages: string[];
|
||||
};
|
||||
|
||||
export const initialControlNetState: ControlNetState = {
|
||||
controlNets: {},
|
||||
isEnabled: false,
|
||||
isProcessingControlImage: false,
|
||||
pendingControlImages: [],
|
||||
};
|
||||
|
||||
export const controlNetSlice = createSlice({
|
||||
@ -114,7 +119,7 @@ export const controlNetSlice = createSlice({
|
||||
controlImage !== null &&
|
||||
state.controlNets[controlNetId].processorType !== 'none'
|
||||
) {
|
||||
state.isProcessingControlImage = true;
|
||||
state.pendingControlImages.push(controlNetId);
|
||||
}
|
||||
},
|
||||
controlNetProcessedImageChanged: (
|
||||
@ -127,7 +132,9 @@ export const controlNetSlice = createSlice({
|
||||
const { controlNetId, processedControlImage } = action.payload;
|
||||
state.controlNets[controlNetId].processedControlImage =
|
||||
processedControlImage;
|
||||
state.isProcessingControlImage = false;
|
||||
state.pendingControlImages = state.pendingControlImages.filter(
|
||||
(id) => id !== controlNetId
|
||||
);
|
||||
},
|
||||
controlNetModelChanged: (
|
||||
state,
|
||||
@ -135,6 +142,21 @@ export const controlNetSlice = createSlice({
|
||||
) => {
|
||||
const { controlNetId, model } = action.payload;
|
||||
state.controlNets[controlNetId].model = model;
|
||||
state.controlNets[controlNetId].processedControlImage = null;
|
||||
|
||||
if (state.controlNets[controlNetId].shouldAutoConfig) {
|
||||
const processorType = CONTROLNET_MODEL_MAP[model];
|
||||
if (processorType) {
|
||||
state.controlNets[controlNetId].processorType = processorType;
|
||||
state.controlNets[controlNetId].processorNode = CONTROLNET_PROCESSORS[
|
||||
processorType
|
||||
].default as RequiredControlNetProcessorNode;
|
||||
} else {
|
||||
state.controlNets[controlNetId].processorType = 'none';
|
||||
state.controlNets[controlNetId].processorNode = CONTROLNET_PROCESSORS
|
||||
.none.default as RequiredControlNetProcessorNode;
|
||||
}
|
||||
}
|
||||
},
|
||||
controlNetWeightChanged: (
|
||||
state,
|
||||
@ -173,6 +195,7 @@ export const controlNetSlice = createSlice({
|
||||
...processorNode,
|
||||
...changes,
|
||||
};
|
||||
state.controlNets[controlNetId].shouldAutoConfig = false;
|
||||
},
|
||||
controlNetProcessorTypeChanged: (
|
||||
state,
|
||||
@ -182,10 +205,40 @@ export const controlNetSlice = createSlice({
|
||||
}>
|
||||
) => {
|
||||
const { controlNetId, processorType } = action.payload;
|
||||
state.controlNets[controlNetId].processedControlImage = null;
|
||||
state.controlNets[controlNetId].processorType = processorType;
|
||||
state.controlNets[controlNetId].processorNode = CONTROLNET_PROCESSORS[
|
||||
processorType
|
||||
].default as RequiredControlNetProcessorNode;
|
||||
state.controlNets[controlNetId].shouldAutoConfig = false;
|
||||
},
|
||||
controlNetAutoConfigToggled: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
controlNetId: string;
|
||||
}>
|
||||
) => {
|
||||
const { controlNetId } = action.payload;
|
||||
const newShouldAutoConfig =
|
||||
!state.controlNets[controlNetId].shouldAutoConfig;
|
||||
|
||||
if (newShouldAutoConfig) {
|
||||
// manage the processor for the user
|
||||
const processorType =
|
||||
CONTROLNET_MODEL_MAP[state.controlNets[controlNetId].model];
|
||||
if (processorType) {
|
||||
state.controlNets[controlNetId].processorType = processorType;
|
||||
state.controlNets[controlNetId].processorNode = CONTROLNET_PROCESSORS[
|
||||
processorType
|
||||
].default as RequiredControlNetProcessorNode;
|
||||
} else {
|
||||
state.controlNets[controlNetId].processorType = 'none';
|
||||
state.controlNets[controlNetId].processorNode = CONTROLNET_PROCESSORS
|
||||
.none.default as RequiredControlNetProcessorNode;
|
||||
}
|
||||
}
|
||||
|
||||
state.controlNets[controlNetId].shouldAutoConfig = newShouldAutoConfig;
|
||||
},
|
||||
controlNetReset: () => {
|
||||
return { ...initialControlNetState };
|
||||
@ -196,7 +249,7 @@ export const controlNetSlice = createSlice({
|
||||
if (
|
||||
state.controlNets[action.payload.controlNetId].controlImage !== null
|
||||
) {
|
||||
state.isProcessingControlImage = true;
|
||||
state.pendingControlImages.push(action.payload.controlNetId);
|
||||
}
|
||||
});
|
||||
|
||||
@ -229,6 +282,14 @@ export const controlNetSlice = createSlice({
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
builder.addCase(appSocketInvocationError, (state, action) => {
|
||||
state.pendingControlImages = [];
|
||||
});
|
||||
|
||||
builder.addMatcher(isAnySessionRejected, (state, action) => {
|
||||
state.pendingControlImages = [];
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -247,6 +308,7 @@ export const {
|
||||
controlNetProcessorParamsChanged,
|
||||
controlNetProcessorTypeChanged,
|
||||
controlNetReset,
|
||||
controlNetAutoConfigToggled,
|
||||
} = controlNetSlice.actions;
|
||||
|
||||
export default controlNetSlice.reducer;
|
||||
|
@ -50,21 +50,8 @@ const CurrentImagePreview = () => {
|
||||
shouldShowProgressInViewer,
|
||||
shouldAntialiasProgressImage,
|
||||
} = useAppSelector(imagesSelector);
|
||||
const { shouldFetchImages } = useAppSelector(configSelector);
|
||||
const toaster = useAppToaster();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleError = useCallback(() => {
|
||||
dispatch(imageSelected());
|
||||
if (shouldFetchImages) {
|
||||
toaster({
|
||||
title: 'Something went wrong, please refresh',
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
}, [dispatch, toaster, shouldFetchImages]);
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(droppedImage: ImageDTO) => {
|
||||
if (droppedImage.image_name === image?.image_name) {
|
||||
@ -90,6 +77,7 @@ const CurrentImagePreview = () => {
|
||||
src={progressImage.dataURL}
|
||||
width={progressImage.width}
|
||||
height={progressImage.height}
|
||||
draggable={false}
|
||||
sx={{
|
||||
objectFit: 'contain',
|
||||
maxWidth: 'full',
|
||||
@ -112,8 +100,8 @@ const CurrentImagePreview = () => {
|
||||
<IAIDndImage
|
||||
image={image}
|
||||
onDrop={handleDrop}
|
||||
onError={handleError}
|
||||
fallback={<IAIImageFallback sx={{ bg: 'none' }} />}
|
||||
isUploadDisabled={true}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
|
@ -279,6 +279,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
objectFit={
|
||||
shouldUseSingleGalleryColumn ? 'contain' : galleryImageObjectFit
|
||||
}
|
||||
draggable={false}
|
||||
rounded="md"
|
||||
src={thumbnail_url || image_url}
|
||||
fallback={<FaImage />}
|
||||
|
@ -13,12 +13,13 @@ const NodeGraphOverlay = () => {
|
||||
as="pre"
|
||||
fontFamily="monospace"
|
||||
position="absolute"
|
||||
top={10}
|
||||
top={2}
|
||||
right={2}
|
||||
opacity={0.7}
|
||||
background="base.800"
|
||||
p={2}
|
||||
maxHeight={500}
|
||||
maxWidth={500}
|
||||
overflowY="scroll"
|
||||
borderRadius="md"
|
||||
>
|
||||
|
@ -60,6 +60,11 @@ const ImageInputFieldComponent = (
|
||||
onDrop={handleDrop}
|
||||
onReset={handleReset}
|
||||
resetIconSize="sm"
|
||||
postUploadAction={{
|
||||
type: 'SET_NODES_IMAGE',
|
||||
nodeId,
|
||||
fieldName: field.name,
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
|
@ -63,7 +63,7 @@ export const addControlNetToLinearGraph = (
|
||||
image_name,
|
||||
image_origin,
|
||||
};
|
||||
} else if (controlImage && processorType !== 'none') {
|
||||
} else if (controlImage) {
|
||||
// The control image is preprocessed
|
||||
const { image_name, image_origin } = controlImage;
|
||||
controlNetNode.image = {
|
||||
|
@ -20,10 +20,7 @@ const ParamNegativeConditioning = () => {
|
||||
name="negativePrompt"
|
||||
value={negativePrompt}
|
||||
onChange={(e) => dispatch(setNegativePrompt(e.target.value))}
|
||||
placeholder={t('parameters.negativePrompts')}
|
||||
_focusVisible={{
|
||||
borderColor: 'error.600',
|
||||
}}
|
||||
placeholder={t('parameters.negativePromptPlaceholder')}
|
||||
fontSize="sm"
|
||||
minH={16}
|
||||
/>
|
||||
|
@ -70,13 +70,11 @@ const ParamPositiveConditioning = () => {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<FormControl
|
||||
isInvalid={prompt.length === 0 || Boolean(prompt.match(/^[\s\r\n]+$/))}
|
||||
>
|
||||
<FormControl>
|
||||
<IAITextarea
|
||||
id="prompt"
|
||||
name="prompt"
|
||||
placeholder={t('parameters.promptPlaceholder')}
|
||||
placeholder={t('parameters.positivePromptPlaceholder')}
|
||||
value={prompt}
|
||||
onChange={handleChangePrompt}
|
||||
onKeyDown={handleKeyDown}
|
||||
|
@ -6,11 +6,8 @@ import {
|
||||
initialImageChanged,
|
||||
} from 'features/parameters/store/generationSlice';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { generationSelector } from 'features/parameters/store/generationSelectors';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { configSelector } from '../../../../system/store/configSelectors';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { IAIImageFallback } from 'common/components/IAIImageFallback';
|
||||
@ -28,28 +25,7 @@ const selector = createSelector(
|
||||
|
||||
const InitialImagePreview = () => {
|
||||
const { initialImage } = useAppSelector(selector);
|
||||
const { shouldFetchImages } = useAppSelector(configSelector);
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const toaster = useAppToaster();
|
||||
|
||||
const handleError = useCallback(() => {
|
||||
dispatch(clearInitialImage());
|
||||
if (shouldFetchImages) {
|
||||
toaster({
|
||||
title: 'Something went wrong, please refresh',
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
} else {
|
||||
toaster({
|
||||
title: t('toast.parametersFailed'),
|
||||
description: t('toast.parametersFailedDesc'),
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
}, [dispatch, t, toaster, shouldFetchImages]);
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(droppedImage: ImageDTO) => {
|
||||
@ -81,6 +57,7 @@ const InitialImagePreview = () => {
|
||||
onDrop={handleDrop}
|
||||
onReset={handleReset}
|
||||
fallback={<IAIImageFallback sx={{ bg: 'none' }} />}
|
||||
postUploadAction={{ type: 'SET_INITIAL_IMAGE' }}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
|
@ -1,15 +1,11 @@
|
||||
import { UseToastOptions } from '@chakra-ui/react';
|
||||
import { PayloadAction, isAnyOf } from '@reduxjs/toolkit';
|
||||
import { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import * as InvokeAI from 'app/types/invokeai';
|
||||
|
||||
import { ProgressImage } from 'services/events/types';
|
||||
import { makeToast } from '../../../app/components/Toaster';
|
||||
import {
|
||||
sessionCanceled,
|
||||
sessionCreated,
|
||||
sessionInvoked,
|
||||
} from 'services/thunks/session';
|
||||
import { isAnySessionRejected, sessionCanceled } from 'services/thunks/session';
|
||||
import { receivedModels } from 'services/thunks/model';
|
||||
import { parsedOpenAPISchema } from 'features/nodes/store/nodesSlice';
|
||||
import { LogLevelName } from 'roarr';
|
||||
@ -462,8 +458,3 @@ export const {
|
||||
} = systemSlice.actions;
|
||||
|
||||
export default systemSlice.reducer;
|
||||
|
||||
const isAnySessionRejected = isAnyOf(
|
||||
sessionCreated.rejected,
|
||||
sessionInvoked.rejected
|
||||
);
|
||||
|
@ -32,7 +32,49 @@ export const imageMetadataReceived = createAppAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
type ImageUploadedArg = Parameters<(typeof ImagesService)['uploadImage']>[0];
|
||||
type ControlNetAction = {
|
||||
type: 'SET_CONTROLNET_IMAGE';
|
||||
controlNetId: string;
|
||||
};
|
||||
|
||||
type InitialImageAction = {
|
||||
type: 'SET_INITIAL_IMAGE';
|
||||
};
|
||||
|
||||
type NodesAction = {
|
||||
type: 'SET_NODES_IMAGE';
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
type CanvasInitialImageAction = {
|
||||
type: 'SET_CANVAS_INITIAL_IMAGE';
|
||||
};
|
||||
|
||||
type CanvasMergedAction = {
|
||||
type: 'TOAST_CANVAS_MERGED';
|
||||
};
|
||||
|
||||
type CanvasSavedToGalleryAction = {
|
||||
type: 'TOAST_CANVAS_SAVED_TO_GALLERY';
|
||||
};
|
||||
|
||||
type UploadedToastAction = {
|
||||
type: 'TOAST_UPLOADED';
|
||||
};
|
||||
|
||||
export type PostUploadAction =
|
||||
| ControlNetAction
|
||||
| InitialImageAction
|
||||
| NodesAction
|
||||
| CanvasInitialImageAction
|
||||
| CanvasMergedAction
|
||||
| CanvasSavedToGalleryAction
|
||||
| UploadedToastAction;
|
||||
|
||||
type ImageUploadedArg = Parameters<(typeof ImagesService)['uploadImage']>[0] & {
|
||||
postUploadAction?: PostUploadAction;
|
||||
};
|
||||
|
||||
/**
|
||||
* `ImagesService.uploadImage()` thunk
|
||||
@ -40,8 +82,9 @@ type ImageUploadedArg = Parameters<(typeof ImagesService)['uploadImage']>[0];
|
||||
export const imageUploaded = createAppAsyncThunk(
|
||||
'api/imageUploaded',
|
||||
async (arg: ImageUploadedArg) => {
|
||||
// strip out `activeTabName` from arg - the route does not need it
|
||||
const response = await ImagesService.uploadImage(arg);
|
||||
// `postUploadAction` is only used by the listener middleware - destructure it out
|
||||
const { postUploadAction, ...rest } = arg;
|
||||
const response = await ImagesService.uploadImage(rest);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
@ -2,6 +2,7 @@ import { createAppAsyncThunk } from 'app/store/storeUtils';
|
||||
import { GraphExecutionState, SessionsService } from 'services/api';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { isObject } from 'lodash-es';
|
||||
import { isAnyOf } from '@reduxjs/toolkit';
|
||||
|
||||
const sessionLog = log.child({ namespace: 'session' });
|
||||
|
||||
@ -115,3 +116,8 @@ export const listedSessions = createAppAsyncThunk(
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const isAnySessionRejected = isAnyOf(
|
||||
sessionCreated.rejected,
|
||||
sessionInvoked.rejected
|
||||
);
|
||||
|
@ -35,6 +35,6 @@ export const getInputOutlineStyles = (_props?: StyleFunctionProps) => ({
|
||||
},
|
||||
},
|
||||
_placeholder: {
|
||||
color: 'base.400',
|
||||
color: 'base.500',
|
||||
},
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user