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