diff --git a/docs/installation/020_INSTALL_MANUAL.md b/docs/installation/020_INSTALL_MANUAL.md index 865253bb14..54f8b54565 100644 --- a/docs/installation/020_INSTALL_MANUAL.md +++ b/docs/installation/020_INSTALL_MANUAL.md @@ -296,8 +296,18 @@ code for InvokeAI. For this to work, you will need to install the on your system, please see the [Git Installation Guide](https://github.com/git-guides/install-git) +You will also need to install the [frontend development toolchain](https://github.com/invoke-ai/InvokeAI/blob/main/docs/contributing/contribution_guides/contributingToFrontend.md). + +If you have a "normal" installation, you should create a totally separate virtual environment for the git-based installation, else the two may interfere. + +> **Why do I need the frontend toolchain**? +> +> The InvokeAI project uses trunk-based development. That means our `main` branch is the development branch, and releases are tags on that branch. Because development is very active, we don't keep an updated build of the UI in `main` - we only build it for production releases. +> +> That means that between releases, to have a functioning application when running directly from the repo, you will need to run the UI in dev mode or build it regularly (any time the UI code changes). + 1. Create a fork of the InvokeAI repository through the GitHub UI or [this link](https://github.com/invoke-ai/InvokeAI/fork) -1. From the command line, run this command: +2. From the command line, run this command: ```bash git clone https://github.com//InvokeAI.git ``` @@ -305,10 +315,10 @@ Guide](https://github.com/git-guides/install-git) This will create a directory named `InvokeAI` and populate it with the full source code from your fork of the InvokeAI repository. -2. Activate the InvokeAI virtual environment as per step (4) of the manual +3. Activate the InvokeAI virtual environment as per step (4) of the manual installation protocol (important!) -3. Enter the InvokeAI repository directory and run one of these +4. Enter the InvokeAI repository directory and run one of these commands, based on your GPU: === "CUDA (NVidia)" @@ -334,11 +344,15 @@ installation protocol (important!) Be sure to pass `-e` (for an editable install) and don't forget the dot ("."). It is part of the command. - You can now run `invokeai` and its related commands. The code will be +5. Install the [frontend toolchain](https://github.com/invoke-ai/InvokeAI/blob/main/docs/contributing/contribution_guides/contributingToFrontend.md) and do a production build of the UI as described. + +6. You can now run `invokeai` and its related commands. The code will be read from the repository, so that you can edit the .py source files and watch the code's behavior change. -4. If you wish to contribute to the InvokeAI project, you are + When you pull in new changes to the repo, be sure to re-build the UI. + +7. If you wish to contribute to the InvokeAI project, you are encouraged to establish a GitHub account and "fork" https://github.com/invoke-ai/InvokeAI into your own copy of the repository. You can then use GitHub functions to create and submit diff --git a/docs/nodes/communityNodes.md b/docs/nodes/communityNodes.md index 2b30b9f0af..151b9ea262 100644 --- a/docs/nodes/communityNodes.md +++ b/docs/nodes/communityNodes.md @@ -121,18 +121,6 @@ To be imported, an .obj must use triangulated meshes, so make sure to enable tha **Example Usage:** ![depth from obj usage graph](https://raw.githubusercontent.com/dwringer/depth-from-obj-node/main/depth_from_obj_usage.jpg) --------------------------------- -### Enhance Image (simple adjustments) - -**Description:** Boost or reduce color saturation, contrast, brightness, sharpness, or invert colors of any image at any stage with this simple wrapper for pillow [PIL]'s ImageEnhance module. - -Color inversion is toggled with a simple switch, while each of the four enhancer modes are activated by entering a value other than 1 in each corresponding input field. Values less than 1 will reduce the corresponding property, while values greater than 1 will enhance it. - -**Node Link:** https://github.com/dwringer/image-enhance-node - -**Example Usage:** -![enhance image usage graph](https://raw.githubusercontent.com/dwringer/image-enhance-node/main/image_enhance_usage.jpg) - -------------------------------- ### Generative Grammar-Based Prompt Nodes @@ -153,16 +141,26 @@ This includes 3 Nodes: **Description:** This is a pack of nodes for composing masks and images, including a simple text mask creator and both image and latent offset nodes. The offsets wrap around, so these can be used in conjunction with the Seamless node to progressively generate centered on different parts of the seamless tiling. -This includes 4 Nodes: -- *Text Mask (simple 2D)* - create and position a white on black (or black on white) line of text using any font locally available to Invoke. +This includes 14 Nodes: +- *Adjust Image Hue Plus* - Rotate the hue of an image in one of several different color spaces. +- *Blend Latents/Noise (Masked)* - Use a mask to blend part of one latents tensor [including Noise outputs] into another. Can be used to "renoise" sections during a multi-stage [masked] denoising process. +- *Enhance Image* - Boost or reduce color saturation, contrast, brightness, sharpness, or invert colors of any image at any stage with this simple wrapper for pillow [PIL]'s ImageEnhance module. +- *Equivalent Achromatic Lightness* - Calculates image lightness accounting for Helmholtz-Kohlrausch effect based on a method described by High, Green, and Nussbaum (2023). +- *Text to Mask (Clipseg)* - Input a prompt and an image to generate a mask representing areas of the image matched by the prompt. +- *Text to Mask Advanced (Clipseg)* - Output up to four prompt masks combined with logical "and", logical "or", or as separate channels of an RGBA image. +- *Image Layer Blend* - Perform a layered blend of two images using alpha compositing. Opacity of top layer is selectable, with optional mask and several different blend modes/color spaces. - *Image Compositor* - Take a subject from an image with a flat backdrop and layer it on another image using a chroma key or flood select background removal. +- *Image Dilate or Erode* - Dilate or expand a mask (or any image!). This is equivalent to an expand/contract operation. +- *Image Value Thresholds* - Clip an image to pure black/white beyond specified thresholds. - *Offset Latents* - Offset a latents tensor in the vertical and/or horizontal dimensions, wrapping it around. - *Offset Image* - Offset an image in the vertical and/or horizontal dimensions, wrapping it around. +- *Shadows/Highlights/Midtones* - Extract three masks (with adjustable hard or soft thresholds) representing shadows, midtones, and highlights regions of an image. +- *Text Mask (simple 2D)* - create and position a white on black (or black on white) line of text using any font locally available to Invoke. **Node Link:** https://github.com/dwringer/composition-nodes -**Example Usage:** -![composition nodes usage graph](https://raw.githubusercontent.com/dwringer/composition-nodes/main/composition_nodes_usage.jpg) +**Nodes and Output Examples:** +![composition nodes usage graph](https://raw.githubusercontent.com/dwringer/composition-nodes/main/composition_pack_overview.jpg) -------------------------------- ### Size Stepper Nodes diff --git a/installer/lib/installer.py b/installer/lib/installer.py index aaf5779801..70ed4d4331 100644 --- a/installer/lib/installer.py +++ b/installer/lib/installer.py @@ -332,6 +332,7 @@ class InvokeAiInstance: Configure the InvokeAI runtime directory """ + auto_install = False # set sys.argv to a consistent state new_argv = [sys.argv[0]] for i in range(1, len(sys.argv)): @@ -340,13 +341,17 @@ class InvokeAiInstance: new_argv.append(el) new_argv.append(sys.argv[i + 1]) elif el in ["-y", "--yes", "--yes-to-all"]: - new_argv.append(el) + auto_install = True sys.argv = new_argv + import messages import requests # to catch download exceptions - from messages import introduction - introduction() + auto_install = auto_install or messages.user_wants_auto_configuration() + if auto_install: + sys.argv.append("--yes") + else: + messages.introduction() from invokeai.frontend.install.invokeai_configure import invokeai_configure diff --git a/installer/lib/messages.py b/installer/lib/messages.py index c5a39dc91c..e4c03bbfd2 100644 --- a/installer/lib/messages.py +++ b/installer/lib/messages.py @@ -7,7 +7,7 @@ import os import platform from pathlib import Path -from prompt_toolkit import prompt +from prompt_toolkit import HTML, prompt from prompt_toolkit.completion import PathCompleter from prompt_toolkit.validation import Validator from rich import box, print @@ -65,17 +65,50 @@ def confirm_install(dest: Path) -> bool: if dest.exists(): print(f":exclamation: Directory {dest} already exists :exclamation:") dest_confirmed = Confirm.ask( - ":stop_sign: Are you sure you want to (re)install in this location?", + ":stop_sign: (re)install in this location?", default=False, ) else: print(f"InvokeAI will be installed in {dest}") - dest_confirmed = not Confirm.ask("Would you like to pick a different location?", default=False) + dest_confirmed = Confirm.ask("Use this location?", default=True) console.line() return dest_confirmed +def user_wants_auto_configuration() -> bool: + """Prompt the user to choose between manual and auto configuration.""" + console.rule("InvokeAI Configuration Section") + console.print( + Panel( + Group( + "\n".join( + [ + "Libraries are installed and InvokeAI will now set up its root directory and configuration. Choose between:", + "", + " * AUTOMATIC configuration: install reasonable defaults and a minimal set of starter models.", + " * MANUAL configuration: manually inspect and adjust configuration options and pick from a larger set of starter models.", + "", + "Later you can fine tune your configuration by selecting option [6] 'Change InvokeAI startup options' from the invoke.bat/invoke.sh launcher script.", + ] + ), + ), + box=box.MINIMAL, + padding=(1, 1), + ) + ) + choice = ( + prompt( + HTML("Choose <a>utomatic or <m>anual configuration [a/m] (a): "), + validator=Validator.from_callable( + lambda n: n == "" or n.startswith(("a", "A", "m", "M")), error_message="Please select 'a' or 'm'" + ), + ) + or "a" + ) + return choice.lower().startswith("a") + + def dest_path(dest=None) -> Path: """ Prompt the user for the destination path and create the path diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index c2a32010c5..9db35fb5c3 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -49,7 +49,7 @@ def check_internet() -> bool: return False -logger = InvokeAILogger.getLogger() +logger = InvokeAILogger.get_logger() class ApiDependencies: diff --git a/invokeai/app/api/routers/app_info.py b/invokeai/app/api/routers/app_info.py index a98c8edc6a..39d570ec99 100644 --- a/invokeai/app/api/routers/app_info.py +++ b/invokeai/app/api/routers/app_info.py @@ -7,6 +7,7 @@ from fastapi.routing import APIRouter from pydantic import BaseModel, Field from invokeai.app.invocations.upscale import ESRGAN_MODELS +from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark from invokeai.backend.image_util.patchmatch import PatchMatch from invokeai.backend.image_util.safety_checker import SafetyChecker @@ -113,3 +114,33 @@ async def set_log_level( async def clear_invocation_cache() -> None: """Clears the invocation cache""" ApiDependencies.invoker.services.invocation_cache.clear() + + +@app_router.put( + "/invocation_cache/enable", + operation_id="enable_invocation_cache", + responses={200: {"description": "The operation was successful"}}, +) +async def enable_invocation_cache() -> None: + """Clears the invocation cache""" + ApiDependencies.invoker.services.invocation_cache.enable() + + +@app_router.put( + "/invocation_cache/disable", + operation_id="disable_invocation_cache", + responses={200: {"description": "The operation was successful"}}, +) +async def disable_invocation_cache() -> None: + """Clears the invocation cache""" + ApiDependencies.invoker.services.invocation_cache.disable() + + +@app_router.get( + "/invocation_cache/status", + operation_id="get_invocation_cache_status", + responses={200: {"model": InvocationCacheStatus}}, +) +async def get_invocation_cache_status() -> InvocationCacheStatus: + """Clears the invocation cache""" + return ApiDependencies.invoker.services.invocation_cache.get_status() diff --git a/invokeai/app/api/routers/models.py b/invokeai/app/api/routers/models.py index 276eda902d..ebc40f5ce5 100644 --- a/invokeai/app/api/routers/models.py +++ b/invokeai/app/api/routers/models.py @@ -146,7 +146,8 @@ async def update_model( async def import_model( location: str = Body(description="A model path, repo_id or URL to import"), prediction_type: Optional[Literal["v_prediction", "epsilon", "sample"]] = Body( - description="Prediction type for SDv2 checkpoint files", default="v_prediction" + description="Prediction type for SDv2 checkpoints and rare SDv1 checkpoints", + default=None, ), ) -> ImportModelResponse: """Add a model using its local path, repo_id, or remote URL. Model characteristics will be probed and configured automatically""" diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index c93197d1bf..fdbd64b30d 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -8,7 +8,6 @@ app_config.parse_args() if True: # hack to make flake8 happy with imports coming after setting up the config import asyncio - import logging import mimetypes import socket from inspect import signature @@ -41,7 +40,9 @@ if True: # hack to make flake8 happy with imports coming after setting up the c import invokeai.backend.util.mps_fixes # noqa: F401 (monkeypatching on import) -logger = InvokeAILogger.getLogger(config=app_config) +app_config = InvokeAIAppConfig.get_config() +app_config.parse_args() +logger = InvokeAILogger.get_logger(config=app_config) # fix for windows mimetypes registry entries being borked # see https://github.com/invoke-ai/InvokeAI/discussions/3684#discussioncomment-6391352 @@ -223,7 +224,7 @@ def invoke_api(): exc_info=e, ) else: - jurigged.watch(logger=InvokeAILogger.getLogger(name="jurigged").info) + jurigged.watch(logger=InvokeAILogger.get_logger(name="jurigged").info) port = find_port(app_config.port) if port != app_config.port: @@ -242,7 +243,7 @@ def invoke_api(): # replace uvicorn's loggers with InvokeAI's for consistent appearance for logname in ["uvicorn.access", "uvicorn"]: - log = logging.getLogger(logname) + log = InvokeAILogger.get_logger(logname) log.handlers.clear() for ch in logger.handlers: log.addHandler(ch) diff --git a/invokeai/app/cli_app.py b/invokeai/app/cli_app.py index dc59954e9b..2f8a4d2cbd 100644 --- a/invokeai/app/cli_app.py +++ b/invokeai/app/cli_app.py @@ -7,8 +7,6 @@ from .services.config import InvokeAIAppConfig # parse_args() must be called before any other imports. if it is not called first, consumers of the config # which are imported/used before parse_args() is called will get the default config values instead of the # values from the command line or config file. -config = InvokeAIAppConfig.get_config() -config.parse_args() if True: # hack to make flake8 happy with imports coming after setting up the config import argparse @@ -61,8 +59,9 @@ if True: # hack to make flake8 happy with imports coming after setting up the c if torch.backends.mps.is_available(): import invokeai.backend.util.mps_fixes # noqa: F401 (monkeypatching on import) - -logger = InvokeAILogger().getLogger(config=config) +config = InvokeAIAppConfig.get_config() +config.parse_args() +logger = InvokeAILogger().get_logger(config=config) class CliCommand(BaseModel): diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index 97bd29ff17..3285de3d5a 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -88,6 +88,12 @@ class FieldDescriptions: num_1 = "The first number" num_2 = "The second number" mask = "The mask to use for the operation" + board = "The board to save the image to" + image = "The image to process" + tile_size = "Tile size" + inclusive_low = "The inclusive low value" + exclusive_high = "The exclusive high value" + decimal_places = "The number of decimal places to round to" class Input(str, Enum): @@ -173,6 +179,7 @@ class UIType(str, Enum): WorkflowField = "WorkflowField" IsIntermediate = "IsIntermediate" MetadataField = "MetadataField" + BoardField = "BoardField" # endregion @@ -656,6 +663,8 @@ def invocation( :param Optional[str] title: Adds a title to the invocation. Use if the auto-generated title isn't quite right. Defaults to None. :param Optional[list[str]] tags: Adds tags to the invocation. Invocations may be searched for by their tags. Defaults to None. :param Optional[str] category: Adds a category to the invocation. Used to group the invocations in the UI. Defaults to None. + :param Optional[str] version: Adds a version to the invocation. Must be a valid semver string. Defaults to None. + :param Optional[bool] use_cache: Whether or not to use the invocation cache. Defaults to True. The user may override this in the workflow editor. """ def wrapper(cls: Type[GenericBaseInvocation]) -> Type[GenericBaseInvocation]: diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index 5696f43f8d..933c32c908 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -559,3 +559,33 @@ class SamDetectorReproducibleColors(SamDetector): img[:, :] = ann_color final_img.paste(Image.fromarray(img, mode="RGB"), (0, 0), Image.fromarray(np.uint8(m * 255))) return np.array(final_img, dtype=np.uint8) + + +@invocation( + "color_map_image_processor", + title="Color Map Processor", + tags=["controlnet"], + category="controlnet", + version="1.0.0", +) +class ColorMapImageProcessorInvocation(ImageProcessorInvocation): + """Generates a color map from the provided image""" + + color_map_tile_size: int = InputField(default=64, ge=0, description=FieldDescriptions.tile_size) + + def run_processor(self, image: Image.Image): + image = image.convert("RGB") + image = np.array(image, dtype=np.uint8) + height, width = image.shape[:2] + + width_tile_size = min(self.color_map_tile_size, width) + height_tile_size = min(self.color_map_tile_size, height) + + color_map = cv2.resize( + image, + (width // width_tile_size, height // height_tile_size), + interpolation=cv2.INTER_CUBIC, + ) + color_map = cv2.resize(color_map, (width, height), interpolation=cv2.INTER_NEAREST) + color_map = Image.fromarray(color_map) + return color_map diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 0403fa71e3..0301768219 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -8,12 +8,12 @@ import numpy from PIL import Image, ImageChops, ImageFilter, ImageOps from invokeai.app.invocations.metadata import CoreMetadata -from invokeai.app.invocations.primitives import ColorField, ImageField, ImageOutput +from invokeai.app.invocations.primitives import BoardField, ColorField, ImageField, ImageOutput from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark from invokeai.backend.image_util.safety_checker import SafetyChecker from ..models.image import ImageCategory, ResourceOrigin -from .baseinvocation import BaseInvocation, FieldDescriptions, InputField, InvocationContext, invocation +from .baseinvocation import BaseInvocation, FieldDescriptions, Input, InputField, InvocationContext, invocation @invocation("show_image", title="Show Image", tags=["image"], category="image", version="1.0.0") @@ -972,13 +972,14 @@ class ImageChannelMultiplyInvocation(BaseInvocation): title="Save Image", tags=["primitives", "image"], category="primitives", - version="1.0.0", + version="1.0.1", use_cache=False, ) class SaveImageInvocation(BaseInvocation): """Saves an image. Unlike an image primitive, this invocation stores a copy of the image.""" - image: ImageField = InputField(description="The image to load") + image: ImageField = InputField(description=FieldDescriptions.image) + board: Optional[BoardField] = InputField(default=None, description=FieldDescriptions.board, input=Input.Direct) metadata: CoreMetadata = InputField( default=None, description=FieldDescriptions.core_metadata, @@ -992,6 +993,7 @@ class SaveImageInvocation(BaseInvocation): image=image, image_origin=ResourceOrigin.INTERNAL, image_category=ImageCategory.GENERAL, + board_id=self.board.board_id if self.board else None, node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, diff --git a/invokeai/app/invocations/ip_adapter.py b/invokeai/app/invocations/ip_adapter.py index 0c5b858112..3e3a3d9b1f 100644 --- a/invokeai/app/invocations/ip_adapter.py +++ b/invokeai/app/invocations/ip_adapter.py @@ -58,9 +58,7 @@ class IPAdapterInvocation(BaseInvocation): # Inputs image: ImageField = InputField(description="The IP-Adapter image prompt.") ip_adapter_model: IPAdapterModelField = InputField( - description="The IP-Adapter model.", - title="IP-Adapter Model", - input=Input.Direct, + description="The IP-Adapter model.", title="IP-Adapter Model", input=Input.Direct, ui_order=-1 ) # weight: float = InputField(default=1.0, description="The weight of the IP-Adapter.", ui_type=UIType.Float) diff --git a/invokeai/app/invocations/math.py b/invokeai/app/invocations/math.py index 3cdd43fb59..b52cbb28bf 100644 --- a/invokeai/app/invocations/math.py +++ b/invokeai/app/invocations/math.py @@ -65,13 +65,27 @@ class DivideInvocation(BaseInvocation): class RandomIntInvocation(BaseInvocation): """Outputs a single random integer.""" - low: int = InputField(default=0, description="The inclusive low value") - high: int = InputField(default=np.iinfo(np.int32).max, description="The exclusive high value") + low: int = InputField(default=0, description=FieldDescriptions.inclusive_low) + high: int = InputField(default=np.iinfo(np.int32).max, description=FieldDescriptions.exclusive_high) def invoke(self, context: InvocationContext) -> IntegerOutput: return IntegerOutput(value=np.random.randint(self.low, self.high)) +@invocation("rand_float", title="Random Float", tags=["math", "float", "random"], category="math", version="1.0.0") +class RandomFloatInvocation(BaseInvocation): + """Outputs a single random float""" + + low: float = InputField(default=0.0, description=FieldDescriptions.inclusive_low) + high: float = InputField(default=1.0, description=FieldDescriptions.exclusive_high) + decimals: int = InputField(default=2, description=FieldDescriptions.decimal_places) + + def invoke(self, context: InvocationContext) -> FloatOutput: + random_float = np.random.uniform(self.low, self.high) + rounded_float = round(random_float, self.decimals) + return FloatOutput(value=rounded_float) + + @invocation( "float_to_int", title="Float To Integer", diff --git a/invokeai/app/invocations/metadata.py b/invokeai/app/invocations/metadata.py index 39fa3beba0..a84befcb2e 100644 --- a/invokeai/app/invocations/metadata.py +++ b/invokeai/app/invocations/metadata.py @@ -42,7 +42,8 @@ class CoreMetadata(BaseModelExcludeNull): cfg_scale: float = Field(description="The classifier-free guidance scale parameter") steps: int = Field(description="The number of steps used for inference") scheduler: str = Field(description="The scheduler used for inference") - clip_skip: int = Field( + clip_skip: Optional[int] = Field( + default=None, description="The number of skipped CLIP layers", ) model: MainModelField = Field(description="The main model used for inference") @@ -116,7 +117,8 @@ class MetadataAccumulatorInvocation(BaseInvocation): cfg_scale: float = InputField(description="The classifier-free guidance scale parameter") steps: int = InputField(description="The number of steps used for inference") scheduler: str = InputField(description="The scheduler used for inference") - clip_skip: int = InputField( + clip_skip: Optional[int] = Field( + default=None, description="The number of skipped CLIP layers", ) model: MainModelField = InputField(description="The main model used for inference") diff --git a/invokeai/app/invocations/primitives.py b/invokeai/app/invocations/primitives.py index 93cf29f7d6..c314edfd15 100644 --- a/invokeai/app/invocations/primitives.py +++ b/invokeai/app/invocations/primitives.py @@ -226,6 +226,12 @@ class ImageField(BaseModel): image_name: str = Field(description="The name of the image") +class BoardField(BaseModel): + """A board primitive field""" + + board_id: str = Field(description="The id of the board") + + @invocation_output("image_output") class ImageOutput(BaseInvocationOutput): """Base class for nodes that output a single image""" diff --git a/invokeai/app/services/config/invokeai_config.py b/invokeai/app/services/config/invokeai_config.py index 65bf9b9eba..8ea703f39a 100644 --- a/invokeai/app/services/config/invokeai_config.py +++ b/invokeai/app/services/config/invokeai_config.py @@ -241,7 +241,7 @@ class InvokeAIAppConfig(InvokeAISettings): version : bool = Field(default=False, description="Show InvokeAI version and exit", category="Other") # CACHE - ram : Union[float, Literal["auto"]] = Field(default=6.0, gt=0, description="Maximum memory amount used by model cache for rapid switching (floating point number or 'auto')", category="Model Cache", ) + ram : Union[float, Literal["auto"]] = Field(default=7.5, gt=0, description="Maximum memory amount used by model cache for rapid switching (floating point number or 'auto')", category="Model Cache", ) vram : Union[float, Literal["auto"]] = Field(default=0.25, ge=0, description="Amount of VRAM reserved for model storage (floating point number or 'auto')", category="Model Cache", ) lazy_offload : bool = Field(default=True, description="Keep models in VRAM until their space is needed", category="Model Cache", ) @@ -277,6 +277,7 @@ class InvokeAIAppConfig(InvokeAISettings): class Config: validate_assignment = True + env_prefix = "INVOKEAI" def parse_args(self, argv: Optional[list[str]] = None, conf: Optional[DictConfig] = None, clobber=False): """ diff --git a/invokeai/app/services/graph.py b/invokeai/app/services/graph.py index 2a5fc4c441..9dccd14026 100644 --- a/invokeai/app/services/graph.py +++ b/invokeai/app/services/graph.py @@ -117,6 +117,10 @@ def are_connection_types_compatible(from_type: Any, to_type: Any) -> bool: if from_type is int and to_type is float: return True + # allow int|float -> str, pydantic will cast for us + if (from_type is int or from_type is float) and to_type is str: + return True + # if not issubclass(from_type, to_type): if not is_union_subtype(from_type, to_type): return False diff --git a/invokeai/app/services/invocation_cache/invocation_cache_base.py b/invokeai/app/services/invocation_cache/invocation_cache_base.py index c35a31f851..d913e050ab 100644 --- a/invokeai/app/services/invocation_cache/invocation_cache_base.py +++ b/invokeai/app/services/invocation_cache/invocation_cache_base.py @@ -2,6 +2,7 @@ from abc import ABC, abstractmethod from typing import Optional, Union from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput +from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus class InvocationCacheBase(ABC): @@ -32,7 +33,7 @@ class InvocationCacheBase(ABC): @abstractmethod def delete(self, key: Union[int, str]) -> None: - """Deleteds an invocation output from the cache""" + """Deletes an invocation output from the cache""" pass @abstractmethod @@ -44,3 +45,18 @@ class InvocationCacheBase(ABC): def create_key(self, invocation: BaseInvocation) -> int: """Gets the key for the invocation's cache item""" pass + + @abstractmethod + def disable(self) -> None: + """Disables the cache, overriding the max cache size""" + pass + + @abstractmethod + def enable(self) -> None: + """Enables the cache, letting the the max cache size take effect""" + pass + + @abstractmethod + def get_status(self) -> InvocationCacheStatus: + """Returns the status of the cache""" + pass diff --git a/invokeai/app/services/invocation_cache/invocation_cache_common.py b/invokeai/app/services/invocation_cache/invocation_cache_common.py new file mode 100644 index 0000000000..6ce2d02f3b --- /dev/null +++ b/invokeai/app/services/invocation_cache/invocation_cache_common.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel, Field + + +class InvocationCacheStatus(BaseModel): + size: int = Field(description="The current size of the invocation cache") + hits: int = Field(description="The number of cache hits") + misses: int = Field(description="The number of cache misses") + enabled: bool = Field(description="Whether the invocation cache is enabled") + max_size: int = Field(description="The maximum size of the invocation cache") diff --git a/invokeai/app/services/invocation_cache/invocation_cache_memory.py b/invokeai/app/services/invocation_cache/invocation_cache_memory.py index 4c0eb2106f..817dbb958e 100644 --- a/invokeai/app/services/invocation_cache/invocation_cache_memory.py +++ b/invokeai/app/services/invocation_cache/invocation_cache_memory.py @@ -1,81 +1,126 @@ -from queue import Queue +from collections import OrderedDict +from dataclasses import dataclass, field +from threading import Lock from typing import Optional, Union from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput from invokeai.app.services.invocation_cache.invocation_cache_base import InvocationCacheBase +from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus from invokeai.app.services.invoker import Invoker +@dataclass(order=True) +class CachedItem: + invocation_output: BaseInvocationOutput = field(compare=False) + invocation_output_json: str = field(compare=False) + + class MemoryInvocationCache(InvocationCacheBase): - __cache: dict[Union[int, str], tuple[BaseInvocationOutput, str]] - __max_cache_size: int - __cache_ids: Queue - __invoker: Invoker + _cache: OrderedDict[Union[int, str], CachedItem] + _max_cache_size: int + _disabled: bool + _hits: int + _misses: int + _invoker: Invoker + _lock: Lock def __init__(self, max_cache_size: int = 0) -> None: - self.__cache = dict() - self.__max_cache_size = max_cache_size - self.__cache_ids = Queue() + self._cache = OrderedDict() + self._max_cache_size = max_cache_size + self._disabled = False + self._hits = 0 + self._misses = 0 + self._lock = Lock() def start(self, invoker: Invoker) -> None: - self.__invoker = invoker - if self.__max_cache_size == 0: + self._invoker = invoker + if self._max_cache_size == 0: return - self.__invoker.services.images.on_deleted(self._delete_by_match) - self.__invoker.services.latents.on_deleted(self._delete_by_match) + self._invoker.services.images.on_deleted(self._delete_by_match) + self._invoker.services.latents.on_deleted(self._delete_by_match) def get(self, key: Union[int, str]) -> Optional[BaseInvocationOutput]: - if self.__max_cache_size == 0: - return - - item = self.__cache.get(key, None) - if item is not None: - return item[0] + with self._lock: + if self._max_cache_size == 0 or self._disabled: + return None + item = self._cache.get(key, None) + if item is not None: + self._hits += 1 + self._cache.move_to_end(key) + return item.invocation_output + self._misses += 1 + return None def save(self, key: Union[int, str], invocation_output: BaseInvocationOutput) -> None: - if self.__max_cache_size == 0: - return + with self._lock: + if self._max_cache_size == 0 or self._disabled or key in self._cache: + return + # If the cache is full, we need to remove the least used + number_to_delete = len(self._cache) + 1 - self._max_cache_size + self._delete_oldest_access(number_to_delete) + self._cache[key] = CachedItem(invocation_output, invocation_output.json()) - if key not in self.__cache: - self.__cache[key] = (invocation_output, invocation_output.json()) - self.__cache_ids.put(key) - if self.__cache_ids.qsize() > self.__max_cache_size: - try: - self.__cache.pop(self.__cache_ids.get()) - except KeyError: - # this means the cache_ids are somehow out of sync w/ the cache - pass + def _delete_oldest_access(self, number_to_delete: int) -> None: + number_to_delete = min(number_to_delete, len(self._cache)) + for _ in range(number_to_delete): + self._cache.popitem(last=False) + + def _delete(self, key: Union[int, str]) -> None: + if self._max_cache_size == 0: + return + if key in self._cache: + del self._cache[key] def delete(self, key: Union[int, str]) -> None: - if self.__max_cache_size == 0: - return - - if key in self.__cache: - del self.__cache[key] + with self._lock: + return self._delete(key) def clear(self, *args, **kwargs) -> None: - if self.__max_cache_size == 0: - return + with self._lock: + if self._max_cache_size == 0: + return + self._cache.clear() + self._misses = 0 + self._hits = 0 - self.__cache.clear() - self.__cache_ids = Queue() - - def create_key(self, invocation: BaseInvocation) -> int: + @staticmethod + def create_key(invocation: BaseInvocation) -> int: return hash(invocation.json(exclude={"id"})) + def disable(self) -> None: + with self._lock: + if self._max_cache_size == 0: + return + self._disabled = True + + def enable(self) -> None: + with self._lock: + if self._max_cache_size == 0: + return + self._disabled = False + + def get_status(self) -> InvocationCacheStatus: + with self._lock: + return InvocationCacheStatus( + hits=self._hits, + misses=self._misses, + enabled=not self._disabled and self._max_cache_size > 0, + size=len(self._cache), + max_size=self._max_cache_size, + ) + def _delete_by_match(self, to_match: str) -> None: - if self.__max_cache_size == 0: - return - - keys_to_delete = set() - for key, value_tuple in self.__cache.items(): - if to_match in value_tuple[1]: - keys_to_delete.add(key) - - if not keys_to_delete: - return - - for key in keys_to_delete: - self.delete(key) - - self.__invoker.services.logger.debug(f"Deleted {len(keys_to_delete)} cached invocation outputs for {to_match}") + with self._lock: + if self._max_cache_size == 0: + return + keys_to_delete = set() + for key, cached_item in self._cache.items(): + if to_match in cached_item.invocation_output_json: + keys_to_delete.add(key) + if not keys_to_delete: + return + for key in keys_to_delete: + self._delete(key) + self._invoker.services.logger.debug( + f"Deleted {len(keys_to_delete)} cached invocation outputs for {to_match}" + ) diff --git a/invokeai/app/services/session_processor/session_processor_default.py b/invokeai/app/services/session_processor/session_processor_default.py index b682c7e56c..d5d08cc779 100644 --- a/invokeai/app/services/session_processor/session_processor_default.py +++ b/invokeai/app/services/session_processor/session_processor_default.py @@ -47,20 +47,27 @@ class DefaultSessionProcessor(SessionProcessorBase): async def _on_queue_event(self, event: FastAPIEvent) -> None: event_name = event[1]["event"] - match event_name: - case "graph_execution_state_complete" | "invocation_error" | "session_retrieval_error" | "invocation_retrieval_error": - self.__queue_item = None - self._poll_now() - case "session_canceled" if self.__queue_item is not None and self.__queue_item.session_id == event[1][ - "data" - ]["graph_execution_state_id"]: - self.__queue_item = None - self._poll_now() - case "batch_enqueued": - self._poll_now() - case "queue_cleared": - self.__queue_item = None - self._poll_now() + # This was a match statement, but match is not supported on python 3.9 + if event_name in [ + "graph_execution_state_complete", + "invocation_error", + "session_retrieval_error", + "invocation_retrieval_error", + ]: + self.__queue_item = None + self._poll_now() + elif ( + event_name == "session_canceled" + and self.__queue_item is not None + and self.__queue_item.session_id == event[1]["data"]["graph_execution_state_id"] + ): + self.__queue_item = None + self._poll_now() + elif event_name == "batch_enqueued": + self._poll_now() + elif event_name == "queue_cleared": + self.__queue_item = None + self._poll_now() def resume(self) -> SessionProcessorStatus: if not self.__resume_event.is_set(): @@ -92,30 +99,34 @@ class DefaultSessionProcessor(SessionProcessorBase): self.__invoker.services.logger while not stop_event.is_set(): poll_now_event.clear() + try: + # do not dequeue if there is already a session running + if self.__queue_item is None and resume_event.is_set(): + queue_item = self.__invoker.services.session_queue.dequeue() - # do not dequeue if there is already a session running - if self.__queue_item is None and resume_event.is_set(): - queue_item = self.__invoker.services.session_queue.dequeue() + if queue_item is not None: + self.__invoker.services.logger.debug(f"Executing queue item {queue_item.item_id}") + self.__queue_item = queue_item + self.__invoker.services.graph_execution_manager.set(queue_item.session) + self.__invoker.invoke( + session_queue_batch_id=queue_item.batch_id, + session_queue_id=queue_item.queue_id, + session_queue_item_id=queue_item.item_id, + graph_execution_state=queue_item.session, + invoke_all=True, + ) + queue_item = None - if queue_item is not None: - self.__invoker.services.logger.debug(f"Executing queue item {queue_item.item_id}") - self.__queue_item = queue_item - self.__invoker.services.graph_execution_manager.set(queue_item.session) - self.__invoker.invoke( - session_queue_batch_id=queue_item.batch_id, - session_queue_id=queue_item.queue_id, - session_queue_item_id=queue_item.item_id, - graph_execution_state=queue_item.session, - invoke_all=True, - ) - queue_item = None - - if queue_item is None: - self.__invoker.services.logger.debug("Waiting for next polling interval or event") + if queue_item is None: + self.__invoker.services.logger.debug("Waiting for next polling interval or event") + poll_now_event.wait(POLLING_INTERVAL) + continue + except Exception as e: + self.__invoker.services.logger.error(f"Error in session processor: {e}") poll_now_event.wait(POLLING_INTERVAL) continue except Exception as e: - self.__invoker.services.logger.error(f"Error in session processor: {e}") + self.__invoker.services.logger.error(f"Fatal Error in session processor: {e}") pass finally: stop_event.clear() diff --git a/invokeai/app/services/session_queue/session_queue_common.py b/invokeai/app/services/session_queue/session_queue_common.py index a7032c7f1f..905e568fa1 100644 --- a/invokeai/app/services/session_queue/session_queue_common.py +++ b/invokeai/app/services/session_queue/session_queue_common.py @@ -162,15 +162,15 @@ class SessionQueueItemWithoutGraph(BaseModel): session_id: str = Field( description="The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed." ) - field_values: Optional[list[NodeFieldValue]] = Field( - default=None, description="The field values that were used for this queue item" - ) - queue_id: str = Field(description="The id of the queue with which this item is associated") error: Optional[str] = Field(default=None, description="The error message if this queue item errored") created_at: Union[datetime.datetime, str] = Field(description="When this queue item was created") updated_at: Union[datetime.datetime, str] = Field(description="When this queue item was updated") started_at: Optional[Union[datetime.datetime, str]] = Field(description="When this queue item was started") completed_at: Optional[Union[datetime.datetime, str]] = Field(description="When this queue item was completed") + queue_id: str = Field(description="The id of the queue with which this item is associated") + field_values: Optional[list[NodeFieldValue]] = Field( + default=None, description="The field values that were used for this queue item" + ) @classmethod def from_dict(cls, queue_item_dict: dict) -> "SessionQueueItemDTO": diff --git a/invokeai/app/services/session_queue/session_queue_sqlite.py b/invokeai/app/services/session_queue/session_queue_sqlite.py index 1cedf09c89..a13e13050e 100644 --- a/invokeai/app/services/session_queue/session_queue_sqlite.py +++ b/invokeai/app/services/session_queue/session_queue_sqlite.py @@ -59,13 +59,14 @@ class SqliteSessionQueue(SessionQueueBase): async def _on_session_event(self, event: FastAPIEvent) -> FastAPIEvent: event_name = event[1]["event"] - match event_name: - case "graph_execution_state_complete": - await self._handle_complete_event(event) - case "invocation_error" | "session_retrieval_error" | "invocation_retrieval_error": - await self._handle_error_event(event) - case "session_canceled": - await self._handle_cancel_event(event) + + # This was a match statement, but match is not supported on python 3.9 + if event_name == "graph_execution_state_complete": + await self._handle_complete_event(event) + elif event_name in ["invocation_error", "session_retrieval_error", "invocation_retrieval_error"]: + await self._handle_error_event(event) + elif event_name == "session_canceled": + await self._handle_cancel_event(event) return event async def _handle_complete_event(self, event: FastAPIEvent) -> None: diff --git a/invokeai/backend/install/invokeai_configure.py b/invokeai/backend/install/invokeai_configure.py index 0b3f50e3fc..5afbdfb5a3 100755 --- a/invokeai/backend/install/invokeai_configure.py +++ b/invokeai/backend/install/invokeai_configure.py @@ -70,7 +70,6 @@ def get_literal_fields(field) -> list[Any]: config = InvokeAIAppConfig.get_config() Model_dir = "models" - Default_config_file = config.model_conf_path SD_Configs = config.legacy_conf_path @@ -93,7 +92,7 @@ INIT_FILE_PREAMBLE = """# InvokeAI initialization file # or renaming it and then running invokeai-configure again. """ -logger = InvokeAILogger.getLogger() +logger = InvokeAILogger.get_logger() class DummyWidgetValue(Enum): @@ -458,7 +457,7 @@ Use cursor arrows to make a checkbox selection, and space to toggle. ) self.add_widget_intelligent( npyscreen.TitleFixedText, - name="Model RAM cache size (GB). Make this at least large enough to hold a single full model.", + name="Model RAM cache size (GB). Make this at least large enough to hold a single full model (2GB for SD-1, 6GB for SDXL).", begin_entry_at=0, editable=False, color="CONTROL", @@ -651,8 +650,19 @@ def edit_opts(program_opts: Namespace, invokeai_opts: Namespace) -> argparse.Nam return editApp.new_opts() +def default_ramcache() -> float: + """Run a heuristic for the default RAM cache based on installed RAM.""" + + # Note that on my 64 GB machine, psutil.virtual_memory().total gives 62 GB, + # So we adjust everthing down a bit. + return ( + 15.0 if MAX_RAM >= 60 else 7.5 if MAX_RAM >= 30 else 4 if MAX_RAM >= 14 else 2.1 + ) # 2.1 is just large enough for sd 1.5 ;-) + + def default_startup_options(init_file: Path) -> Namespace: opts = InvokeAIAppConfig.get_config() + opts.ram = default_ramcache() return opts @@ -894,7 +904,7 @@ def main(): if opt.full_precision: invoke_args.extend(["--precision", "float32"]) config.parse_args(invoke_args) - logger = InvokeAILogger().getLogger(config=config) + logger = InvokeAILogger().get_logger(config=config) errors = set() diff --git a/invokeai/backend/install/model_install_backend.py b/invokeai/backend/install/model_install_backend.py index 667111047f..8ce5dc5322 100644 --- a/invokeai/backend/install/model_install_backend.py +++ b/invokeai/backend/install/model_install_backend.py @@ -30,7 +30,7 @@ warnings.filterwarnings("ignore") # --------------------------globals----------------------- config = InvokeAIAppConfig.get_config() -logger = InvokeAILogger.getLogger(name="InvokeAI") +logger = InvokeAILogger.get_logger(name="InvokeAI") # the initial "configs" dir is now bundled in the `invokeai.configs` package Dataset_path = Path(configs.__path__[0]) / "INITIAL_MODELS.yaml" @@ -47,8 +47,14 @@ Config_preamble = """ LEGACY_CONFIGS = { BaseModelType.StableDiffusion1: { - ModelVariantType.Normal: "v1-inference.yaml", - ModelVariantType.Inpaint: "v1-inpainting-inference.yaml", + ModelVariantType.Normal: { + SchedulerPredictionType.Epsilon: "v1-inference.yaml", + SchedulerPredictionType.VPrediction: "v1-inference-v.yaml", + }, + ModelVariantType.Inpaint: { + SchedulerPredictionType.Epsilon: "v1-inpainting-inference.yaml", + SchedulerPredictionType.VPrediction: "v1-inpainting-inference-v.yaml", + }, }, BaseModelType.StableDiffusion2: { ModelVariantType.Normal: { @@ -69,14 +75,6 @@ LEGACY_CONFIGS = { } -@dataclass -class ModelInstallList: - """Class for listing models to be installed/removed""" - - install_models: List[str] = field(default_factory=list) - remove_models: List[str] = field(default_factory=list) - - @dataclass class InstallSelections: install_models: List[str] = field(default_factory=list) @@ -94,6 +92,7 @@ class ModelLoadInfo: installed: bool = False recommended: bool = False default: bool = False + requires: Optional[List[str]] = field(default_factory=list) class ModelInstall(object): @@ -131,8 +130,6 @@ class ModelInstall(object): # supplement with entries in models.yaml installed_models = [x for x in self.mgr.list_models()] - # suppresses autoloaded models - # installed_models = [x for x in self.mgr.list_models() if not self._is_autoloaded(x)] for md in installed_models: base = md["base_model"] @@ -164,9 +161,12 @@ class ModelInstall(object): def list_models(self, model_type): installed = self.mgr.list_models(model_type=model_type) + print() print(f"Installed models of type `{model_type}`:") + print(f"{'Model Key':50} Model Path") for i in installed: - print(f"{i['model_name']}\t{i['base_model']}\t{i['path']}") + print(f"{'/'.join([i['base_model'],i['model_type'],i['model_name']]):50} {i['path']}") + print() # logic here a little reversed to maintain backward compatibility def starter_models(self, all_models: bool = False) -> Set[str]: @@ -204,6 +204,8 @@ class ModelInstall(object): job += 1 # add requested models + self._remove_installed(selections.install_models) + self._add_required_models(selections.install_models) for path in selections.install_models: logger.info(f"Installing {path} [{job}/{jobs}]") try: @@ -263,6 +265,26 @@ class ModelInstall(object): return models_installed + def _remove_installed(self, model_list: List[str]): + all_models = self.all_models() + for path in model_list: + key = self.reverse_paths.get(path) + if key and all_models[key].installed: + logger.warning(f"{path} already installed. Skipping.") + model_list.remove(path) + + def _add_required_models(self, model_list: List[str]): + additional_models = [] + all_models = self.all_models() + for path in model_list: + if not (key := self.reverse_paths.get(path)): + continue + for requirement in all_models[key].requires: + requirement_key = self.reverse_paths.get(requirement) + if not all_models[requirement_key].installed: + additional_models.append(requirement) + model_list.extend(additional_models) + # install a model from a local path. The optional info parameter is there to prevent # the model from being probed twice in the event that it has already been probed. def _install_path(self, path: Path, info: ModelProbeInfo = None) -> AddModelResult: @@ -286,7 +308,7 @@ class ModelInstall(object): location = download_with_resume(url, Path(staging)) if not location: logger.error(f"Unable to download {url}. Skipping.") - info = ModelProbe().heuristic_probe(location) + info = ModelProbe().heuristic_probe(location, self.prediction_helper) dest = self.config.models_path / info.base_type.value / info.model_type.value / location.name dest.parent.mkdir(parents=True, exist_ok=True) models_path = shutil.move(location, dest) @@ -393,7 +415,7 @@ class ModelInstall(object): possible_conf = path.with_suffix(".yaml") if possible_conf.exists(): legacy_conf = str(self.relative_to_root(possible_conf)) - elif info.base_type == BaseModelType.StableDiffusion2: + elif info.base_type in [BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2]: legacy_conf = Path( self.config.legacy_conf_dir, LEGACY_CONFIGS[info.base_type][info.variant_type][info.prediction_type], @@ -492,7 +514,7 @@ def yes_or_no(prompt: str, default_yes=True): # --------------------------------------------- def hf_download_from_pretrained(model_class: object, model_name: str, destination: Path, **kwargs): - logger = InvokeAILogger.getLogger("InvokeAI") + logger = InvokeAILogger.get_logger("InvokeAI") logger.addFilter(lambda x: "fp16 is not a valid" not in x.getMessage()) model = model_class.from_pretrained( diff --git a/invokeai/backend/model_management/convert_ckpt_to_diffusers.py b/invokeai/backend/model_management/convert_ckpt_to_diffusers.py index 69d32a49c7..0a3a63dad6 100644 --- a/invokeai/backend/model_management/convert_ckpt_to_diffusers.py +++ b/invokeai/backend/model_management/convert_ckpt_to_diffusers.py @@ -74,7 +74,7 @@ if is_accelerate_available(): from accelerate import init_empty_weights from accelerate.utils import set_module_tensor_to_device -logger = InvokeAILogger.getLogger(__name__) +logger = InvokeAILogger.get_logger(__name__) CONVERT_MODEL_ROOT = InvokeAIAppConfig.get_config().models_path / "core/convert" @@ -1279,12 +1279,12 @@ def download_from_original_stable_diffusion_ckpt( extract_ema = original_config["model"]["params"]["use_ema"] if ( - model_version == BaseModelType.StableDiffusion2 + model_version in [BaseModelType.StableDiffusion2, BaseModelType.StableDiffusion1] and original_config["model"]["params"].get("parameterization") == "v" ): prediction_type = "v_prediction" upcast_attention = True - image_size = 768 + image_size = 768 if model_version == BaseModelType.StableDiffusion2 else 512 else: prediction_type = "epsilon" upcast_attention = False diff --git a/invokeai/backend/model_management/model_probe.py b/invokeai/backend/model_management/model_probe.py index 1fc4a51354..7821971b38 100644 --- a/invokeai/backend/model_management/model_probe.py +++ b/invokeai/backend/model_management/model_probe.py @@ -90,8 +90,7 @@ class ModelProbe(object): to place it somewhere in the models directory hierarchy. If the model is already loaded into memory, you may provide it as model in order to avoid opening it a second time. The prediction_type_helper callable is a function that receives - the path to the model and returns the BaseModelType. It is called to distinguish - between V2-Base and V2-768 SD models. + the path to the model and returns the SchedulerPredictionType. """ if model_path: format_type = "diffusers" if model_path.is_dir() else "checkpoint" @@ -305,25 +304,36 @@ class PipelineCheckpointProbe(CheckpointProbeBase): else: raise InvalidModelException("Cannot determine base type") - def get_scheduler_prediction_type(self) -> SchedulerPredictionType: + def get_scheduler_prediction_type(self) -> Optional[SchedulerPredictionType]: + """Return model prediction type.""" + # if there is a .yaml associated with this checkpoint, then we do not need + # to probe for the prediction type as it will be ignored. + if self.checkpoint_path and self.checkpoint_path.with_suffix(".yaml").exists(): + return None + type = self.get_base_type() - if type == BaseModelType.StableDiffusion1: - return SchedulerPredictionType.Epsilon - checkpoint = self.checkpoint - state_dict = self.checkpoint.get("state_dict") or checkpoint - key_name = "model.diffusion_model.input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight" - if key_name in state_dict and state_dict[key_name].shape[-1] == 1024: - if "global_step" in checkpoint: - if checkpoint["global_step"] == 220000: - return SchedulerPredictionType.Epsilon - elif checkpoint["global_step"] == 110000: - return SchedulerPredictionType.VPrediction - if ( - self.checkpoint_path and self.helper and not self.checkpoint_path.with_suffix(".yaml").exists() - ): # if a .yaml config file exists, then this step not needed - return self.helper(self.checkpoint_path) - else: - return None + if type == BaseModelType.StableDiffusion2: + checkpoint = self.checkpoint + state_dict = self.checkpoint.get("state_dict") or checkpoint + key_name = "model.diffusion_model.input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight" + if key_name in state_dict and state_dict[key_name].shape[-1] == 1024: + if "global_step" in checkpoint: + if checkpoint["global_step"] == 220000: + return SchedulerPredictionType.Epsilon + elif checkpoint["global_step"] == 110000: + return SchedulerPredictionType.VPrediction + if self.helper and self.checkpoint_path: + if helper_guess := self.helper(self.checkpoint_path): + return helper_guess + return SchedulerPredictionType.VPrediction # a guess for sd2 ckpts + + elif type == BaseModelType.StableDiffusion1: + if self.helper and self.checkpoint_path: + if helper_guess := self.helper(self.checkpoint_path): + return helper_guess + return SchedulerPredictionType.Epsilon # a reasonable guess for sd1 ckpts + else: + return None class VaeCheckpointProbe(CheckpointProbeBase): diff --git a/invokeai/backend/model_management/model_search.py b/invokeai/backend/model_management/model_search.py index 3cded73d80..7e6b37c832 100644 --- a/invokeai/backend/model_management/model_search.py +++ b/invokeai/backend/model_management/model_search.py @@ -71,7 +71,13 @@ class ModelSearch(ABC): if any( [ (path / x).exists() - for x in {"config.json", "model_index.json", "learned_embeds.bin", "pytorch_lora_weights.bin"} + for x in { + "config.json", + "model_index.json", + "learned_embeds.bin", + "pytorch_lora_weights.bin", + "image_encoder.txt", + } ] ): try: diff --git a/invokeai/backend/util/hotfixes.py b/invokeai/backend/util/hotfixes.py index 852d640161..fb1297996c 100644 --- a/invokeai/backend/util/hotfixes.py +++ b/invokeai/backend/util/hotfixes.py @@ -24,7 +24,7 @@ from invokeai.backend.util.logging import InvokeAILogger # Modified ControlNetModel with encoder_attention_mask argument added -logger = InvokeAILogger.getLogger(__name__) +logger = InvokeAILogger.get_logger(__name__) class ControlNetModel(ModelMixin, ConfigMixin, FromOriginalControlnetMixin): diff --git a/invokeai/backend/util/logging.py b/invokeai/backend/util/logging.py index accbc407f7..3c829a1a02 100644 --- a/invokeai/backend/util/logging.py +++ b/invokeai/backend/util/logging.py @@ -1,7 +1,6 @@ # Copyright (c) 2023 Lincoln D. Stein and The InvokeAI Development Team -""" -invokeai.backend.util.logging +"""invokeai.backend.util.logging Logging class for InvokeAI that produces console messages @@ -9,9 +8,9 @@ Usage: from invokeai.backend.util.logging import InvokeAILogger -logger = InvokeAILogger.getLogger(name='InvokeAI') // Initialization +logger = InvokeAILogger.get_logger(name='InvokeAI') // Initialization (or) -logger = InvokeAILogger.getLogger(__name__) // To use the filename +logger = InvokeAILogger.get_logger(__name__) // To use the filename logger.configure() logger.critical('this is critical') // Critical Message @@ -34,13 +33,13 @@ IAILogger.debug('this is a debugging message') ## Configuration The default configuration will print to stderr on the console. To add -additional logging handlers, call getLogger with an initialized InvokeAIAppConfig +additional logging handlers, call get_logger with an initialized InvokeAIAppConfig object: config = InvokeAIAppConfig.get_config() config.parse_args() - logger = InvokeAILogger.getLogger(config=config) + logger = InvokeAILogger.get_logger(config=config) ### Three command-line options control logging: @@ -173,6 +172,7 @@ InvokeAI: log_level: info log_format: color ``` + """ import logging.handlers @@ -193,39 +193,35 @@ except ImportError: # module level functions def debug(msg, *args, **kwargs): - InvokeAILogger.getLogger().debug(msg, *args, **kwargs) + InvokeAILogger.get_logger().debug(msg, *args, **kwargs) def info(msg, *args, **kwargs): - InvokeAILogger.getLogger().info(msg, *args, **kwargs) + InvokeAILogger.get_logger().info(msg, *args, **kwargs) def warning(msg, *args, **kwargs): - InvokeAILogger.getLogger().warning(msg, *args, **kwargs) + InvokeAILogger.get_logger().warning(msg, *args, **kwargs) def error(msg, *args, **kwargs): - InvokeAILogger.getLogger().error(msg, *args, **kwargs) + InvokeAILogger.get_logger().error(msg, *args, **kwargs) def critical(msg, *args, **kwargs): - InvokeAILogger.getLogger().critical(msg, *args, **kwargs) + InvokeAILogger.get_logger().critical(msg, *args, **kwargs) def log(level, msg, *args, **kwargs): - InvokeAILogger.getLogger().log(level, msg, *args, **kwargs) + InvokeAILogger.get_logger().log(level, msg, *args, **kwargs) def disable(level=logging.CRITICAL): - InvokeAILogger.getLogger().disable(level) + InvokeAILogger.get_logger().disable(level) def basicConfig(**kwargs): - InvokeAILogger.getLogger().basicConfig(**kwargs) - - -def getLogger(name: str = None) -> logging.Logger: - return InvokeAILogger.getLogger(name) + InvokeAILogger.get_logger().basicConfig(**kwargs) _FACILITY_MAP = ( @@ -351,7 +347,7 @@ class InvokeAILogger(object): loggers = dict() @classmethod - def getLogger( + def get_logger( cls, name: str = "InvokeAI", config: InvokeAIAppConfig = InvokeAIAppConfig.get_config() ) -> logging.Logger: if name in cls.loggers: @@ -360,13 +356,13 @@ class InvokeAILogger(object): else: logger = logging.getLogger(name) logger.setLevel(config.log_level.upper()) # yes, strings work here - for ch in cls.getLoggers(config): + for ch in cls.get_loggers(config): logger.addHandler(ch) cls.loggers[name] = logger return cls.loggers[name] @classmethod - def getLoggers(cls, config: InvokeAIAppConfig) -> list[logging.Handler]: + def get_loggers(cls, config: InvokeAIAppConfig) -> list[logging.Handler]: handler_strs = config.log_handlers handlers = list() for handler in handler_strs: diff --git a/invokeai/configs/INITIAL_MODELS.yaml b/invokeai/configs/INITIAL_MODELS.yaml index e250b5efba..f3dbc11c2a 100644 --- a/invokeai/configs/INITIAL_MODELS.yaml +++ b/invokeai/configs/INITIAL_MODELS.yaml @@ -103,3 +103,35 @@ sd-1/lora/LowRA: recommended: True sd-1/lora/Ink scenery: path: https://civitai.com/api/download/models/83390 +sd-1/ip_adapter/ip_adapter_sd15: + repo_id: InvokeAI/ip_adapter_sd15 + recommended: True + requires: + - InvokeAI/ip_adapter_sd_image_encoder + description: IP-Adapter for SD 1.5 models +sd-1/ip_adapter/ip_adapter_plus_sd15: + repo_id: InvokeAI/ip_adapter_plus_sd15 + recommended: False + requires: + - InvokeAI/ip_adapter_sd_image_encoder + description: Refined IP-Adapter for SD 1.5 models +sd-1/ip_adapter/ip_adapter_plus_face_sd15: + repo_id: InvokeAI/ip_adapter_plus_face_sd15 + recommended: False + requires: + - InvokeAI/ip_adapter_sd_image_encoder + description: Refined IP-Adapter for SD 1.5 models, adapted for faces +sdxl/ip_adapter/ip_adapter_sdxl: + repo_id: InvokeAI/ip_adapter_sdxl + recommended: False + requires: + - InvokeAI/ip_adapter_sdxl_image_encoder + description: IP-Adapter for SDXL models +any/clip_vision/ip_adapter_sd_image_encoder: + repo_id: InvokeAI/ip_adapter_sd_image_encoder + recommended: False + description: Required model for using IP-Adapters with SD-1/2 models +any/clip_vision/ip_adapter_sdxl_image_encoder: + repo_id: InvokeAI/ip_adapter_sdxl_image_encoder + recommended: False + description: Required model for using IP-Adapters with SDXL models diff --git a/invokeai/configs/stable-diffusion/v1-inference-v.yaml b/invokeai/configs/stable-diffusion/v1-inference-v.yaml new file mode 100644 index 0000000000..cb413c2567 --- /dev/null +++ b/invokeai/configs/stable-diffusion/v1-inference-v.yaml @@ -0,0 +1,80 @@ +model: + base_learning_rate: 1.0e-04 + target: invokeai.backend.models.diffusion.ddpm.LatentDiffusion + params: + parameterization: "v" + linear_start: 0.00085 + linear_end: 0.0120 + num_timesteps_cond: 1 + log_every_t: 200 + timesteps: 1000 + first_stage_key: "jpg" + cond_stage_key: "txt" + image_size: 64 + channels: 4 + cond_stage_trainable: false # Note: different from the one we trained before + conditioning_key: crossattn + monitor: val/loss_simple_ema + scale_factor: 0.18215 + use_ema: False + + scheduler_config: # 10000 warmup steps + target: invokeai.backend.stable_diffusion.lr_scheduler.LambdaLinearScheduler + params: + warm_up_steps: [ 10000 ] + cycle_lengths: [ 10000000000000 ] # incredibly large number to prevent corner cases + f_start: [ 1.e-6 ] + f_max: [ 1. ] + f_min: [ 1. ] + + personalization_config: + target: invokeai.backend.stable_diffusion.embedding_manager.EmbeddingManager + params: + placeholder_strings: ["*"] + initializer_words: ['sculpture'] + per_image_tokens: false + num_vectors_per_token: 1 + progressive_words: False + + unet_config: + target: invokeai.backend.stable_diffusion.diffusionmodules.openaimodel.UNetModel + params: + image_size: 32 # unused + in_channels: 4 + out_channels: 4 + model_channels: 320 + attention_resolutions: [ 4, 2, 1 ] + num_res_blocks: 2 + channel_mult: [ 1, 2, 4, 4 ] + num_heads: 8 + use_spatial_transformer: True + transformer_depth: 1 + context_dim: 768 + use_checkpoint: True + legacy: False + + first_stage_config: + target: invokeai.backend.stable_diffusion.autoencoder.AutoencoderKL + params: + embed_dim: 4 + monitor: val/rec_loss + ddconfig: + double_z: true + z_channels: 4 + resolution: 256 + in_channels: 3 + out_ch: 3 + ch: 128 + ch_mult: + - 1 + - 2 + - 4 + - 4 + num_res_blocks: 2 + attn_resolutions: [] + dropout: 0.0 + lossconfig: + target: torch.nn.Identity + + cond_stage_config: + target: invokeai.backend.stable_diffusion.encoders.modules.WeightedFrozenCLIPEmbedder diff --git a/invokeai/frontend/install/model_install.py b/invokeai/frontend/install/model_install.py index fae67df736..b8a44ae089 100644 --- a/invokeai/frontend/install/model_install.py +++ b/invokeai/frontend/install/model_install.py @@ -45,7 +45,7 @@ from invokeai.frontend.install.widgets import ( ) config = InvokeAIAppConfig.get_config() -logger = InvokeAILogger.getLogger() +logger = InvokeAILogger.get_logger() # build a table mapping all non-printable characters to None # for stripping control characters @@ -101,11 +101,12 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): "STARTER MODELS", "MAIN MODELS", "CONTROLNETS", + "IP-ADAPTERS", "LORA/LYCORIS", "TEXTUAL INVERSION", ], value=[self.current_tab], - columns=5, + columns=6, max_height=2, relx=8, scroll_exit=True, @@ -130,6 +131,13 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): ) bottom_of_table = max(bottom_of_table, self.nextrely) + self.nextrely = top_of_table + self.ipadapter_models = self.add_model_widgets( + model_type=ModelType.IPAdapter, + window_width=window_width, + ) + bottom_of_table = max(bottom_of_table, self.nextrely) + self.nextrely = top_of_table self.lora_models = self.add_model_widgets( model_type=ModelType.Lora, @@ -343,6 +351,7 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): self.starter_pipelines, self.pipeline_models, self.controlnet_models, + self.ipadapter_models, self.lora_models, self.ti_models, ] @@ -532,6 +541,7 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): self.starter_pipelines, self.pipeline_models, self.controlnet_models, + self.ipadapter_models, self.lora_models, self.ti_models, ] @@ -553,6 +563,25 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): if downloads := section.get("download_ids"): selections.install_models.extend(downloads.value.split()) + # NOT NEEDED - DONE IN BACKEND NOW + # # special case for the ipadapter_models. If any of the adapters are + # # chosen, then we add the corresponding encoder(s) to the install list. + # section = self.ipadapter_models + # if section.get("models_selected"): + # selected_adapters = [ + # self.all_models[section["models"][x]].name for x in section.get("models_selected").value + # ] + # encoders = [] + # if any(["sdxl" in x for x in selected_adapters]): + # encoders.append("ip_adapter_sdxl_image_encoder") + # if any(["sd15" in x for x in selected_adapters]): + # encoders.append("ip_adapter_sd_image_encoder") + # for encoder in encoders: + # key = f"any/clip_vision/{encoder}" + # repo_id = f"InvokeAI/{encoder}" + # if key not in self.all_models: + # selections.install_models.append(repo_id) + class AddModelApplication(npyscreen.NPSAppManaged): def __init__(self, opt): @@ -652,7 +681,7 @@ def process_and_execute( translator = StderrToMessage(conn_out) sys.stderr = translator sys.stdout = translator - logger = InvokeAILogger.getLogger() + logger = InvokeAILogger.get_logger() logger.handlers.clear() logger.addHandler(logging.StreamHandler(translator)) @@ -765,7 +794,7 @@ def main(): if opt.full_precision: invoke_args.extend(["--precision", "float32"]) config.parse_args(invoke_args) - logger = InvokeAILogger().getLogger(config=config) + logger = InvokeAILogger().get_logger(config=config) if not config.model_conf_path.exists(): logger.info("Your InvokeAI root directory is not set up. Calling invokeai-configure.") diff --git a/invokeai/frontend/web/dist/locales/en.json b/invokeai/frontend/web/dist/locales/en.json index c309e4de50..f00c3782b9 100644 --- a/invokeai/frontend/web/dist/locales/en.json +++ b/invokeai/frontend/web/dist/locales/en.json @@ -635,7 +635,7 @@ "onnxModels": "Onnx", "pathToCustomConfig": "Path To Custom Config", "pickModelType": "Pick Model Type", - "predictionType": "Prediction Type (for Stable Diffusion 2.x Models only)", + "predictionType": "Prediction Type (for Stable Diffusion 2.x Models and occasional Stable Diffusion 1.x Models)", "quickAdd": "Quick Add", "repo_id": "Repo ID", "repoIDValidationMsg": "Online repository of your model", diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index c309e4de50..fc9dd0cc5f 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -58,6 +58,7 @@ "githubLabel": "Github", "hotkeysLabel": "Hotkeys", "imagePrompt": "Image Prompt", + "imageFailedToLoad": "Unable to Load Image", "img2img": "Image To Image", "langArabic": "العربية", "langBrPortuguese": "Português do Brasil", @@ -79,8 +80,9 @@ "lightMode": "Light Mode", "linear": "Linear", "load": "Load", - "loading": "Loading", + "loading": "Loading $t({{noun}})...", "loadingInvokeAI": "Loading Invoke AI", + "learnMore": "Learn More", "modelManager": "Model Manager", "nodeEditor": "Node Editor", "nodes": "Workflow Editor", @@ -135,6 +137,8 @@ "bgth": "bg_th", "canny": "Canny", "cannyDescription": "Canny edge detection", + "colorMap": "Color", + "colorMapDescription": "Generates a color map from the image", "coarse": "Coarse", "contentShuffle": "Content Shuffle", "contentShuffleDescription": "Shuffles the content in an image", @@ -158,6 +162,7 @@ "hideAdvanced": "Hide Advanced", "highThreshold": "High Threshold", "imageResolution": "Image Resolution", + "colorMapTileSize": "Tile Size", "importImageFromCanvas": "Import Image From Canvas", "importMaskFromCanvas": "Import Mask From Canvas", "incompatibleBaseModel": "Incompatible base model:", @@ -264,6 +269,22 @@ "graphQueued": "Graph queued", "graphFailedToQueue": "Failed to queue graph" }, + "invocationCache": { + "invocationCache": "Invocation Cache", + "cacheSize": "Cache Size", + "maxCacheSize": "Max Cache Size", + "hits": "Cache Hits", + "misses": "Cache Misses", + "clear": "Clear", + "clearSucceeded": "Invocation Cache Cleared", + "clearFailed": "Problem Clearing Invocation Cache", + "enable": "Enable", + "enableSucceeded": "Invocation Cache Enabled", + "enableFailed": "Problem Enabling Invocation Cache", + "disable": "Disable", + "disableSucceeded": "Invocation Cache Disabled", + "disableFailed": "Problem Disabling Invocation Cache" + }, "gallery": { "allImagesLoaded": "All Images Loaded", "assets": "Assets", @@ -635,7 +656,7 @@ "onnxModels": "Onnx", "pathToCustomConfig": "Path To Custom Config", "pickModelType": "Pick Model Type", - "predictionType": "Prediction Type (for Stable Diffusion 2.x Models only)", + "predictionType": "Prediction Type (for Stable Diffusion 2.x Models and occasional Stable Diffusion 1.x Models)", "quickAdd": "Quick Add", "repo_id": "Repo ID", "repoIDValidationMsg": "Online repository of your model", @@ -685,6 +706,8 @@ "addNodeToolTip": "Add Node (Shift+A, Space)", "animatedEdges": "Animated Edges", "animatedEdgesHelp": "Animate selected edges and edges connected to selected nodes", + "boardField": "Board", + "boardFieldDescription": "A gallery board", "boolean": "Booleans", "booleanCollection": "Boolean Collection", "booleanCollectionDescription": "A collection of booleans.", @@ -694,6 +717,7 @@ "cannotConnectInputToInput": "Cannot connect input to input", "cannotConnectOutputToOutput": "Cannot connect output to output", "cannotConnectToSelf": "Cannot connect to self", + "cannotDuplicateConnection": "Cannot create duplicate connections", "clipField": "Clip", "clipFieldDescription": "Tokenizer and text_encoder submodels.", "collection": "Collection", @@ -872,7 +896,7 @@ "zoomOutNodes": "Zoom Out" }, "parameters": { - "aspectRatio": "Ratio", + "aspectRatio": "Aspect Ratio", "boundingBoxHeader": "Bounding Box", "boundingBoxHeight": "Bounding Box Height", "boundingBoxWidth": "Bounding Box Width", @@ -1004,8 +1028,8 @@ "label": "Seed Behaviour", "perIterationLabel": "Seed per Iteration", "perIterationDesc": "Use a different seed for each iteration", - "perPromptLabel": "Seed per Prompt", - "perPromptDesc": "Use a different seed for each prompt" + "perPromptLabel": "Seed per Image", + "perPromptDesc": "Use a different seed for each image" } }, "sdxl": { @@ -1157,131 +1181,205 @@ "popovers": { "clipSkip": { "heading": "CLIP Skip", - "paragraph": "Choose how many layers of the CLIP model to skip. Certain models are better suited to be used with CLIP Skip." - }, - "compositingBlur": { - "heading": "Blur", - "paragraph": "The blur radius of the mask." - }, - "compositingBlurMethod": { - "heading": "Blur Method", - "paragraph": "The method of blur applied to the masked area." - }, - "compositingCoherencePass": { - "heading": "Coherence Pass", - "paragraph": "Composite the Inpainted/Outpainted images." - }, - "compositingCoherenceMode": { - "heading": "Mode", - "paragraph": "The mode of the Coherence Pass." - }, - "compositingCoherenceSteps": { - "heading": "Steps", - "paragraph": "Number of steps in the Coherence Pass. Similar to Denoising Steps." - }, - "compositingStrength": { - "heading": "Strength", - "paragraph": "Amount of noise added for the Coherence Pass. Similar to Denoising Strength." - }, - "compositingMaskAdjustments": { - "heading": "Mask Adjustments", - "paragraph": "Adjust the mask." - }, - "controlNetBeginEnd": { - "heading": "Begin / End Step Percentage", - "paragraph": "Which parts of the denoising process will have the ControlNet applied. ControlNets applied at the start of the process guide composition, and ControlNets applied at the end guide details." - }, - "controlNetControlMode": { - "heading": "Control Mode", - "paragraph": "Lends more weight to either the prompt or ControlNet." - }, - "controlNetResizeMode": { - "heading": "Resize Mode", - "paragraph": "How the ControlNet image will be fit to the image generation Ratio" - }, - "controlNetToggle": { - "heading": "Enable ControlNet", - "paragraph": "ControlNets provide guidance to the generation process, helping create images with controlled composition, structure, or style, depending on the model selected." - }, - "controlNetWeight": { - "heading": "Weight", - "paragraph": "How strongly the ControlNet will impact the generated image." - }, - "dynamicPromptsToggle": { - "heading": "Enable Dynamic Prompts", - "paragraph": "Dynamic prompts allow multiple options within a prompt. Dynamic prompts can be used by: {option1|option2|option3}. Combinations of prompts will be randomly generated until the “Images” number has been reached." - }, - "dynamicPromptsCombinatorial": { - "heading": "Combinatorial Generation", - "paragraph": "Generate an image for every possible combination of Dynamic Prompt until the Max Prompts is reached." - }, - "infillMethod": { - "heading": "Infill Method", - "paragraph": "Method to infill the selected area." - }, - "lora": { - "heading": "LoRA", - "paragraph": "Weight of the LoRA. Higher weight will lead to larger impacts on the final image." - }, - "noiseEnable": { - "heading": "Enable Noise Settings", - "paragraph": "Advanced control over noise generation." - }, - "noiseUseCPU": { - "heading": "Use CPU Noise", - "paragraph": "Uses the CPU to generate random noise." - }, - "paramCFGScale": { - "heading": "CFG Scale", - "paragraph": "Controls how much your prompt influences the generation process." - }, - "paramDenoisingStrength": { - "heading": "Denoising Strength", - "paragraph": "How much noise is added to the input image. 0 will result in an identical image, while 1 will result in a completely new image." - }, - "paramImages": { - "heading": "Images", - "paragraph": "Number of images that will be generated." - }, - "paramModel": { - "heading": "Model", - "paragraph": "Model used for the denoising steps. Different models are trained to specialize in producing different aesthetic results and content." + "paragraphs": [ + "Choose how many layers of the CLIP model to skip.", + "Some models work better with certain CLIP Skip settings.", + "A higher value typically results in a less detailed image." + ] }, "paramNegativeConditioning": { - "heading": "Negative Prompts", - "paragraph": "This is where you enter your negative prompts." + "heading": "Negative Prompt", + "paragraphs": [ + "The generation process avoids the concepts in the negative prompt. Use this to exclude qualities or objects from the output.", + "Supports Compel syntax and embeddings." + ] }, "paramPositiveConditioning": { - "heading": "Positive Prompts", - "paragraph": "This is where you enter your positive prompts." - }, - "paramRatio": { - "heading": "Ratio", - "paragraph": "The ratio of the dimensions of the image generated. An image size (in number of pixels) equivalent to 512x512 is recommended for SD1.5 models and a size equivalent to 1024x1024 is recommended for SDXL models." + "heading": "Positive Prompt", + "paragraphs": [ + "Guides the generation process. You may use any words or phrases.", + "Compel and Dynamic Prompts syntaxes and embeddings." + ] }, "paramScheduler": { "heading": "Scheduler", - "paragraph": "Scheduler defines how to iteratively add noise to an image or how to update a sample based on a model's output." + "paragraphs": [ + "Scheduler defines how to iteratively add noise to an image or how to update a sample based on a model's output." + ] + }, + "compositingBlur": { + "heading": "Blur", + "paragraphs": ["The blur radius of the mask."] + }, + "compositingBlurMethod": { + "heading": "Blur Method", + "paragraphs": ["The method of blur applied to the masked area."] + }, + "compositingCoherencePass": { + "heading": "Coherence Pass", + "paragraphs": [ + "A second round of denoising helps to composite the Inpainted/Outpainted image." + ] + }, + "compositingCoherenceMode": { + "heading": "Mode", + "paragraphs": ["The mode of the Coherence Pass."] + }, + "compositingCoherenceSteps": { + "heading": "Steps", + "paragraphs": [ + "Number of denoising steps used in the Coherence Pass.", + "Same as the main Steps parameter." + ] + }, + "compositingStrength": { + "heading": "Strength", + "paragraphs": [ + "Denoising strength for the Coherence Pass.", + "Same as the Image to Image Denoising Strength parameter." + ] + }, + "compositingMaskAdjustments": { + "heading": "Mask Adjustments", + "paragraphs": ["Adjust the mask."] + }, + "controlNetBeginEnd": { + "heading": "Begin / End Step Percentage", + "paragraphs": [ + "Which steps of the denoising process will have the ControlNet applied.", + "ControlNets applied at the beginning of the process guide composition, and ControlNets applied at the end guide details." + ] + }, + "controlNetControlMode": { + "heading": "Control Mode", + "paragraphs": [ + "Lends more weight to either the prompt or ControlNet." + ] + }, + "controlNetResizeMode": { + "heading": "Resize Mode", + "paragraphs": [ + "How the ControlNet image will be fit to the image output size." + ] + }, + "controlNet": { + "heading": "ControlNet", + "paragraphs": [ + "ControlNets provide guidance to the generation process, helping create images with controlled composition, structure, or style, depending on the model selected." + ] + }, + "controlNetWeight": { + "heading": "Weight", + "paragraphs": [ + "How strongly the ControlNet will impact the generated image." + ] + }, + "dynamicPrompts": { + "heading": "Dynamic Prompts", + "paragraphs": [ + "Dynamic Prompts parses a single prompt into many.", + "The basic syntax is \"a {red|green|blue} ball\". This will produce three prompts: \"a red ball\", \"a green ball\" and \"a blue ball\".", + "You can use the syntax as many times as you like in a single prompt, but be sure to keep the number of prompts generated in check with the Max Prompts setting." + ] + }, + "dynamicPromptsMaxPrompts": { + "heading": "Max Prompts", + "paragraphs": [ + "Limits the number of prompts that can be generated by Dynamic Prompts." + ] + }, + "dynamicPromptsSeedBehaviour": { + "heading": "Seed Behaviour", + "paragraphs": [ + "Controls how the seed is used when generating prompts.", + "Per Iteration will use a unique seed for each iteration. Use this to explore prompt variations on a single seed.", + "For example, if you have 5 prompts, each image will use the same seed.", + "Per Image will use a unique seed for each image. This provides more variation." + ] + }, + "infillMethod": { + "heading": "Infill Method", + "paragraphs": ["Method to infill the selected area."] + }, + "lora": { + "heading": "LoRA Weight", + "paragraphs": [ + "Higher LoRA weight will lead to larger impacts on the final image." + ] + }, + "noiseUseCPU": { + "heading": "Use CPU Noise", + "paragraphs": [ + "Controls whether noise is generated on the CPU or GPU.", + "With CPU Noise enabled, a particular seed will produce the same image on any machine.", + "There is no performance impact to enabling CPU Noise." + ] + }, + "paramCFGScale": { + "heading": "CFG Scale", + "paragraphs": [ + "Controls how much your prompt influences the generation process." + ] + }, + "paramDenoisingStrength": { + "heading": "Denoising Strength", + "paragraphs": [ + "How much noise is added to the input image.", + "0 will result in an identical image, while 1 will result in a completely new image." + ] + }, + "paramIterations": { + "heading": "Iterations", + "paragraphs": [ + "The number of images to generate.", + "If Dynamic Prompts is enabled, each of the prompts will be generated this many times." + ] + }, + "paramModel": { + "heading": "Model", + "paragraphs": [ + "Model used for the denoising steps.", + "Different models are typically trained to specialize in producing particular aesthetic results and content." + ] + }, + "paramRatio": { + "heading": "Aspect Ratio", + "paragraphs": [ + "The aspect ratio of the dimensions of the image generated.", + "An image size (in number of pixels) equivalent to 512x512 is recommended for SD1.5 models and a size equivalent to 1024x1024 is recommended for SDXL models." + ] }, "paramSeed": { "heading": "Seed", - "paragraph": "Controls the starting noise used for generation. Disable “Random Seed” to produce identical results with the same generation settings." + "paragraphs": [ + "Controls the starting noise used for generation.", + "Disable “Random Seed” to produce identical results with the same generation settings." + ] }, "paramSteps": { "heading": "Steps", - "paragraph": "Number of steps that will be performed in each generation. Higher step counts will typically create better images but will require more generation time." + "paragraphs": [ + "Number of steps that will be performed in each generation.", + "Higher step counts will typically create better images but will require more generation time." + ] }, "paramVAE": { "heading": "VAE", - "paragraph": "Model used for translating AI output into the final image." + "paragraphs": [ + "Model used for translating AI output into the final image." + ] }, "paramVAEPrecision": { "heading": "VAE Precision", - "paragraph": "The precision used during VAE encoding and decoding. Fp16/Half precision is more efficient, at the expense of minor image variations." + "paragraphs": [ + "The precision used during VAE encoding and decoding. FP16/half precision is more efficient, at the expense of minor image variations." + ] }, "scaleBeforeProcessing": { "heading": "Scale Before Processing", - "paragraph": "Scales the selected area to the size best suited for the model before the image generation process." + "paragraphs": [ + "Scales the selected area to the size best suited for the model before the image generation process." + ] } }, "ui": { @@ -1346,6 +1444,8 @@ "showCanvasDebugInfo": "Show Additional Canvas Info", "showGrid": "Show Grid", "showHide": "Show/Hide", + "showResultsOn": "Show Results (On)", + "showResultsOff": "Show Results (Off)", "showIntermediates": "Show Intermediates", "snapToGrid": "Snap to Grid", "undo": "Undo" diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 8c033440e3..b3ffeee333 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -36,7 +36,8 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => { const logger = useLogger('system'); const dispatch = useAppDispatch(); - const { handlePreselectedImage } = usePreselectedImage(); + const { handleSendToCanvas, handleSendToImg2Img, handleUseAllMetadata } = + usePreselectedImage(selectedImage?.imageName); const handleReset = useCallback(() => { localStorage.clear(); location.reload(); @@ -59,8 +60,22 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => { }, [dispatch]); useEffect(() => { - handlePreselectedImage(selectedImage); - }, [handlePreselectedImage, selectedImage]); + if (selectedImage && selectedImage.action === 'sendToCanvas') { + handleSendToCanvas(); + } + }, [selectedImage, handleSendToCanvas]); + + useEffect(() => { + if (selectedImage && selectedImage.action === 'sendToImg2Img') { + handleSendToImg2Img(); + } + }, [selectedImage, handleSendToImg2Img]); + + useEffect(() => { + if (selectedImage && selectedImage.action === 'useAllParameters') { + handleUseAllMetadata(); + } + }, [selectedImage, handleUseAllMetadata]); const headerComponent = useStore($headerComponent); diff --git a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx index 8c8c02ee85..02d682dfb4 100644 --- a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx +++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx @@ -17,7 +17,10 @@ import '../../i18n'; import AppDndContext from '../../features/dnd/components/AppDndContext'; import { $customStarUI, CustomStarUi } from 'app/store/nanostores/customStarUI'; import { $headerComponent } from 'app/store/nanostores/headerComponent'; -import { $queueId, DEFAULT_QUEUE_ID } from 'features/queue/store/nanoStores'; +import { + $queueId, + DEFAULT_QUEUE_ID, +} from 'features/queue/store/queueNanoStore'; const App = lazy(() => import('./App')); const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider')); diff --git a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx index 9bcc7c831b..a9d56a7f16 100644 --- a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx +++ b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx @@ -5,7 +5,7 @@ import { } from '@chakra-ui/react'; import { ReactNode, memo, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { theme as invokeAITheme } from 'theme/theme'; +import { TOAST_OPTIONS, theme as invokeAITheme } from 'theme/theme'; import '@fontsource-variable/inter'; import { MantineProvider } from '@mantine/core'; @@ -39,7 +39,11 @@ function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) { return ( - + {children} diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index ead6e1cd42..677b0fd20c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -54,21 +54,6 @@ import { addModelSelectedListener } from './listeners/modelSelected'; import { addModelsLoadedListener } from './listeners/modelsLoaded'; import { addDynamicPromptsListener } from './listeners/promptChanged'; import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema'; -import { - addSessionCanceledFulfilledListener, - addSessionCanceledPendingListener, - addSessionCanceledRejectedListener, -} from './listeners/sessionCanceled'; -import { - addSessionCreatedFulfilledListener, - addSessionCreatedPendingListener, - addSessionCreatedRejectedListener, -} from './listeners/sessionCreated'; -import { - addSessionInvokedFulfilledListener, - addSessionInvokedPendingListener, - addSessionInvokedRejectedListener, -} from './listeners/sessionInvoked'; import { addSocketConnectedEventListener as addSocketConnectedListener } from './listeners/socketio/socketConnected'; import { addSocketDisconnectedEventListener as addSocketDisconnectedListener } from './listeners/socketio/socketDisconnected'; import { addGeneratorProgressEventListener as addGeneratorProgressListener } from './listeners/socketio/socketGeneratorProgress'; @@ -86,6 +71,7 @@ import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSa import { addTabChangedListener } from './listeners/tabChanged'; import { addUpscaleRequestedListener } from './listeners/upscaleRequested'; import { addWorkflowLoadedListener } from './listeners/workflowLoaded'; +import { addBatchEnqueuedListener } from './listeners/batchEnqueued'; export const listenerMiddleware = createListenerMiddleware(); @@ -136,6 +122,7 @@ addEnqueueRequestedCanvasListener(); addEnqueueRequestedNodes(); addEnqueueRequestedLinear(); addAnyEnqueuedListener(); +addBatchEnqueuedListener(); // Canvas actions addCanvasSavedToGalleryListener(); @@ -175,21 +162,6 @@ addSessionRetrievalErrorEventListener(); addInvocationRetrievalErrorEventListener(); addSocketQueueItemStatusChangedEventListener(); -// Session Created -addSessionCreatedPendingListener(); -addSessionCreatedFulfilledListener(); -addSessionCreatedRejectedListener(); - -// Session Invoked -addSessionInvokedPendingListener(); -addSessionInvokedFulfilledListener(); -addSessionInvokedRejectedListener(); - -// Session Canceled -addSessionCanceledPendingListener(); -addSessionCanceledFulfilledListener(); -addSessionCanceledRejectedListener(); - // ControlNet addControlNetImageProcessedListener(); addControlNetAutoProcessListener(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts new file mode 100644 index 0000000000..fe351f3be6 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts @@ -0,0 +1,96 @@ +import { createStandaloneToast } from '@chakra-ui/react'; +import { logger } from 'app/logging/logger'; +import { parseify } from 'common/util/serialize'; +import { zPydanticValidationError } from 'features/system/store/zodSchemas'; +import { t } from 'i18next'; +import { get, truncate, upperFirst } from 'lodash-es'; +import { queueApi } from 'services/api/endpoints/queue'; +import { TOAST_OPTIONS, theme } from 'theme/theme'; +import { startAppListening } from '..'; + +const { toast } = createStandaloneToast({ + theme: theme, + defaultOptions: TOAST_OPTIONS.defaultOptions, +}); + +export const addBatchEnqueuedListener = () => { + // success + startAppListening({ + matcher: queueApi.endpoints.enqueueBatch.matchFulfilled, + effect: async (action) => { + const response = action.payload; + const arg = action.meta.arg.originalArgs; + logger('queue').debug( + { enqueueResult: parseify(response) }, + 'Batch enqueued' + ); + + if (!toast.isActive('batch-queued')) { + toast({ + id: 'batch-queued', + title: t('queue.batchQueued'), + description: t('queue.batchQueuedDesc', { + item_count: response.enqueued, + direction: arg.prepend ? t('queue.front') : t('queue.back'), + }), + duration: 1000, + status: 'success', + }); + } + }, + }); + + // error + startAppListening({ + matcher: queueApi.endpoints.enqueueBatch.matchRejected, + effect: async (action) => { + const response = action.payload; + const arg = action.meta.arg.originalArgs; + + if (!response) { + toast({ + title: t('queue.batchFailedToQueue'), + status: 'error', + description: 'Unknown Error', + }); + logger('queue').error( + { batchConfig: parseify(arg), error: parseify(response) }, + t('queue.batchFailedToQueue') + ); + return; + } + + const result = zPydanticValidationError.safeParse(response); + if (result.success) { + result.data.data.detail.map((e) => { + toast({ + id: 'batch-failed-to-queue', + title: truncate(upperFirst(e.msg), { length: 128 }), + status: 'error', + description: truncate( + `Path: + ${e.loc.join('.')}`, + { length: 128 } + ), + }); + }); + } else { + let detail = 'Unknown Error'; + if (response.status === 403 && 'body' in response) { + detail = get(response, 'body.detail', 'Unknown Error'); + } else if (response.status === 403 && 'error' in response) { + detail = get(response, 'error.detail', 'Unknown Error'); + } + toast({ + title: t('queue.batchFailedToQueue'), + status: 'error', + description: detail, + }); + } + logger('queue').error( + { batchConfig: parseify(arg), error: parseify(response) }, + t('queue.batchFailedToQueue') + ); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts index 1b13181911..a8e1a04fc1 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts @@ -25,7 +25,7 @@ export const addBoardIdSelectedListener = () => { const state = getState(); const board_id = boardIdSelected.match(action) - ? action.payload + ? action.payload.boardId : state.gallery.selectedBoardId; const galleryView = galleryViewChanged.match(action) @@ -55,7 +55,12 @@ export const addBoardIdSelectedListener = () => { if (boardImagesData) { const firstImage = imagesSelectors.selectAll(boardImagesData)[0]; - dispatch(imageSelected(firstImage ?? null)); + const selectedImage = imagesSelectors.selectById( + boardImagesData, + action.payload.selectedImageName + ); + + dispatch(imageSelected(selectedImage || firstImage || null)); } else { // board has no images - deselect dispatch(imageSelected(null)); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts index c328aceedf..1ac80d219b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts @@ -3,7 +3,7 @@ import { startAppListening } from '..'; import { $logger } from 'app/logging/logger'; import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; import { addToast } from 'features/system/store/systemSlice'; -import { copyBlobToClipboard } from 'features/canvas/util/copyBlobToClipboard'; +import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard'; import { t } from 'i18next'; export const addCanvasCopiedToClipboardListener = () => { @@ -15,10 +15,12 @@ export const addCanvasCopiedToClipboardListener = () => { .child({ namespace: 'canvasCopiedToClipboardListener' }); const state = getState(); - const blob = await getBaseLayerBlob(state); + try { + const blob = getBaseLayerBlob(state); - if (!blob) { - moduleLog.error('Problem getting base layer blob'); + copyBlobToClipboard(blob); + } catch (err) { + moduleLog.error(String(err)); dispatch( addToast({ title: t('toast.problemCopyingCanvas'), @@ -29,8 +31,6 @@ export const addCanvasCopiedToClipboardListener = () => { return; } - copyBlobToClipboard(blob); - dispatch( addToast({ title: t('toast.canvasCopiedClipboard'), diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts index 23faf4a356..cfaf20b64c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts @@ -15,10 +15,11 @@ export const addCanvasDownloadedAsImageListener = () => { .child({ namespace: 'canvasSavedToGalleryListener' }); const state = getState(); - const blob = await getBaseLayerBlob(state); - - if (!blob) { - moduleLog.error('Problem getting base layer blob'); + let blob; + try { + blob = await getBaseLayerBlob(state); + } catch (err) { + moduleLog.error(String(err)); dispatch( addToast({ title: t('toast.problemDownloadingCanvas'), diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts index 5181df134f..9389b0f373 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts @@ -3,9 +3,9 @@ import { canvasImageToControlNet } from 'features/canvas/store/actions'; import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice'; import { addToast } from 'features/system/store/systemSlice'; +import { t } from 'i18next'; import { imagesApi } from 'services/api/endpoints/images'; import { startAppListening } from '..'; -import { t } from 'i18next'; export const addCanvasImageToControlNetListener = () => { startAppListening({ @@ -14,10 +14,11 @@ export const addCanvasImageToControlNetListener = () => { const log = logger('canvas'); const state = getState(); - const blob = await getBaseLayerBlob(state); - - if (!blob) { - log.error('Problem getting base layer blob'); + let blob; + try { + blob = await getBaseLayerBlob(state, true); + } catch (err) { + log.error(String(err)); dispatch( addToast({ title: t('toast.problemSavingCanvas'), @@ -35,10 +36,10 @@ export const addCanvasImageToControlNetListener = () => { file: new File([blob], 'savedCanvas.png', { type: 'image/png', }), - image_category: 'mask', + image_category: 'control', is_intermediate: false, board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId, - crop_visible: true, + crop_visible: false, postUploadAction: { type: 'TOAST', toastOptions: { title: t('toast.canvasSentControlnetAssets') }, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts index 671c7f63e4..2c5c26e830 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts @@ -3,9 +3,9 @@ import { canvasMaskToControlNet } from 'features/canvas/store/actions'; import { getCanvasData } from 'features/canvas/util/getCanvasData'; import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice'; import { addToast } from 'features/system/store/systemSlice'; +import { t } from 'i18next'; import { imagesApi } from 'services/api/endpoints/images'; import { startAppListening } from '..'; -import { t } from 'i18next'; export const addCanvasMaskToControlNetListener = () => { startAppListening({ @@ -50,7 +50,7 @@ export const addCanvasMaskToControlNetListener = () => { image_category: 'mask', is_intermediate: false, board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId, - crop_visible: true, + crop_visible: false, postUploadAction: { type: 'TOAST', toastOptions: { title: t('toast.maskSentControlnetAssets') }, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts index 0bb8ad8550..23e2cebe53 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts @@ -13,10 +13,11 @@ export const addCanvasSavedToGalleryListener = () => { const log = logger('canvas'); const state = getState(); - const blob = await getBaseLayerBlob(state); - - if (!blob) { - log.error('Problem getting base layer blob'); + let blob; + try { + blob = await getBaseLayerBlob(state); + } catch (err) { + log.error(String(err)); dispatch( addToast({ title: t('toast.problemSavingCanvas'), diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts index c1511bd0e8..8c283ce64e 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts @@ -12,8 +12,6 @@ import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGeneratio import { canvasGraphBuilt } from 'features/nodes/store/actions'; import { buildCanvasGraph } from 'features/nodes/util/graphBuilders/buildCanvasGraph'; import { prepareLinearUIBatch } from 'features/nodes/util/graphBuilders/buildLinearBatchConfig'; -import { addToast } from 'features/system/store/systemSlice'; -import { t } from 'i18next'; import { imagesApi } from 'services/api/endpoints/images'; import { queueApi } from 'services/api/endpoints/queue'; import { ImageDTO } from 'services/api/types'; @@ -140,8 +138,6 @@ export const addEnqueueRequestedCanvasListener = () => { const enqueueResult = await req.unwrap(); req.reset(); - log.debug({ enqueueResult: parseify(enqueueResult) }, 'Batch enqueued'); - const batchId = enqueueResult.batch.batch_id as string; // we know the is a string, backend provides it // Prep the canvas staging area if it is not yet initialized @@ -158,28 +154,8 @@ export const addEnqueueRequestedCanvasListener = () => { // Associate the session with the canvas session ID dispatch(canvasBatchIdAdded(batchId)); - - dispatch( - addToast({ - title: t('queue.batchQueued'), - description: t('queue.batchQueuedDesc', { - item_count: enqueueResult.enqueued, - direction: prepend ? t('queue.front') : t('queue.back'), - }), - status: 'success', - }) - ); } catch { - log.error( - { batchConfig: parseify(batchConfig) }, - t('queue.batchFailedToQueue') - ); - dispatch( - addToast({ - title: t('queue.batchFailedToQueue'), - status: 'error', - }) - ); + // no-op } }, }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index e36c6f2ebe..bb89d18b91 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -1,13 +1,9 @@ -import { logger } from 'app/logging/logger'; import { enqueueRequested } from 'app/store/actions'; -import { parseify } from 'common/util/serialize'; import { prepareLinearUIBatch } from 'features/nodes/util/graphBuilders/buildLinearBatchConfig'; import { buildLinearImageToImageGraph } from 'features/nodes/util/graphBuilders/buildLinearImageToImageGraph'; import { buildLinearSDXLImageToImageGraph } from 'features/nodes/util/graphBuilders/buildLinearSDXLImageToImageGraph'; import { buildLinearSDXLTextToImageGraph } from 'features/nodes/util/graphBuilders/buildLinearSDXLTextToImageGraph'; import { buildLinearTextToImageGraph } from 'features/nodes/util/graphBuilders/buildLinearTextToImageGraph'; -import { addToast } from 'features/system/store/systemSlice'; -import { t } from 'i18next'; import { queueApi } from 'services/api/endpoints/queue'; import { startAppListening } from '..'; @@ -18,7 +14,6 @@ export const addEnqueueRequestedLinear = () => { (action.payload.tabName === 'txt2img' || action.payload.tabName === 'img2img'), effect: async (action, { getState, dispatch }) => { - const log = logger('queue'); const state = getState(); const model = state.generation.model; const { prepend } = action.payload; @@ -41,38 +36,12 @@ export const addEnqueueRequestedLinear = () => { const batchConfig = prepareLinearUIBatch(state, graph, prepend); - try { - const req = dispatch( - queueApi.endpoints.enqueueBatch.initiate(batchConfig, { - fixedCacheKey: 'enqueueBatch', - }) - ); - const enqueueResult = await req.unwrap(); - req.reset(); - - log.debug({ enqueueResult: parseify(enqueueResult) }, 'Batch enqueued'); - dispatch( - addToast({ - title: t('queue.batchQueued'), - description: t('queue.batchQueuedDesc', { - item_count: enqueueResult.enqueued, - direction: prepend ? t('queue.front') : t('queue.back'), - }), - status: 'success', - }) - ); - } catch { - log.error( - { batchConfig: parseify(batchConfig) }, - t('queue.batchFailedToQueue') - ); - dispatch( - addToast({ - title: t('queue.batchFailedToQueue'), - status: 'error', - }) - ); - } + const req = dispatch( + queueApi.endpoints.enqueueBatch.initiate(batchConfig, { + fixedCacheKey: 'enqueueBatch', + }) + ); + req.reset(); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts index 31281678d4..b87e443a4e 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts @@ -1,9 +1,5 @@ -import { logger } from 'app/logging/logger'; import { enqueueRequested } from 'app/store/actions'; -import { parseify } from 'common/util/serialize'; import { buildNodesGraph } from 'features/nodes/util/graphBuilders/buildNodesGraph'; -import { addToast } from 'features/system/store/systemSlice'; -import { t } from 'i18next'; import { queueApi } from 'services/api/endpoints/queue'; import { BatchConfig } from 'services/api/types'; import { startAppListening } from '..'; @@ -13,9 +9,7 @@ export const addEnqueueRequestedNodes = () => { predicate: (action): action is ReturnType => enqueueRequested.match(action) && action.payload.tabName === 'nodes', effect: async (action, { getState, dispatch }) => { - const log = logger('queue'); const state = getState(); - const { prepend } = action.payload; const graph = buildNodesGraph(state.nodes); const batchConfig: BatchConfig = { batch: { @@ -25,38 +19,12 @@ export const addEnqueueRequestedNodes = () => { prepend: action.payload.prepend, }; - try { - const req = dispatch( - queueApi.endpoints.enqueueBatch.initiate(batchConfig, { - fixedCacheKey: 'enqueueBatch', - }) - ); - const enqueueResult = await req.unwrap(); - req.reset(); - - log.debug({ enqueueResult: parseify(enqueueResult) }, 'Batch enqueued'); - dispatch( - addToast({ - title: t('queue.batchQueued'), - description: t('queue.batchQueuedDesc', { - item_count: enqueueResult.enqueued, - direction: prepend ? t('queue.front') : t('queue.back'), - }), - status: 'success', - }) - ); - } catch { - log.error( - { batchConfig: parseify(batchConfig) }, - 'Failed to enqueue batch' - ); - dispatch( - addToast({ - title: t('queue.batchFailedToQueue'), - status: 'error', - }) - ); - } + const req = dispatch( + queueApi.endpoints.enqueueBatch.initiate(batchConfig, { + fixedCacheKey: 'enqueueBatch', + }) + ); + req.reset(); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index e8b4aa9210..d38a20a917 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -4,7 +4,9 @@ import { parseify } from 'common/util/serialize'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { controlNetImageChanged, + controlNetIsEnabledChanged, ipAdapterImageChanged, + isIPAdapterEnabledChanged, } from 'features/controlNet/store/controlNetSlice'; import { TypesafeDraggableData, @@ -99,6 +101,12 @@ export const addImageDroppedListener = () => { controlNetId, }) ); + dispatch( + controlNetIsEnabledChanged({ + controlNetId, + isEnabled: true, + }) + ); return; } @@ -111,6 +119,7 @@ export const addImageDroppedListener = () => { activeData.payload.imageDTO ) { dispatch(ipAdapterImageChanged(activeData.payload.imageDTO)); + dispatch(isIPAdapterEnabledChanged(true)); return; } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index 1c7caaeb2f..b27c922342 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -3,7 +3,9 @@ import { logger } from 'app/logging/logger'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { controlNetImageChanged, + controlNetIsEnabledChanged, ipAdapterImageChanged, + isIPAdapterEnabledChanged, } from 'features/controlNet/store/controlNetSlice'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { initialImageChanged } from 'features/parameters/store/generationSlice'; @@ -87,6 +89,12 @@ export const addImageUploadedFulfilledListener = () => { if (postUploadAction?.type === 'SET_CONTROLNET_IMAGE') { const { controlNetId } = postUploadAction; + dispatch( + controlNetIsEnabledChanged({ + controlNetId, + isEnabled: true, + }) + ); dispatch( controlNetImageChanged({ controlNetId, @@ -104,6 +112,7 @@ export const addImageUploadedFulfilledListener = () => { if (postUploadAction?.type === 'SET_IP_ADAPTER_IMAGE') { dispatch(ipAdapterImageChanged(imageDTO)); + dispatch(isIPAdapterEnabledChanged(true)); dispatch( addToast({ ...DEFAULT_UPLOADED_TOAST, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionCanceled.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionCanceled.ts deleted file mode 100644 index 2592437348..0000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionCanceled.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { logger } from 'app/logging/logger'; -import { serializeError } from 'serialize-error'; -import { sessionCanceled } from 'services/api/thunks/session'; -import { startAppListening } from '..'; - -export const addSessionCanceledPendingListener = () => { - startAppListening({ - actionCreator: sessionCanceled.pending, - effect: () => { - // - }, - }); -}; - -export const addSessionCanceledFulfilledListener = () => { - startAppListening({ - actionCreator: sessionCanceled.fulfilled, - effect: (action) => { - const log = logger('session'); - const { session_id } = action.meta.arg; - log.debug({ session_id }, `Session canceled (${session_id})`); - }, - }); -}; - -export const addSessionCanceledRejectedListener = () => { - startAppListening({ - actionCreator: sessionCanceled.rejected, - effect: (action) => { - const log = logger('session'); - const { session_id } = action.meta.arg; - if (action.payload) { - const { error } = action.payload; - log.error( - { - session_id, - error: serializeError(error), - }, - `Problem canceling session` - ); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionCreated.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionCreated.ts deleted file mode 100644 index e89acb7542..0000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionCreated.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { logger } from 'app/logging/logger'; -import { parseify } from 'common/util/serialize'; -import { serializeError } from 'serialize-error'; -import { sessionCreated } from 'services/api/thunks/session'; -import { startAppListening } from '..'; - -export const addSessionCreatedPendingListener = () => { - startAppListening({ - actionCreator: sessionCreated.pending, - effect: () => { - // - }, - }); -}; - -export const addSessionCreatedFulfilledListener = () => { - startAppListening({ - actionCreator: sessionCreated.fulfilled, - effect: (action) => { - const log = logger('session'); - const session = action.payload; - log.debug( - { session: parseify(session) }, - `Session created (${session.id})` - ); - }, - }); -}; - -export const addSessionCreatedRejectedListener = () => { - startAppListening({ - actionCreator: sessionCreated.rejected, - effect: (action) => { - const log = logger('session'); - if (action.payload) { - const { error, status } = action.payload; - const graph = parseify(action.meta.arg); - log.error( - { graph, status, error: serializeError(error) }, - `Problem creating session` - ); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionInvoked.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionInvoked.ts deleted file mode 100644 index a62f75d957..0000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionInvoked.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { logger } from 'app/logging/logger'; -import { serializeError } from 'serialize-error'; -import { sessionInvoked } from 'services/api/thunks/session'; -import { startAppListening } from '..'; - -export const addSessionInvokedPendingListener = () => { - startAppListening({ - actionCreator: sessionInvoked.pending, - effect: () => { - // - }, - }); -}; - -export const addSessionInvokedFulfilledListener = () => { - startAppListening({ - actionCreator: sessionInvoked.fulfilled, - effect: (action) => { - const log = logger('session'); - const { session_id } = action.meta.arg; - log.debug({ session_id }, `Session invoked (${session_id})`); - }, - }); -}; - -export const addSessionInvokedRejectedListener = () => { - startAppListening({ - actionCreator: sessionInvoked.rejected, - effect: (action) => { - const log = logger('session'); - const { session_id } = action.meta.arg; - if (action.payload) { - const { error } = action.payload; - log.error( - { - session_id, - error: serializeError(error), - }, - `Problem invoking session` - ); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts index b6d8acc82e..beaa4835b3 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts @@ -17,7 +17,8 @@ import { } from 'services/events/actions'; import { startAppListening } from '../..'; -const nodeDenylist = ['load_image']; +// These nodes output an image, but do not actually *save* an image, so we don't want to handle the gallery logic on them +const nodeDenylist = ['load_image', 'image']; export const addInvocationCompleteEventListener = () => { startAppListening({ @@ -37,6 +38,7 @@ export const addInvocationCompleteEventListener = () => { const { image_name } = result.image; const { canvas, gallery } = getState(); + // This populates the `getImageDTO` cache const imageDTO = await dispatch( imagesApi.endpoints.getImageDTO.initiate(image_name) ).unwrap(); @@ -52,54 +54,59 @@ export const addInvocationCompleteEventListener = () => { if (!imageDTO.is_intermediate) { /** * Cache updates for when an image result is received - * - *add* to getImageDTO - * - IF `autoAddBoardId` is set: - * - THEN add it to the board_id/images - * - ELSE (`autoAddBoardId` is not set): - * - THEN add it to the no_board/images + * - add it to the no_board/images */ - const { autoAddBoardId } = gallery; - if (autoAddBoardId && autoAddBoardId !== 'none') { - dispatch( - imagesApi.endpoints.addImageToBoard.initiate({ - board_id: autoAddBoardId, - imageDTO, - }) - ); - } else { - dispatch( - imagesApi.util.updateQueryData( - 'listImages', - { - board_id: 'none', - categories: IMAGE_CATEGORIES, - }, - (draft) => { - imagesAdapter.addOne(draft, imageDTO); - } - ) - ); - } + dispatch( + imagesApi.util.updateQueryData( + 'listImages', + { + board_id: imageDTO.board_id ?? 'none', + categories: IMAGE_CATEGORIES, + }, + (draft) => { + imagesAdapter.addOne(draft, imageDTO); + } + ) + ); dispatch( imagesApi.util.invalidateTags([ - { type: 'BoardImagesTotal', id: autoAddBoardId }, - { type: 'BoardAssetsTotal', id: autoAddBoardId }, + { type: 'BoardImagesTotal', id: imageDTO.board_id }, + { type: 'BoardAssetsTotal', id: imageDTO.board_id }, ]) ); - const { selectedBoardId, shouldAutoSwitch } = gallery; + const { shouldAutoSwitch } = gallery; // If auto-switch is enabled, select the new image if (shouldAutoSwitch) { - // if auto-add is enabled, switch the board as the image comes in - if (autoAddBoardId && autoAddBoardId !== selectedBoardId) { - dispatch(boardIdSelected(autoAddBoardId)); - dispatch(galleryViewChanged('images')); - } else if (!autoAddBoardId) { + // if auto-add is enabled, switch the gallery view and board if needed as the image comes in + if (gallery.galleryView !== 'images') { dispatch(galleryViewChanged('images')); } + + if ( + imageDTO.board_id && + imageDTO.board_id !== gallery.selectedBoardId + ) { + dispatch( + boardIdSelected({ + boardId: imageDTO.board_id, + selectedImageName: imageDTO.image_name, + }) + ); + } + + if (!imageDTO.board_id && gallery.selectedBoardId !== 'none') { + dispatch( + boardIdSelected({ + boardId: 'none', + selectedImageName: imageDTO.image_name, + }) + ); + } + dispatch(imageSelected(imageDTO)); } } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.ts index b0377e950b..4af35dbe9c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.ts @@ -35,6 +35,7 @@ export const addSocketQueueItemStatusChangedEventListener = () => { queueApi.util.invalidateTags([ 'CurrentSessionQueueItem', 'NextSessionQueueItem', + 'InvocationCacheStatus', { type: 'SessionQueueItem', id: item_id }, { type: 'SessionQueueItemDTO', id: item_id }, { type: 'BatchStatus', id: queue_batch_id }, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/upscaleRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/upscaleRequested.ts index 61ecc61a03..b54d8b553c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/upscaleRequested.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/upscaleRequested.ts @@ -18,11 +18,14 @@ export const addUpscaleRequestedListener = () => { const log = logger('session'); const { image_name } = action.payload; - const { esrganModelName } = getState().postprocessing; + const state = getState(); + const { esrganModelName } = state.postprocessing; + const { autoAddBoardId } = state.gallery; const graph = buildAdHocUpscaleGraph({ image_name, esrganModelName, + autoAddBoardId, }); try { diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/util/enqueueBatch.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/util/enqueueBatch.ts deleted file mode 100644 index 1d5a1232c8..0000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/util/enqueueBatch.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { logger } from 'app/logging/logger'; -import { AppThunkDispatch } from 'app/store/store'; -import { parseify } from 'common/util/serialize'; -import { addToast } from 'features/system/store/systemSlice'; -import { t } from 'i18next'; -import { queueApi } from 'services/api/endpoints/queue'; -import { BatchConfig } from 'services/api/types'; - -export const enqueueBatch = async ( - batchConfig: BatchConfig, - dispatch: AppThunkDispatch -) => { - const log = logger('session'); - const { prepend } = batchConfig; - - try { - const req = dispatch( - queueApi.endpoints.enqueueBatch.initiate(batchConfig, { - fixedCacheKey: 'enqueueBatch', - }) - ); - const enqueueResult = await req.unwrap(); - req.reset(); - - dispatch( - queueApi.endpoints.resumeProcessor.initiate(undefined, { - fixedCacheKey: 'resumeProcessor', - }) - ); - - log.debug({ enqueueResult: parseify(enqueueResult) }, 'Batch enqueued'); - dispatch( - addToast({ - title: t('queue.batchQueued'), - description: t('queue.batchQueuedDesc', { - item_count: enqueueResult.enqueued, - direction: prepend ? t('queue.front') : t('queue.back'), - }), - status: 'success', - }) - ); - } catch { - log.error( - { batchConfig: parseify(batchConfig) }, - t('queue.batchFailedToQueue') - ); - dispatch( - addToast({ - title: t('queue.batchFailedToQueue'), - status: 'error', - }) - ); - } -}; diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index ca2e80535c..2cb2173b19 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -21,7 +21,8 @@ export type AppFeature = | 'multiselect' | 'pauseQueue' | 'resumeQueue' - | 'prependQueue'; + | 'prependQueue' + | 'invocationCache'; /** * A disable-able Stable Diffusion feature diff --git a/invokeai/frontend/web/src/common/components/IAIColorPicker.tsx b/invokeai/frontend/web/src/common/components/IAIColorPicker.tsx index f6a05c86b1..5854f7503f 100644 --- a/invokeai/frontend/web/src/common/components/IAIColorPicker.tsx +++ b/invokeai/frontend/web/src/common/components/IAIColorPicker.tsx @@ -1,18 +1,9 @@ -import { chakra, ChakraProps } from '@chakra-ui/react'; +import { Box, ChakraProps } from '@chakra-ui/react'; import { memo } from 'react'; import { RgbaColorPicker } from 'react-colorful'; import { ColorPickerBaseProps, RgbaColor } from 'react-colorful/dist/types'; -type IAIColorPickerProps = Omit, 'color'> & - ChakraProps & { - pickerColor: RgbaColor; - styleClass?: string; - }; - -const ChakraRgbaColorPicker = chakra(RgbaColorPicker, { - baseStyle: { paddingInline: 4 }, - shouldForwardProp: (prop) => !['pickerColor'].includes(prop), -}); +type IAIColorPickerProps = ColorPickerBaseProps; const colorPickerStyles: NonNullable = { width: 6, @@ -20,19 +11,17 @@ const colorPickerStyles: NonNullable = { borderColor: 'base.100', }; -const IAIColorPicker = (props: IAIColorPickerProps) => { - const { styleClass = '', ...rest } = props; +const sx = { + '.react-colorful__hue-pointer': colorPickerStyles, + '.react-colorful__saturation-pointer': colorPickerStyles, + '.react-colorful__alpha-pointer': colorPickerStyles, +}; +const IAIColorPicker = (props: IAIColorPickerProps) => { return ( - + + + ); }; diff --git a/invokeai/frontend/web/src/common/components/IAIDroppable.tsx b/invokeai/frontend/web/src/common/components/IAIDroppable.tsx index e4fb121c78..bf98961c21 100644 --- a/invokeai/frontend/web/src/common/components/IAIDroppable.tsx +++ b/invokeai/frontend/web/src/common/components/IAIDroppable.tsx @@ -31,7 +31,7 @@ const IAIDroppable = (props: IAIDroppableProps) => { insetInlineStart={0} w="full" h="full" - pointerEvents="none" + pointerEvents={active ? 'auto' : 'none'} > {isValidDrop(data, active) && ( diff --git a/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx index ca61ea847f..3c1a05d527 100644 --- a/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx +++ b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx @@ -81,3 +81,38 @@ export const IAINoContentFallback = (props: IAINoImageFallbackProps) => { ); }; + +type IAINoImageFallbackWithSpinnerProps = FlexProps & { + label?: string; +}; + +export const IAINoContentFallbackWithSpinner = ( + props: IAINoImageFallbackWithSpinnerProps +) => { + const { sx, ...rest } = props; + + return ( + + + {props.label && {props.label}} + + ); +}; diff --git a/invokeai/frontend/web/src/common/components/IAIInformationalPopover.tsx b/invokeai/frontend/web/src/common/components/IAIInformationalPopover.tsx deleted file mode 100644 index 876ce6488a..0000000000 --- a/invokeai/frontend/web/src/common/components/IAIInformationalPopover.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { - Button, - Popover, - PopoverTrigger, - PopoverContent, - PopoverArrow, - PopoverCloseButton, - PopoverHeader, - PopoverBody, - PopoverProps, - Flex, - Text, - Image, -} from '@chakra-ui/react'; -import { useAppSelector } from '../../app/store/storeHooks'; -import { useTranslation } from 'react-i18next'; - -interface Props extends PopoverProps { - details: string; - children: JSX.Element; - image?: string; - buttonLabel?: string; - buttonHref?: string; - placement?: PopoverProps['placement']; -} - -function IAIInformationalPopover({ - details, - image, - buttonLabel, - buttonHref, - children, - placement, -}: Props): JSX.Element { - const shouldEnableInformationalPopovers = useAppSelector( - (state) => state.system.shouldEnableInformationalPopovers - ); - const { t } = useTranslation(); - - const heading = t(`popovers.${details}.heading`); - const paragraph = t(`popovers.${details}.paragraph`); - - if (!shouldEnableInformationalPopovers) { - return children; - } else { - return ( - - -
{children}
-
- - - - - - - {image && ( - Optional Image - )} - - {heading && {heading}} - {paragraph} - {buttonLabel && ( - - - - )} - - - - -
- ); - } -} - -export default IAIInformationalPopover; diff --git a/invokeai/frontend/web/src/common/components/IAIInformationalPopover/IAIInformationalPopover.tsx b/invokeai/frontend/web/src/common/components/IAIInformationalPopover/IAIInformationalPopover.tsx new file mode 100644 index 0000000000..b58f8fc565 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/IAIInformationalPopover/IAIInformationalPopover.tsx @@ -0,0 +1,155 @@ +import { + Box, + BoxProps, + Button, + Divider, + Flex, + Heading, + Image, + Popover, + PopoverBody, + PopoverCloseButton, + PopoverContent, + PopoverProps, + PopoverTrigger, + Portal, + Text, + forwardRef, +} from '@chakra-ui/react'; +import { merge, omit } from 'lodash-es'; +import { PropsWithChildren, memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FaExternalLinkAlt } from 'react-icons/fa'; +import { useAppSelector } from '../../../app/store/storeHooks'; +import { + Feature, + OPEN_DELAY, + POPOVER_DATA, + POPPER_MODIFIERS, +} from './constants'; + +type Props = PropsWithChildren & { + feature: Feature; + wrapperProps?: BoxProps; + popoverProps?: PopoverProps; +}; + +const IAIInformationalPopover = forwardRef( + ({ feature, children, wrapperProps, ...rest }: Props, ref) => { + const { t } = useTranslation(); + const shouldEnableInformationalPopovers = useAppSelector( + (state) => state.system.shouldEnableInformationalPopovers + ); + + const data = useMemo(() => POPOVER_DATA[feature], [feature]); + + const popoverProps = useMemo( + () => merge(omit(data, ['image', 'href', 'buttonLabel']), rest), + [data, rest] + ); + + const heading = useMemo( + () => t(`popovers.${feature}.heading`), + [feature, t] + ); + + const paragraphs = useMemo( + () => + t(`popovers.${feature}.paragraphs`, { + returnObjects: true, + }) ?? [], + [feature, t] + ); + + const handleClick = useCallback(() => { + if (!data?.href) { + return; + } + window.open(data.href); + }, [data?.href]); + + if (!shouldEnableInformationalPopovers) { + return ( + + {children} + + ); + } + + return ( + + + + {children} + + + + + + + + {heading && ( + <> + {heading} + + + )} + {data?.image && ( + <> + Optional Image + + + )} + {paragraphs.map((p) => ( + {p} + ))} + {data?.href && ( + <> + + + + )} + + + + + + ); + } +); + +IAIInformationalPopover.displayName = 'IAIInformationalPopover'; + +export default memo(IAIInformationalPopover); diff --git a/invokeai/frontend/web/src/common/components/IAIInformationalPopover/constants.ts b/invokeai/frontend/web/src/common/components/IAIInformationalPopover/constants.ts new file mode 100644 index 0000000000..f2398483bf --- /dev/null +++ b/invokeai/frontend/web/src/common/components/IAIInformationalPopover/constants.ts @@ -0,0 +1,98 @@ +import { PopoverProps } from '@chakra-ui/react'; + +export type Feature = + | 'clipSkip' + | 'paramNegativeConditioning' + | 'paramPositiveConditioning' + | 'paramScheduler' + | 'compositingBlur' + | 'compositingBlurMethod' + | 'compositingCoherencePass' + | 'compositingCoherenceMode' + | 'compositingCoherenceSteps' + | 'compositingStrength' + | 'compositingMaskAdjustments' + | 'controlNetBeginEnd' + | 'controlNetControlMode' + | 'controlNetResizeMode' + | 'controlNet' + | 'controlNetWeight' + | 'dynamicPrompts' + | 'dynamicPromptsMaxPrompts' + | 'dynamicPromptsSeedBehaviour' + | 'infillMethod' + | 'lora' + | 'noiseUseCPU' + | 'paramCFGScale' + | 'paramDenoisingStrength' + | 'paramIterations' + | 'paramModel' + | 'paramRatio' + | 'paramSeed' + | 'paramSteps' + | 'paramVAE' + | 'paramVAEPrecision' + | 'scaleBeforeProcessing'; + +export type PopoverData = PopoverProps & { + image?: string; + href?: string; + buttonLabel?: string; +}; + +export const POPOVER_DATA: { [key in Feature]?: PopoverData } = { + paramNegativeConditioning: { + placement: 'right', + }, + controlNet: { + href: 'https://support.invoke.ai/support/solutions/articles/151000105880', + }, + lora: { + href: 'https://support.invoke.ai/support/solutions/articles/151000159072', + }, + compositingCoherenceMode: { + href: 'https://support.invoke.ai/support/solutions/articles/151000158838', + }, + infillMethod: { + href: 'https://support.invoke.ai/support/solutions/articles/151000158841', + }, + scaleBeforeProcessing: { + href: 'https://support.invoke.ai/support/solutions/articles/151000158841', + }, + paramIterations: { + href: 'https://support.invoke.ai/support/solutions/articles/151000159073', + }, + paramPositiveConditioning: { + href: 'https://support.invoke.ai/support/solutions/articles/151000096606-tips-on-crafting-prompts', + placement: 'right', + }, + paramScheduler: { + placement: 'right', + href: 'https://support.invoke.ai/support/solutions/articles/151000159073', + }, + paramModel: { + placement: 'right', + href: 'https://support.invoke.ai/support/solutions/articles/151000096601-what-is-a-model-which-should-i-use-', + }, + paramRatio: { + gutter: 16, + }, + controlNetControlMode: { + placement: 'right', + }, + controlNetResizeMode: { + placement: 'right', + }, + paramVAE: { + placement: 'right', + }, + paramVAEPrecision: { + placement: 'right', + }, +} as const; + +export const OPEN_DELAY = 1000; // in milliseconds + +export const POPPER_MODIFIERS: PopoverProps['modifiers'] = [ + { name: 'preventOverflow', options: { padding: 10 } }, +]; diff --git a/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx b/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx index 28c680b824..7c85b3557e 100644 --- a/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx +++ b/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx @@ -1,4 +1,4 @@ -import { FormControl, FormLabel, Tooltip } from '@chakra-ui/react'; +import { FormControl, FormLabel, Tooltip, forwardRef } from '@chakra-ui/react'; import { MultiSelect, MultiSelectProps } from '@mantine/core'; import { useAppDispatch } from 'app/store/storeHooks'; import { shiftKeyPressed } from 'features/ui/store/hotkeysSlice'; @@ -11,7 +11,7 @@ type IAIMultiSelectProps = Omit & { label?: string; }; -const IAIMantineMultiSelect = (props: IAIMultiSelectProps) => { +const IAIMantineMultiSelect = forwardRef((props: IAIMultiSelectProps, ref) => { const { searchable = true, tooltip, @@ -44,25 +44,23 @@ const IAIMantineMultiSelect = (props: IAIMultiSelectProps) => { return ( - - {label} - - ) : undefined - } - ref={inputRef} - disabled={disabled} - onKeyDown={handleKeyDown} - onKeyUp={handleKeyUp} - searchable={searchable} - maxDropdownHeight={300} - styles={styles} - {...rest} - /> + + {label && {label}} + + ); -}; +}); + +IAIMantineMultiSelect.displayName = 'IAIMantineMultiSelect'; export default memo(IAIMantineMultiSelect); diff --git a/invokeai/frontend/web/src/common/components/IAIMantineSearchableSelect.tsx b/invokeai/frontend/web/src/common/components/IAIMantineSearchableSelect.tsx index 079421d4e5..ee6cd86170 100644 --- a/invokeai/frontend/web/src/common/components/IAIMantineSearchableSelect.tsx +++ b/invokeai/frontend/web/src/common/components/IAIMantineSearchableSelect.tsx @@ -1,4 +1,4 @@ -import { FormControl, FormLabel, Tooltip } from '@chakra-ui/react'; +import { FormControl, FormLabel, Tooltip, forwardRef } from '@chakra-ui/react'; import { Select, SelectProps } from '@mantine/core'; import { useAppDispatch } from 'app/store/storeHooks'; import { shiftKeyPressed } from 'features/ui/store/hotkeysSlice'; @@ -17,7 +17,7 @@ type IAISelectProps = Omit & { inputRef?: RefObject; }; -const IAIMantineSearchableSelect = (props: IAISelectProps) => { +const IAIMantineSearchableSelect = forwardRef((props: IAISelectProps, ref) => { const { searchable = true, tooltip, @@ -70,28 +70,26 @@ const IAIMantineSearchableSelect = (props: IAISelectProps) => { return ( - + ); -}; +}); + +IAIMantineSearchableSelect.displayName = 'IAIMantineSearchableSelect'; export default memo(IAIMantineSearchableSelect); diff --git a/invokeai/frontend/web/src/common/components/IAIMantineSelect.tsx b/invokeai/frontend/web/src/common/components/IAIMantineSelect.tsx index 46b5fc95f6..c1150c2c9e 100644 --- a/invokeai/frontend/web/src/common/components/IAIMantineSelect.tsx +++ b/invokeai/frontend/web/src/common/components/IAIMantineSelect.tsx @@ -1,4 +1,4 @@ -import { FormControl, FormLabel, Tooltip } from '@chakra-ui/react'; +import { FormControl, FormLabel, Tooltip, forwardRef } from '@chakra-ui/react'; import { Select, SelectProps } from '@mantine/core'; import { useMantineSelectStyles } from 'mantine-theme/hooks/useMantineSelectStyles'; import { RefObject, memo } from 'react'; @@ -15,28 +15,26 @@ export type IAISelectProps = Omit & { label?: string; }; -const IAIMantineSelect = (props: IAISelectProps) => { +const IAIMantineSelect = forwardRef((props: IAISelectProps, ref) => { const { tooltip, inputRef, label, disabled, required, ...rest } = props; const styles = useMantineSelectStyles(); return ( - + ); -}; +}); + +IAIMantineSelect.displayName = 'IAIMantineSelect'; export default memo(IAIMantineSelect); diff --git a/invokeai/frontend/web/src/common/components/IAINumberInput.tsx b/invokeai/frontend/web/src/common/components/IAINumberInput.tsx index de3b44564a..243dac9d63 100644 --- a/invokeai/frontend/web/src/common/components/IAINumberInput.tsx +++ b/invokeai/frontend/web/src/common/components/IAINumberInput.tsx @@ -13,6 +13,7 @@ import { NumberInputStepperProps, Tooltip, TooltipProps, + forwardRef, } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; import { stopPastePropagation } from 'common/util/stopPastePropagation'; @@ -50,7 +51,7 @@ interface Props extends Omit { /** * Customized Chakra FormControl + NumberInput multi-part component. */ -const IAINumberInput = (props: Props) => { +const IAINumberInput = forwardRef((props: Props, ref) => { const { label, isDisabled = false, @@ -141,6 +142,7 @@ const IAINumberInput = (props: Props) => { return ( { ); -}; +}); + +IAINumberInput.displayName = 'IAINumberInput'; export default memo(IAINumberInput); diff --git a/invokeai/frontend/web/src/common/components/IAISlider.tsx b/invokeai/frontend/web/src/common/components/IAISlider.tsx index fd3eed754f..e879c00977 100644 --- a/invokeai/frontend/web/src/common/components/IAISlider.tsx +++ b/invokeai/frontend/web/src/common/components/IAISlider.tsx @@ -22,6 +22,7 @@ import { SliderTrackProps, Tooltip, TooltipProps, + forwardRef, } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; @@ -71,7 +72,7 @@ export type IAIFullSliderProps = { sliderIAIIconButtonProps?: IAIIconButtonProps; }; -const IAISlider = (props: IAIFullSliderProps) => { +const IAISlider = forwardRef((props: IAIFullSliderProps, ref) => { const [showTooltip, setShowTooltip] = useState(false); const { label, @@ -187,6 +188,7 @@ const IAISlider = (props: IAIFullSliderProps) => { return ( { ); -}; +}); + +IAISlider.displayName = 'IAISlider'; export default memo(IAISlider); diff --git a/invokeai/frontend/web/src/common/components/IAISwitch.tsx b/invokeai/frontend/web/src/common/components/IAISwitch.tsx index da0883d77e..8773be49e5 100644 --- a/invokeai/frontend/web/src/common/components/IAISwitch.tsx +++ b/invokeai/frontend/web/src/common/components/IAISwitch.tsx @@ -72,4 +72,6 @@ const IAISwitch = (props: IAISwitchProps) => { ); }; +IAISwitch.displayName = 'IAISwitch'; + export default memo(IAISwitch); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx index e2f20f99a2..360d764a6e 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx @@ -139,6 +139,11 @@ const IAICanvas = () => { const { handleDragStart, handleDragMove, handleDragEnd } = useCanvasDragMove(); + const handleContextMenu = useCallback( + (e: KonvaEventObject) => e.evt.preventDefault(), + [] + ); + useEffect(() => { if (!containerRef.current) { return; @@ -205,9 +210,7 @@ const IAICanvas = () => { onDragStart={handleDragStart} onDragMove={handleDragMove} onDragEnd={handleDragEnd} - onContextMenu={(e: KonvaEventObject) => - e.evt.preventDefault() - } + onContextMenu={handleContextMenu} onWheel={handleWheel} draggable={(tool === 'move' || isStaging) && !isModifyingBoundingBox} > @@ -223,7 +226,11 @@ const IAICanvas = () => { > - + diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx index 9f8829c280..d87d912a1e 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx @@ -1,26 +1,27 @@ import { skipToken } from '@reduxjs/toolkit/dist/query'; -import { Image, Rect } from 'react-konva'; +import { memo } from 'react'; +import { Image } from 'react-konva'; +import { $authToken } from 'services/api/client'; import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import useImage from 'use-image'; import { CanvasImage } from '../store/canvasTypes'; -import { $authToken } from 'services/api/client'; -import { memo } from 'react'; +import IAICanvasImageErrorFallback from './IAICanvasImageErrorFallback'; type IAICanvasImageProps = { canvasImage: CanvasImage; }; const IAICanvasImage = (props: IAICanvasImageProps) => { - const { width, height, x, y, imageName } = props.canvasImage; + const { x, y, imageName } = props.canvasImage; const { currentData: imageDTO, isError } = useGetImageDTOQuery( imageName ?? skipToken ); - const [image] = useImage( + const [image, status] = useImage( imageDTO?.image_url ?? '', $authToken.get() ? 'use-credentials' : 'anonymous' ); - if (isError) { - return ; + if (isError || status === 'failed') { + return ; } return ; diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImageErrorFallback.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasImageErrorFallback.tsx new file mode 100644 index 0000000000..38322daafa --- /dev/null +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasImageErrorFallback.tsx @@ -0,0 +1,44 @@ +import { useColorModeValue, useToken } from '@chakra-ui/react'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Group, Rect, Text } from 'react-konva'; +import { CanvasImage } from '../store/canvasTypes'; + +type IAICanvasImageErrorFallbackProps = { + canvasImage: CanvasImage; +}; +const IAICanvasImageErrorFallback = ({ + canvasImage, +}: IAICanvasImageErrorFallbackProps) => { + const [errorColorLight, errorColorDark, fontColorLight, fontColorDark] = + useToken('colors', ['base.400', 'base.500', 'base.700', 'base.900']); + const errorColor = useColorModeValue(errorColorLight, errorColorDark); + const fontColor = useColorModeValue(fontColorLight, fontColorDark); + const { t } = useTranslation(); + return ( + + + + + ); +}; + +export default memo(IAICanvasImageErrorFallback); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx index fa73f020da..4585ab76af 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx @@ -3,10 +3,9 @@ import { useAppSelector } from 'app/store/storeHooks'; import { canvasSelector } from 'features/canvas/store/canvasSelectors'; import { GroupConfig } from 'konva/lib/Group'; import { isEqual } from 'lodash-es'; - +import { memo } from 'react'; import { Group, Rect } from 'react-konva'; import IAICanvasImage from './IAICanvasImage'; -import { memo } from 'react'; const selector = createSelector( [canvasSelector], @@ -15,11 +14,11 @@ const selector = createSelector( layerState, shouldShowStagingImage, shouldShowStagingOutline, - boundingBoxCoordinates: { x, y }, - boundingBoxDimensions: { width, height }, + boundingBoxCoordinates: stageBoundingBoxCoordinates, + boundingBoxDimensions: stageBoundingBoxDimensions, } = canvas; - const { selectedImageIndex, images } = layerState.stagingArea; + const { selectedImageIndex, images, boundingBox } = layerState.stagingArea; return { currentStagingAreaImage: @@ -30,10 +29,10 @@ const selector = createSelector( isOnLastImage: selectedImageIndex === images.length - 1, shouldShowStagingImage, shouldShowStagingOutline, - x, - y, - width, - height, + x: boundingBox?.x ?? stageBoundingBoxCoordinates.x, + y: boundingBox?.y ?? stageBoundingBoxCoordinates.y, + width: boundingBox?.width ?? stageBoundingBoxDimensions.width, + height: boundingBox?.height ?? stageBoundingBoxDimensions.height, }; }, { diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx index 3e617f8767..8bb45840d0 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx @@ -14,6 +14,7 @@ import { import { skipToken } from '@reduxjs/toolkit/dist/query'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import IAIButton from 'common/components/IAIButton'; import { memo, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; @@ -23,8 +24,8 @@ import { FaCheck, FaEye, FaEyeSlash, - FaPlus, FaSave, + FaTimes, } from 'react-icons/fa'; import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import { stagingAreaImageSaved } from '../store/actions'; @@ -41,10 +42,10 @@ const selector = createSelector( } = canvas; return { + currentIndex: selectedImageIndex, + total: images.length, currentStagingAreaImage: images.length > 0 ? images[selectedImageIndex] : undefined, - isOnFirstImage: selectedImageIndex === 0, - isOnLastImage: selectedImageIndex === images.length - 1, shouldShowStagingImage, shouldShowStagingOutline, }; @@ -55,10 +56,10 @@ const selector = createSelector( const IAICanvasStagingAreaToolbar = () => { const dispatch = useAppDispatch(); const { - isOnFirstImage, - isOnLastImage, currentStagingAreaImage, shouldShowStagingImage, + currentIndex, + total, } = useAppSelector(selector); const { t } = useTranslation(); @@ -71,39 +72,6 @@ const IAICanvasStagingAreaToolbar = () => { dispatch(setShouldShowStagingOutline(false)); }, [dispatch]); - useHotkeys( - ['left'], - () => { - handlePrevImage(); - }, - { - enabled: () => true, - preventDefault: true, - } - ); - - useHotkeys( - ['right'], - () => { - handleNextImage(); - }, - { - enabled: () => true, - preventDefault: true, - } - ); - - useHotkeys( - ['enter'], - () => { - handleAccept(); - }, - { - enabled: () => true, - preventDefault: true, - } - ); - const handlePrevImage = useCallback( () => dispatch(prevStagingAreaImage()), [dispatch] @@ -119,10 +87,45 @@ const IAICanvasStagingAreaToolbar = () => { [dispatch] ); + useHotkeys(['left'], handlePrevImage, { + enabled: () => true, + preventDefault: true, + }); + + useHotkeys(['right'], handleNextImage, { + enabled: () => true, + preventDefault: true, + }); + + useHotkeys(['enter'], () => handleAccept, { + enabled: () => true, + preventDefault: true, + }); + const { data: imageDTO } = useGetImageDTOQuery( currentStagingAreaImage?.imageName ?? skipToken ); + const handleToggleShouldShowStagingImage = useCallback(() => { + dispatch(setShouldShowStagingImage(!shouldShowStagingImage)); + }, [dispatch, shouldShowStagingImage]); + + const handleSaveToGallery = useCallback(() => { + if (!imageDTO) { + return; + } + + dispatch( + stagingAreaImageSaved({ + imageDTO, + }) + ); + }, [dispatch, imageDTO]); + + const handleDiscardStagingArea = useCallback(() => { + dispatch(discardStagedImages()); + }, [dispatch]); + if (!currentStagingAreaImage) { return null; } @@ -131,11 +134,12 @@ const IAICanvasStagingAreaToolbar = () => { { icon={} onClick={handlePrevImage} colorScheme="accent" - isDisabled={isOnFirstImage} + isDisabled={!shouldShowStagingImage} /> + {`${currentIndex + 1}/${total}`} } onClick={handleNextImage} colorScheme="accent" - isDisabled={isOnLastImage} + isDisabled={!shouldShowStagingImage} /> + + { colorScheme="accent" /> : } - onClick={() => - dispatch(setShouldShowStagingImage(!shouldShowStagingImage)) - } + onClick={handleToggleShouldShowStagingImage} colorScheme="accent" /> { aria-label={t('unifiedCanvas.saveToGallery')} isDisabled={!imageDTO || !imageDTO.is_intermediate} icon={} - onClick={() => { - if (!imageDTO) { - return; - } - - dispatch( - stagingAreaImageSaved({ - imageDTO, - }) - ); - }} + onClick={handleSaveToGallery} colorScheme="accent" /> } - onClick={() => dispatch(discardStagedImages())} + icon={} + onClick={handleDiscardStagingArea} colorScheme="error" fontSize={20} /> diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx index 0f94b1c57a..8f86605726 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx @@ -213,45 +213,45 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => { [scaledStep] ); - const handleStartedTransforming = () => { + const handleStartedTransforming = useCallback(() => { dispatch(setIsTransformingBoundingBox(true)); - }; + }, [dispatch]); - const handleEndedTransforming = () => { + const handleEndedTransforming = useCallback(() => { dispatch(setIsTransformingBoundingBox(false)); dispatch(setIsMovingBoundingBox(false)); dispatch(setIsMouseOverBoundingBox(false)); setIsMouseOverBoundingBoxOutline(false); - }; + }, [dispatch]); - const handleStartedMoving = () => { + const handleStartedMoving = useCallback(() => { dispatch(setIsMovingBoundingBox(true)); - }; + }, [dispatch]); - const handleEndedModifying = () => { + const handleEndedModifying = useCallback(() => { dispatch(setIsTransformingBoundingBox(false)); dispatch(setIsMovingBoundingBox(false)); dispatch(setIsMouseOverBoundingBox(false)); setIsMouseOverBoundingBoxOutline(false); - }; + }, [dispatch]); - const handleMouseOver = () => { + const handleMouseOver = useCallback(() => { setIsMouseOverBoundingBoxOutline(true); - }; + }, []); - const handleMouseOut = () => { + const handleMouseOut = useCallback(() => { !isTransformingBoundingBox && !isMovingBoundingBox && setIsMouseOverBoundingBoxOutline(false); - }; + }, [isMovingBoundingBox, isTransformingBoundingBox]); - const handleMouseEnterBoundingBox = () => { + const handleMouseEnterBoundingBox = useCallback(() => { dispatch(setIsMouseOverBoundingBox(true)); - }; + }, [dispatch]); - const handleMouseLeaveBoundingBox = () => { + const handleMouseLeaveBoundingBox = useCallback(() => { dispatch(setIsMouseOverBoundingBox(false)); - }; + }, [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx index 76211a2e95..43e8febd66 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx @@ -1,4 +1,4 @@ -import { ButtonGroup, Flex } from '@chakra-ui/react'; +import { Box, ButtonGroup, Flex } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIButton from 'common/components/IAIButton'; @@ -135,11 +135,12 @@ const IAICanvasMaskOptions = () => { dispatch(setShouldPreserveMaskedArea(e.target.checked)) } /> - dispatch(setMaskColor(newColor))} - /> + + dispatch(setMaskColor(newColor))} + /> + } onClick={handleSaveMask}> Save Mask diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx index 6a7db0e5f2..b5770fdda6 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx @@ -1,4 +1,4 @@ -import { ButtonGroup, Flex } from '@chakra-ui/react'; +import { ButtonGroup, Flex, Box } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; @@ -237,15 +237,18 @@ const IAICanvasToolChooserOptions = () => { sliderNumberInputProps={{ max: 500 }} /> - dispatch(setBrushColor(newColor))} - /> + > + dispatch(setBrushColor(newColor))} + /> + diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSelectors.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSelectors.ts index 46bf7db3d0..8f1e246aaa 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSelectors.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSelectors.ts @@ -6,7 +6,7 @@ export const canvasSelector = (state: RootState): CanvasState => state.canvas; export const isStagingSelector = createSelector( [stateSelector], - ({ canvas }) => canvas.layerState.stagingArea.images.length > 0 + ({ canvas }) => canvas.batchIds.length > 0 ); export const initialCanvasImageSelector = ( diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index b726e757f6..df601e9e67 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -8,7 +8,6 @@ import { setAspectRatio } from 'features/parameters/store/generationSlice'; import { IRect, Vector2d } from 'konva/lib/types'; import { clamp, cloneDeep } from 'lodash-es'; import { RgbaColor } from 'react-colorful'; -import { sessionCanceled } from 'services/api/thunks/session'; import { ImageDTO } from 'services/api/types'; import calculateCoordinates from '../util/calculateCoordinates'; import calculateScale from '../util/calculateScale'; @@ -187,7 +186,7 @@ export const canvasSlice = createSlice({ state.pastLayerStates.push(cloneDeep(state.layerState)); state.layerState = { - ...initialLayerState, + ...cloneDeep(initialLayerState), objects: [ { kind: 'image', @@ -201,6 +200,7 @@ export const canvasSlice = createSlice({ ], }; state.futureLayerStates = []; + state.batchIds = []; const newScale = calculateScale( stageDimensions.width, @@ -350,11 +350,14 @@ export const canvasSlice = createSlice({ state.pastLayerStates.shift(); } - state.layerState.stagingArea = { ...initialLayerState.stagingArea }; + state.layerState.stagingArea = cloneDeep( + cloneDeep(initialLayerState) + ).stagingArea; state.futureLayerStates = []; state.shouldShowStagingOutline = true; - state.shouldShowStagingOutline = true; + state.shouldShowStagingImage = true; + state.batchIds = []; }, addFillRect: (state) => { const { boundingBoxCoordinates, boundingBoxDimensions, brushColor } = @@ -491,8 +494,9 @@ export const canvasSlice = createSlice({ resetCanvas: (state) => { state.pastLayerStates.push(cloneDeep(state.layerState)); - state.layerState = initialLayerState; + state.layerState = cloneDeep(initialLayerState); state.futureLayerStates = []; + state.batchIds = []; }, canvasResized: ( state, @@ -617,25 +621,22 @@ export const canvasSlice = createSlice({ return; } - const currentIndex = state.layerState.stagingArea.selectedImageIndex; - const length = state.layerState.stagingArea.images.length; + const nextIndex = state.layerState.stagingArea.selectedImageIndex + 1; + const lastIndex = state.layerState.stagingArea.images.length - 1; - state.layerState.stagingArea.selectedImageIndex = Math.min( - currentIndex + 1, - length - 1 - ); + state.layerState.stagingArea.selectedImageIndex = + nextIndex > lastIndex ? 0 : nextIndex; }, prevStagingAreaImage: (state) => { if (!state.layerState.stagingArea.images.length) { return; } - const currentIndex = state.layerState.stagingArea.selectedImageIndex; + const prevIndex = state.layerState.stagingArea.selectedImageIndex - 1; + const lastIndex = state.layerState.stagingArea.images.length - 1; - state.layerState.stagingArea.selectedImageIndex = Math.max( - currentIndex - 1, - 0 - ); + state.layerState.stagingArea.selectedImageIndex = + prevIndex < 0 ? lastIndex : prevIndex; }, commitStagingAreaImage: (state) => { if (!state.layerState.stagingArea.images.length) { @@ -657,13 +658,12 @@ export const canvasSlice = createSlice({ ...imageToCommit, }); } - state.layerState.stagingArea = { - ...initialLayerState.stagingArea, - }; + state.layerState.stagingArea = cloneDeep(initialLayerState).stagingArea; state.futureLayerStates = []; state.shouldShowStagingOutline = true; state.shouldShowStagingImage = true; + state.batchIds = []; }, fitBoundingBoxToStage: (state) => { const { @@ -786,11 +786,6 @@ export const canvasSlice = createSlice({ }, }, extraReducers: (builder) => { - builder.addCase(sessionCanceled.pending, (state) => { - if (!state.layerState.stagingArea.images.length) { - state.layerState.stagingArea = initialLayerState.stagingArea; - } - }); builder.addCase(setAspectRatio, (state, action) => { const ratio = action.payload; if (ratio) { diff --git a/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts b/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts index 20ac482710..b67789e07e 100644 --- a/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts +++ b/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts @@ -1,15 +1,18 @@ -import { getCanvasBaseLayer } from './konvaInstanceProvider'; import { RootState } from 'app/store/store'; +import { getCanvasBaseLayer } from './konvaInstanceProvider'; import { konvaNodeToBlob } from './konvaNodeToBlob'; /** * Get the canvas base layer blob, with or without bounding box according to `shouldCropToBoundingBoxOnSave` */ -export const getBaseLayerBlob = async (state: RootState) => { +export const getBaseLayerBlob = async ( + state: RootState, + alwaysUseBoundingBox: boolean = false +) => { const canvasBaseLayer = getCanvasBaseLayer(); if (!canvasBaseLayer) { - return; + throw new Error('Problem getting base layer blob'); } const { @@ -24,14 +27,15 @@ export const getBaseLayerBlob = async (state: RootState) => { const absPos = clonedBaseLayer.getAbsolutePosition(); - const boundingBox = shouldCropToBoundingBoxOnSave - ? { - x: boundingBoxCoordinates.x + absPos.x, - y: boundingBoxCoordinates.y + absPos.y, - width: boundingBoxDimensions.width, - height: boundingBoxDimensions.height, - } - : clonedBaseLayer.getClientRect(); + const boundingBox = + shouldCropToBoundingBoxOnSave || alwaysUseBoundingBox + ? { + x: boundingBoxCoordinates.x + absPos.x, + y: boundingBoxCoordinates.y + absPos.y, + width: boundingBoxDimensions.width, + height: boundingBoxDimensions.height, + } + : clonedBaseLayer.getClientRect(); return konvaNodeToBlob(clonedBaseLayer, boundingBox); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx index 13ceaf4173..3d00359d18 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx @@ -1,12 +1,12 @@ import { Box, Flex } from '@chakra-ui/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { memo, useCallback } from 'react'; +import { ChangeEvent, memo, useCallback } from 'react'; import { FaCopy, FaTrash } from 'react-icons/fa'; import { ControlNetConfig, controlNetDuplicated, controlNetRemoved, - controlNetToggled, + controlNetIsEnabledChanged, } from '../store/controlNetSlice'; import ParamControlNetModel from './parameters/ParamControlNetModel'; import ParamControlNetWeight from './parameters/ParamControlNetWeight'; @@ -77,9 +77,17 @@ const ControlNet = (props: ControlNetProps) => { ); }, [controlNetId, dispatch]); - const handleToggleIsEnabled = useCallback(() => { - dispatch(controlNetToggled({ controlNetId })); - }, [controlNetId, dispatch]); + const handleToggleIsEnabled = useCallback( + (e: ChangeEvent) => { + dispatch( + controlNetIsEnabledChanged({ + controlNetId, + isEnabled: e.target.checked, + }) + ); + }, + [controlNetId, dispatch] + ); return ( { sx={{ w: 'full', minW: 0, - opacity: isEnabled ? 1 : 0.5, - pointerEvents: isEnabled ? 'auto' : 'none', + // opacity: isEnabled ? 1 : 0.5, + // pointerEvents: isEnabled ? 'auto' : 'none', transitionProperty: 'common', transitionDuration: '0.1s', }} diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx index 02c65c1c83..0b1e0dab87 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx @@ -13,6 +13,7 @@ import { import { setHeight, setWidth } from 'features/parameters/store/generationSlice'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { memo, useCallback, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { FaRulerVertical, FaSave, FaUndo } from 'react-icons/fa'; import { useAddImageToBoardMutation, @@ -26,7 +27,6 @@ import { ControlNetConfig, controlNetImageChanged, } from '../store/controlNetSlice'; -import { useTranslation } from 'react-i18next'; type Props = { controlNet: ControlNetConfig; @@ -52,7 +52,6 @@ const ControlNetImagePreview = ({ isSmall, controlNet }: Props) => { controlImage: controlImageName, processedControlImage: processedControlImageName, processorType, - isEnabled, controlNetId, } = controlNet; @@ -172,15 +171,13 @@ const ControlNetImagePreview = ({ isSmall, controlNet }: Props) => { h: isSmall ? 28 : 366, // magic no touch alignItems: 'center', justifyContent: 'center', - pointerEvents: isEnabled ? 'auto' : 'none', - opacity: isEnabled ? 1 : 0.5, }} > @@ -202,7 +199,6 @@ const ControlNetImagePreview = ({ isSmall, controlNet }: Props) => { droppableData={droppableData} imageDTO={processedControlImage} isUploadDisabled={true} - isDropDisabled={!isEnabled} /> diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorComponent.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorComponent.tsx index 681838ef27..29aaae4e70 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorComponent.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorComponent.tsx @@ -1,6 +1,7 @@ import { memo } from 'react'; import { ControlNetConfig } from '../store/controlNetSlice'; import CannyProcessor from './processors/CannyProcessor'; +import ColorMapProcessor from './processors/ColorMapProcessor'; import ContentShuffleProcessor from './processors/ContentShuffleProcessor'; import HedProcessor from './processors/HedProcessor'; import LineartAnimeProcessor from './processors/LineartAnimeProcessor'; @@ -30,6 +31,16 @@ const ControlNetProcessorComponent = (props: ControlNetProcessorProps) => { ); } + if (processorNode.type === 'color_map_image_processor') { + return ( + + ); + } + if (processorNode.type === 'hed_image_processor') { return ( { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const handleChange = useCallback(() => { - dispatch(isIPAdapterEnableToggled()); - }, [dispatch]); + const handleChange = useCallback( + (e: ChangeEvent) => { + dispatch(isIPAdapterEnabledChanged(e.target.checked)); + }, + [dispatch] + ); return ( { + const { ipAdapterInfo } = controlNet; + return { ipAdapterInfo }; + }, + defaultSelectorOptions +); + const ParamIPAdapterImage = () => { - const ipAdapterInfo = useAppSelector( - (state: RootState) => state.controlNet.ipAdapterInfo - ); - - const isIPAdapterEnabled = useAppSelector( - (state: RootState) => state.controlNet.isIPAdapterEnabled - ); - + const { ipAdapterInfo } = useAppSelector(selector); const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -71,8 +75,6 @@ const ParamIPAdapterImage = () => { droppableData={droppableData} draggableData={draggableData} postUploadAction={postUploadAction} - isUploadDisabled={!isIPAdapterEnabled} - isDropDisabled={!isIPAdapterEnabled} dropLabel={t('toast.setIPAdapterImage')} noContentFallback={ { + const isEnabled = useAppSelector( + (state: RootState) => state.controlNet.isIPAdapterEnabled + ); const ipAdapterModel = useAppSelector( (state: RootState) => state.controlNet.ipAdapterInfo.model ); @@ -90,6 +93,7 @@ const ParamIPAdapterModelSelect = () => { data={data} onChange={handleValueChanged} sx={{ width: '100%' }} + disabled={!isEnabled} /> ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginEnd.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginEnd.tsx index fb43c3553d..4a303a1801 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginEnd.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginEnd.tsx @@ -10,7 +10,7 @@ import { Tooltip, } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; -import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; +import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover'; import { ControlNetConfig, controlNetBeginStepPctChanged, @@ -50,7 +50,7 @@ const ParamControlNetBeginEnd = (props: Props) => { ); return ( - + {t('controlnet.beginEndStepPercent')} diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetControlMode.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetControlMode.tsx index 9b17c2bfb2..19da346fc3 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetControlMode.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetControlMode.tsx @@ -1,5 +1,5 @@ import { useAppDispatch } from 'app/store/storeHooks'; -import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; +import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover'; import IAIMantineSelect from 'common/components/IAIMantineSelect'; import { ControlModes, @@ -35,7 +35,7 @@ export default function ParamControlNetControlMode( ); return ( - + { return ( - + + { ); return ( - + { + const { controlNetId, processorNode, isEnabled } = props; + const { color_map_tile_size } = processorNode; + const processorChanged = useProcessorNodeChanged(); + const { t } = useTranslation(); + + const handleColorMapTileSizeChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { color_map_tile_size: v }); + }, + [controlNetId, processorChanged] + ); + + const handleColorMapTileSizeReset = useCallback(() => { + processorChanged(controlNetId, { + color_map_tile_size: DEFAULTS.color_map_tile_size, + }); + }, [controlNetId, processorChanged]); + + return ( + + + + ); +}; + +export default memo(ColorMapProcessor); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/common/ProcessorWrapper.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/common/ProcessorWrapper.tsx index 5dc0a909d5..c3e5b2258e 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/common/ProcessorWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/common/ProcessorWrapper.tsx @@ -4,5 +4,9 @@ import { PropsWithChildren } from 'react'; type Props = PropsWithChildren; export default function ProcessorWrapper(props: Props) { - return {props.children}; + return ( + + {props.children} + + ); } diff --git a/invokeai/frontend/web/src/features/controlNet/store/constants.ts b/invokeai/frontend/web/src/features/controlNet/store/constants.ts index 15d23d014f..45b9822960 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/constants.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/constants.ts @@ -1,8 +1,8 @@ +import i18n from 'i18next'; import { ControlNetProcessorType, RequiredControlNetProcessorNode, } from './types'; -import i18n from 'i18next'; type ControlNetProcessorsDict = Record< ControlNetProcessorType, @@ -50,6 +50,20 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = { high_threshold: 200, }, }, + color_map_image_processor: { + type: 'color_map_image_processor', + get label() { + return i18n.t('controlnet.colorMap'); + }, + get description() { + return i18n.t('controlnet.colorMapDescription'); + }, + default: { + id: 'color_map_image_processor', + type: 'color_map_image_processor', + color_map_tile_size: 64, + }, + }, content_shuffle_image_processor: { type: 'content_shuffle_image_processor', get label() { @@ -240,4 +254,5 @@ export const CONTROLNET_MODEL_DEFAULT_PROCESSORS: { mediapipe: 'mediapipe_face_processor', pidi: 'pidi_image_processor', zoe: 'zoe_depth_image_processor', + color: 'color_map_image_processor', }; diff --git a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts index 3fe57f4a84..f0745eae2b 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts @@ -6,7 +6,6 @@ import { import { cloneDeep, forEach } from 'lodash-es'; import { imagesApi } from 'services/api/endpoints/images'; import { components } from 'services/api/schema'; -import { isAnySessionRejected } from 'services/api/thunks/session'; import { ImageDTO } from 'services/api/types'; import { appSocketInvocationError } from 'services/events/actions'; import { controlNetImageProcessed } from './actions'; @@ -99,6 +98,9 @@ export const controlNetSlice = createSlice({ isControlNetEnabledToggled: (state) => { state.isEnabled = !state.isEnabled; }, + controlNetEnabled: (state) => { + state.isEnabled = true; + }, controlNetAdded: ( state, action: PayloadAction<{ @@ -112,6 +114,12 @@ export const controlNetSlice = createSlice({ controlNetId, }; }, + controlNetRecalled: (state, action: PayloadAction) => { + const controlNet = action.payload; + state.controlNets[controlNet.controlNetId] = { + ...controlNet, + }; + }, controlNetDuplicated: ( state, action: PayloadAction<{ @@ -146,16 +154,16 @@ export const controlNetSlice = createSlice({ const { controlNetId } = action.payload; delete state.controlNets[controlNetId]; }, - controlNetToggled: ( + controlNetIsEnabledChanged: ( state, - action: PayloadAction<{ controlNetId: string }> + action: PayloadAction<{ controlNetId: string; isEnabled: boolean }> ) => { - const { controlNetId } = action.payload; + const { controlNetId, isEnabled } = action.payload; const cn = state.controlNets[controlNetId]; if (!cn) { return; } - cn.isEnabled = !cn.isEnabled; + cn.isEnabled = isEnabled; }, controlNetImageChanged: ( state, @@ -377,8 +385,8 @@ export const controlNetSlice = createSlice({ controlNetReset: () => { return { ...initialControlNetState }; }, - isIPAdapterEnableToggled: (state) => { - state.isIPAdapterEnabled = !state.isIPAdapterEnabled; + isIPAdapterEnabledChanged: (state, action: PayloadAction) => { + state.isIPAdapterEnabled = action.payload; }, ipAdapterImageChanged: (state, action: PayloadAction) => { state.ipAdapterInfo.adapterImage = action.payload; @@ -418,10 +426,6 @@ export const controlNetSlice = createSlice({ state.pendingControlImages = []; }); - builder.addMatcher(isAnySessionRejected, (state) => { - state.pendingControlImages = []; - }); - builder.addMatcher( imagesApi.endpoints.deleteImage.matchFulfilled, (state, action) => { @@ -444,13 +448,15 @@ export const controlNetSlice = createSlice({ export const { isControlNetEnabledToggled, + controlNetEnabled, controlNetAdded, + controlNetRecalled, controlNetDuplicated, controlNetAddedFromImage, controlNetRemoved, controlNetImageChanged, controlNetProcessedImageChanged, - controlNetToggled, + controlNetIsEnabledChanged, controlNetModelChanged, controlNetWeightChanged, controlNetBeginStepPctChanged, @@ -461,7 +467,7 @@ export const { controlNetProcessorTypeChanged, controlNetReset, controlNetAutoConfigToggled, - isIPAdapterEnableToggled, + isIPAdapterEnabledChanged, ipAdapterImageChanged, ipAdapterWeightChanged, ipAdapterModelChanged, diff --git a/invokeai/frontend/web/src/features/controlNet/store/types.ts b/invokeai/frontend/web/src/features/controlNet/store/types.ts index 80edb41699..871ca91377 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/types.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/types.ts @@ -1,6 +1,7 @@ import { isObject } from 'lodash-es'; import { CannyImageProcessorInvocation, + ColorMapImageProcessorInvocation, ContentShuffleImageProcessorInvocation, HedImageProcessorInvocation, LineartAnimeImageProcessorInvocation, @@ -20,6 +21,7 @@ import { O } from 'ts-toolbelt'; */ export type ControlNetProcessorNode = | CannyImageProcessorInvocation + | ColorMapImageProcessorInvocation | ContentShuffleImageProcessorInvocation | HedImageProcessorInvocation | LineartAnimeImageProcessorInvocation @@ -47,6 +49,14 @@ export type RequiredCannyImageProcessorInvocation = O.Required< 'type' | 'low_threshold' | 'high_threshold' >; +/** + * The Color Map processor node, with parameters flagged as required + */ +export type RequiredColorMapImageProcessorInvocation = O.Required< + ColorMapImageProcessorInvocation, + 'type' | 'color_map_tile_size' +>; + /** * The ContentShuffle processor node, with parameters flagged as required */ @@ -140,6 +150,7 @@ export type RequiredZoeDepthImageProcessorInvocation = O.Required< */ export type RequiredControlNetProcessorNode = O.Required< | RequiredCannyImageProcessorInvocation + | RequiredColorMapImageProcessorInvocation | RequiredContentShuffleImageProcessorInvocation | RequiredHedImageProcessorInvocation | RequiredLineartAnimeImageProcessorInvocation @@ -166,6 +177,22 @@ export const isCannyImageProcessorInvocation = ( return false; }; +/** + * Type guard for ColorMapImageProcessorInvocation + */ +export const isColorMapImageProcessorInvocation = ( + obj: unknown +): obj is ColorMapImageProcessorInvocation => { + if ( + isObject(obj) && + 'type' in obj && + obj.type === 'color_map_image_processor' + ) { + return true; + } + return false; +}; + /** * Type guard for ContentShuffleImageProcessorInvocation */ diff --git a/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx b/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx index bffe738aa9..520abc33ed 100644 --- a/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx +++ b/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx @@ -2,7 +2,6 @@ import { DragOverlay, MouseSensor, TouchSensor, - pointerWithin, useSensor, useSensors, } from '@dnd-kit/core'; @@ -14,6 +13,7 @@ import { AnimatePresence, motion } from 'framer-motion'; import { PropsWithChildren, memo, useCallback, useState } from 'react'; import { useScaledModifer } from '../hooks/useScaledCenteredModifer'; import { DragEndEvent, DragStartEvent, TypesafeDraggableData } from '../types'; +import { customPointerWithin } from '../util/customPointerWithin'; import { DndContextTypesafe } from './DndContextTypesafe'; import DragPreview from './DragPreview'; @@ -77,7 +77,7 @@ const AppDndContext = (props: PropsWithChildren) => { onDragStart={handleDragStart} onDragEnd={handleDragEnd} sensors={sensors} - collisionDetection={pointerWithin} + collisionDetection={customPointerWithin} autoScroll={false} > {props.children} @@ -87,7 +87,7 @@ const AppDndContext = (props: PropsWithChildren) => { style={{ width: 'min-content', height: 'min-content', - cursor: 'none', + cursor: 'grabbing', userSelect: 'none', // expand overlay to prevent cursor from going outside it and displaying padding: '10rem', diff --git a/invokeai/frontend/web/src/features/dnd/util/customPointerWithin.ts b/invokeai/frontend/web/src/features/dnd/util/customPointerWithin.ts new file mode 100644 index 0000000000..68f1e98b16 --- /dev/null +++ b/invokeai/frontend/web/src/features/dnd/util/customPointerWithin.ts @@ -0,0 +1,38 @@ +import { CollisionDetection, pointerWithin } from '@dnd-kit/core'; + +/** + * Filters out droppable elements that are overflowed, then applies the pointerWithin collision detection. + * + * Fixes collision detection firing on droppables that are not visible, having been scrolled out of view. + * + * See https://github.com/clauderic/dnd-kit/issues/1198 + */ +export const customPointerWithin: CollisionDetection = (arg) => { + if (!arg.pointerCoordinates) { + // sanity check + return []; + } + + // Get all elements at the pointer coordinates. This excludes elements which are overflowed, + // so it won't include the droppable elements that are scrolled out of view. + const targetElements = document.elementsFromPoint( + arg.pointerCoordinates.x, + arg.pointerCoordinates.y + ); + + const filteredDroppableContainers = arg.droppableContainers.filter( + (container) => { + if (!container.node.current) { + return false; + } + // Only include droppable elements that are in the list of elements at the pointer coordinates. + return targetElements.includes(container.node.current); + } + ); + + // Run the provided collision detection with the filtered droppable elements. + return pointerWithin({ + ...arg, + droppableContainers: filteredDroppableContainers, + }); +}; diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsCollapse.tsx b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsCollapse.tsx index ff9352d806..f34235bab2 100644 --- a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsCollapse.tsx +++ b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsCollapse.tsx @@ -43,8 +43,8 @@ const ParamDynamicPromptsCollapse = () => { activeLabel={activeLabel} > - + diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsCombinatorial.tsx b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsCombinatorial.tsx index 391d588733..353f0b4eaa 100644 --- a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsCombinatorial.tsx +++ b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsCombinatorial.tsx @@ -4,9 +4,8 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAISwitch from 'common/components/IAISwitch'; import { memo, useCallback } from 'react'; -import { combinatorialToggled } from '../store/dynamicPromptsSlice'; import { useTranslation } from 'react-i18next'; -import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; +import { combinatorialToggled } from '../store/dynamicPromptsSlice'; const selector = createSelector( stateSelector, @@ -28,13 +27,11 @@ const ParamDynamicPromptsCombinatorial = () => { }, [dispatch]); return ( - - - + ); }; diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsMaxPrompts.tsx b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsMaxPrompts.tsx index 29305630cc..df80faaa3b 100644 --- a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsMaxPrompts.tsx +++ b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsMaxPrompts.tsx @@ -9,6 +9,7 @@ import { maxPromptsReset, } from '../store/dynamicPromptsSlice'; import { useTranslation } from 'react-i18next'; +import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover'; const selector = createSelector( stateSelector, @@ -46,19 +47,21 @@ const ParamDynamicPromptsMaxPrompts = () => { }, [dispatch]); return ( - + + + ); }; diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsPreview.tsx b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsPreview.tsx index a961550abe..7e64521be2 100644 --- a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsPreview.tsx +++ b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsPreview.tsx @@ -13,6 +13,7 @@ import { stateSelector } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover'; import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent'; import { memo } from 'react'; import { FaCircleExclamation } from 'react-icons/fa6'; @@ -42,58 +43,73 @@ const ParamDynamicPromptsPreview = () => { if (isError) { return ( - - - + + + + + ); } return ( - - - Prompts Preview ({prompts.length}){parsingError && ` - ${parsingError}`} - - - - - {prompts.map((prompt, i) => ( - - {prompt} - - ))} - - - {isLoading && ( - - - - )} - - + + + + Prompts Preview ({prompts.length}) + {parsingError && ` - ${parsingError}`} + + + + + {prompts.map((prompt, i) => ( + + {prompt} + + ))} + + + {isLoading && ( + + + + )} + + + ); }; diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsSeedBehaviour.tsx b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsSeedBehaviour.tsx index 14a3e29ff7..fdd90919af 100644 --- a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsSeedBehaviour.tsx +++ b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsSeedBehaviour.tsx @@ -7,6 +7,7 @@ import { seedBehaviourChanged, } from '../store/dynamicPromptsSlice'; import IAIMantineSelectItemWithDescription from 'common/components/IAIMantineSelectItemWithDescription'; +import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover'; type Item = { label: string; @@ -47,13 +48,15 @@ const ParamDynamicPromptsSeedBehaviour = () => { ); return ( - + + + ); }; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx index 1bb6816bd9..104512a9c6 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx @@ -93,7 +93,7 @@ const GalleryBoard = ({ const [localBoardName, setLocalBoardName] = useState(board_name); const handleSelectBoard = useCallback(() => { - dispatch(boardIdSelected(board_id)); + dispatch(boardIdSelected({ boardId: board_id })); if (autoAssignBoardOnClick) { dispatch(autoAddBoardIdChanged(board_id)); } diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx index 55034decf0..6cea7d3eac 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx @@ -34,7 +34,7 @@ const NoBoardBoard = memo(({ isSelected }: Props) => { const { autoAddBoardId, autoAssignBoardOnClick } = useAppSelector(selector); const boardName = useBoardName('none'); const handleSelectBoard = useCallback(() => { - dispatch(boardIdSelected('none')); + dispatch(boardIdSelected({ boardId: 'none' })); if (autoAssignBoardOnClick) { dispatch(autoAddBoardIdChanged('none')); } diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/SystemBoardButton.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/SystemBoardButton.tsx index b538eee9d1..462aa4b5e6 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/SystemBoardButton.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/SystemBoardButton.tsx @@ -32,7 +32,7 @@ const SystemBoardButton = ({ board_id }: Props) => { const boardName = useBoardName(board_id); const handleClick = useCallback(() => { - dispatch(boardIdSelected(board_id)); + dispatch(boardIdSelected({ boardId: board_id })); }, [board_id, dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx index 83b9466722..c511ae82be 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx @@ -287,7 +287,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { icon={} tooltip={`${t('parameters.useSeed')} (S)`} aria-label={`${t('parameters.useSeed')} (S)`} - isDisabled={!metadata?.seed} + isDisabled={metadata?.seed === null || metadata?.seed === undefined} onClick={handleUseSeed} /> { recallHeight, recallStrength, recallLoRA, + recallControlNet, } = useRecallParameters(); const handleRecallPositivePrompt = useCallback(() => { @@ -75,6 +83,21 @@ const ImageMetadataActions = (props: Props) => { [recallLoRA] ); + const handleRecallControlNet = useCallback( + (controlnet: ControlNetMetadataItem) => { + recallControlNet(controlnet); + }, + [recallControlNet] + ); + + const validControlNets: ControlNetMetadataItem[] = useMemo(() => { + return metadata?.controlnets + ? metadata.controlnets.filter((controlnet) => + isValidControlNetModel(controlnet.control_model) + ) + : []; + }, [metadata?.controlnets]); + if (!metadata || Object.keys(metadata).length === 0) { return null; } @@ -180,6 +203,14 @@ const ImageMetadataActions = (props: Props) => { ); } })} + {validControlNets.map((controlnet, index) => ( + handleRecallControlNet(controlnet)} + /> + ))} ); }; diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index a4e4b02937..c78b22dd78 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -35,8 +35,11 @@ export const gallerySlice = createSlice({ autoAssignBoardOnClickChanged: (state, action: PayloadAction) => { state.autoAssignBoardOnClick = action.payload; }, - boardIdSelected: (state, action: PayloadAction) => { - state.selectedBoardId = action.payload; + boardIdSelected: ( + state, + action: PayloadAction<{ boardId: BoardId; selectedImageName?: string }> + ) => { + state.selectedBoardId = action.payload.boardId; state.galleryView = 'images'; }, autoAddBoardIdChanged: (state, action: PayloadAction) => { diff --git a/invokeai/frontend/web/src/features/lora/components/ParamLora.tsx b/invokeai/frontend/web/src/features/lora/components/ParamLora.tsx index 951de86217..7338caec22 100644 --- a/invokeai/frontend/web/src/features/lora/components/ParamLora.tsx +++ b/invokeai/frontend/web/src/features/lora/components/ParamLora.tsx @@ -10,7 +10,7 @@ import { loraWeightChanged, loraWeightReset, } from '../store/loraSlice'; -import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; +import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover'; type Props = { lora: LoRA; @@ -36,7 +36,7 @@ const ParamLora = (props: Props) => { }, [dispatch, lora.id]); return ( - + { } }, []); + // #region Updatable Edges + + /** + * Adapted from https://reactflow.dev/docs/examples/edges/updatable-edge/ + * and https://reactflow.dev/docs/examples/edges/delete-edge-on-drop/ + * + * - Edges can be dragged from one handle to another. + * - If the user drags the edge away from the node and drops it, delete the edge. + * - Do not delete the edge if the cursor didn't move (resolves annoying behaviour + * where the edge is deleted if you click it accidentally). + */ + + // We have a ref for cursor position, but it is the *projected* cursor position. + // Easiest to just keep track of the last mouse event for this particular feature + const edgeUpdateMouseEvent = useRef(); + + const onEdgeUpdateStart: NonNullable = + useCallback( + (e, edge, _handleType) => { + // update mouse event + edgeUpdateMouseEvent.current = e; + // always delete the edge when starting an updated + dispatch(edgeDeleted(edge.id)); + }, + [dispatch] + ); + + const onEdgeUpdate: OnEdgeUpdateFunc = useCallback( + (_oldEdge, newConnection) => { + // instead of updating the edge (we deleted it earlier), we instead create + // a new one. + dispatch(connectionMade(newConnection)); + }, + [dispatch] + ); + + const onEdgeUpdateEnd: NonNullable = + useCallback( + (e, edge, _handleType) => { + // Handle the case where user begins a drag but didn't move the cursor - + // bc we deleted the edge, we need to add it back + if ( + // ignore touch events + !('touches' in e) && + edgeUpdateMouseEvent.current?.clientX === e.clientX && + edgeUpdateMouseEvent.current?.clientY === e.clientY + ) { + dispatch(edgeAdded(edge)); + } + // reset mouse event + edgeUpdateMouseEvent.current = undefined; + }, + [dispatch] + ); + + // #endregion + useHotkeys(['Ctrl+c', 'Meta+c'], (e) => { e.preventDefault(); dispatch(selectionCopied()); @@ -196,6 +257,9 @@ export const Flow = () => { onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onEdgesDelete={onEdgesDelete} + onEdgeUpdate={onEdgeUpdate} + onEdgeUpdateStart={onEdgeUpdateStart} + onEdgeUpdateEnd={onEdgeUpdateEnd} onNodesDelete={onNodesDelete} onConnectStart={onConnectStart} onConnect={onConnect} diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx index d2e0667ab2..a33a854c3b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx @@ -8,6 +8,7 @@ import InvocationNodeFooter from './InvocationNodeFooter'; import InvocationNodeHeader from './InvocationNodeHeader'; import InputField from './fields/InputField'; import OutputField from './fields/OutputField'; +import { useWithFooter } from 'features/nodes/hooks/useWithFooter'; type Props = { nodeId: string; @@ -20,6 +21,7 @@ type Props = { const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => { const inputConnectionFieldNames = useConnectionInputFieldNames(nodeId); const inputAnyOrDirectFieldNames = useAnyOrDirectInputFieldNames(nodeId); + const withFooter = useWithFooter(nodeId); const outputFieldNames = useOutputFieldNames(nodeId); return ( @@ -41,7 +43,7 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => { h: 'full', py: 2, gap: 1, - borderBottomRadius: 0, + borderBottomRadius: withFooter ? 0 : 'base', }} > @@ -74,7 +76,7 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => { ))} - + {withFooter && } )} diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeFooter.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeFooter.tsx index 6f4b719f74..ec5085221e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeFooter.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeFooter.tsx @@ -5,6 +5,7 @@ import EmbedWorkflowCheckbox from './EmbedWorkflowCheckbox'; import SaveToGalleryCheckbox from './SaveToGalleryCheckbox'; import UseCacheCheckbox from './UseCacheCheckbox'; import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput'; +import { useFeatureStatus } from '../../../../../system/hooks/useFeatureStatus'; type Props = { nodeId: string; @@ -12,6 +13,7 @@ type Props = { const InvocationNodeFooter = ({ nodeId }: Props) => { const hasImageOutput = useHasImageOutput(nodeId); + const isCacheEnabled = useFeatureStatus('invocationCache').isFeatureEnabled; return ( { justifyContent: 'space-between', }} > + {isCacheEnabled && } {hasImageOutput && } - {hasImageOutput && } ); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx index 549c284c0c..5dc1305900 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx @@ -16,6 +16,7 @@ import SchedulerInputField from './inputs/SchedulerInputField'; import StringInputField from './inputs/StringInputField'; import VaeModelInputField from './inputs/VaeModelInputField'; import IPAdapterModelInputField from './inputs/IPAdapterModelInputField'; +import BoardInputField from './inputs/BoardInputField'; type InputFieldProps = { nodeId: string; @@ -99,6 +100,16 @@ const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => { ); } + if (field?.type === 'BoardField' && fieldTemplate?.type === 'BoardField') { + return ( + + ); + } + if ( field?.type === 'MainModelField' && fieldTemplate?.type === 'MainModelField' diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/BoardInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/BoardInputField.tsx new file mode 100644 index 0000000000..a6e8cbb0c1 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/BoardInputField.tsx @@ -0,0 +1,64 @@ +import { SelectItem } from '@mantine/core'; +import { useAppDispatch } from 'app/store/storeHooks'; +import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect'; +import { fieldBoardValueChanged } from 'features/nodes/store/nodesSlice'; +import { + BoardInputFieldTemplate, + BoardInputFieldValue, + FieldComponentProps, +} from 'features/nodes/types/types'; +import { memo, useCallback } from 'react'; +import { useListAllBoardsQuery } from 'services/api/endpoints/boards'; + +const BoardInputFieldComponent = ( + props: FieldComponentProps +) => { + const { nodeId, field } = props; + const dispatch = useAppDispatch(); + + const { data, hasBoards } = useListAllBoardsQuery(undefined, { + selectFromResult: ({ data }) => { + const boards: SelectItem[] = [ + { + label: 'None', + value: 'none', + }, + ]; + data?.forEach(({ board_id, board_name }) => { + boards.push({ + label: board_name, + value: board_id, + }); + }); + return { + data: boards, + hasBoards: boards.length > 1, + }; + }, + }); + + const handleChange = useCallback( + (v: string | null) => { + dispatch( + fieldBoardValueChanged({ + nodeId, + fieldName: field.name, + value: v && v !== 'none' ? { board_id: v } : undefined, + }) + ); + }, + [dispatch, field.name, nodeId] + ); + + return ( + + ); +}; + +export default memo(BoardInputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SchedulerInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SchedulerInputField.tsx index 557c128942..e4a3fb2a3d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SchedulerInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SchedulerInputField.tsx @@ -65,11 +65,6 @@ const SchedulerInputField = ( return ( { { notes: '', isOpen: true, embedWorkflow: false, - isIntermediate: true, + isIntermediate: type === 'save_image' ? false : true, inputs, outputs, useCache: template.useCache, diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useHasImageOutput.ts b/invokeai/frontend/web/src/features/nodes/hooks/useHasImageOutput.ts index 0976ededd1..111e48a45f 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useHasImageOutput.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useHasImageOutput.ts @@ -17,8 +17,12 @@ export const useHasImageOutput = (nodeId: string) => { if (!isInvocationNode(node)) { return false; } - return some(node.data.outputs, (output) => - IMAGE_FIELDS.includes(output.type) + return some( + node.data.outputs, + (output) => + IMAGE_FIELDS.includes(output.type) && + // the image primitive node does not actually save the image, do not show the image-saving checkboxes + node.data.type !== 'image' ); }, defaultSelectorOptions diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts index d1d10bb7e7..a57787556c 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts @@ -3,12 +3,7 @@ import graphlib from '@dagrejs/graphlib'; import { useAppSelector } from 'app/store/storeHooks'; import { useCallback } from 'react'; import { Connection, Edge, Node, useReactFlow } from 'reactflow'; -import { - COLLECTION_MAP, - COLLECTION_TYPES, - POLYMORPHIC_TO_SINGLE_MAP, - POLYMORPHIC_TYPES, -} from '../types/constants'; +import { validateSourceAndTargetTypes } from '../store/util/validateSourceAndTargetTypes'; import { InvocationNodeData } from '../types/types'; /** @@ -23,11 +18,6 @@ export const useIsValidConnection = () => { ); const isValidConnection = useCallback( ({ source, sourceHandle, target, targetHandle }: Connection): boolean => { - if (!shouldValidateGraph) { - // manual override! - return true; - } - const edges = flow.getEdges(); const nodes = flow.getNodes(); // Connection must have valid targets @@ -52,14 +42,23 @@ export const useIsValidConnection = () => { return false; } + if (source === target) { + // Don't allow nodes to connect to themselves, even if validation is disabled + return false; + } + + if (!shouldValidateGraph) { + // manual override! + return true; + } + if ( - edges - .filter((edge) => { - return edge.target === target && edge.targetHandle === targetHandle; - }) - .find((edge) => { - edge.source === source && edge.sourceHandle === sourceHandle; - }) + edges.find((edge) => { + edge.target === target && + edge.targetHandle === targetHandle && + edge.source === source && + edge.sourceHandle === sourceHandle; + }) ) { // We already have a connection from this source to this target return false; @@ -76,60 +75,8 @@ export const useIsValidConnection = () => { return false; } - /** - * Connection types must be the same for a connection, with exceptions: - * - CollectionItem can connect to any non-Collection - * - Non-Collections can connect to CollectionItem - * - Anything (non-Collections, Collections, Polymorphics) can connect to Polymorphics of the same base type - * - Generic Collection can connect to any other Collection or Polymorphic - * - Any Collection can connect to a Generic Collection - */ - - if (sourceType !== targetType) { - const isCollectionItemToNonCollection = - sourceType === 'CollectionItem' && - !COLLECTION_TYPES.includes(targetType); - - const isNonCollectionToCollectionItem = - targetType === 'CollectionItem' && - !COLLECTION_TYPES.includes(sourceType) && - !POLYMORPHIC_TYPES.includes(sourceType); - - const isAnythingToPolymorphicOfSameBaseType = - POLYMORPHIC_TYPES.includes(targetType) && - (() => { - if (!POLYMORPHIC_TYPES.includes(targetType)) { - return false; - } - const baseType = - POLYMORPHIC_TO_SINGLE_MAP[ - targetType as keyof typeof POLYMORPHIC_TO_SINGLE_MAP - ]; - - const collectionType = - COLLECTION_MAP[baseType as keyof typeof COLLECTION_MAP]; - - return sourceType === baseType || sourceType === collectionType; - })(); - - const isGenericCollectionToAnyCollectionOrPolymorphic = - sourceType === 'Collection' && - (COLLECTION_TYPES.includes(targetType) || - POLYMORPHIC_TYPES.includes(targetType)); - - const isCollectionToGenericCollection = - targetType === 'Collection' && COLLECTION_TYPES.includes(sourceType); - - const isIntToFloat = sourceType === 'integer' && targetType === 'float'; - - return ( - isCollectionItemToNonCollection || - isNonCollectionToCollectionItem || - isAnythingToPolymorphicOfSameBaseType || - isGenericCollectionToAnyCollectionOrPolymorphic || - isCollectionToGenericCollection || - isIntToFloat - ); + if (!validateSourceAndTargetTypes(sourceType, targetType)) { + return false; } // Graphs much be acyclic (no loops!) diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts b/invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts index 57941eaec8..4d2a58cc35 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts @@ -1,31 +1,14 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { stateSelector } from 'app/store/store'; -import { useAppSelector } from 'app/store/storeHooks'; -import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import { some } from 'lodash-es'; +import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useMemo } from 'react'; -import { FOOTER_FIELDS } from '../types/constants'; -import { isInvocationNode } from '../types/types'; +import { useHasImageOutput } from './useHasImageOutput'; -export const useHasImageOutputs = (nodeId: string) => { - const selector = useMemo( - () => - createSelector( - stateSelector, - ({ nodes }) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return false; - } - return some(node.data.outputs, (output) => - FOOTER_FIELDS.includes(output.type) - ); - }, - defaultSelectorOptions - ), - [nodeId] +export const useWithFooter = (nodeId: string) => { + const hasImageOutput = useHasImageOutput(nodeId); + const isCacheEnabled = useFeatureStatus('invocationCache').isFeatureEnabled; + + const withFooter = useMemo( + () => hasImageOutput || isCacheEnabled, + [hasImageOutput, isCacheEnabled] ); - - const withFooter = useAppSelector(selector); return withFooter; }; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index e59105348f..1b3a5ca929 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { cloneDeep, forEach, isEqual, map, uniqBy } from 'lodash-es'; +import { cloneDeep, forEach, isEqual, uniqBy } from 'lodash-es'; import { addEdge, applyEdgeChanges, @@ -15,11 +15,11 @@ import { NodeChange, OnConnectStartParams, SelectionMode, + updateEdge, Viewport, XYPosition, } from 'reactflow'; import { receivedOpenAPISchema } from 'services/api/thunks/schema'; -import { sessionCanceled, sessionInvoked } from 'services/api/thunks/session'; import { ImageField } from 'services/api/types'; import { appSocketGeneratorProgress, @@ -31,6 +31,7 @@ import { import { v4 as uuidv4 } from 'uuid'; import { DRAG_HANDLE_CLASSNAME } from '../types/constants'; import { + BoardInputFieldValue, BooleanInputFieldValue, ColorInputFieldValue, ControlNetModelInputFieldValue, @@ -182,6 +183,16 @@ const nodesSlice = createSlice({ edgesChanged: (state, action: PayloadAction) => { state.edges = applyEdgeChanges(action.payload, state.edges); }, + edgeAdded: (state, action: PayloadAction) => { + state.edges = addEdge(action.payload, state.edges); + }, + edgeUpdated: ( + state, + action: PayloadAction<{ oldEdge: Edge; newConnection: Connection }> + ) => { + const { oldEdge, newConnection } = action.payload; + state.edges = updateEdge(oldEdge, newConnection, state.edges); + }, connectionStarted: (state, action: PayloadAction) => { state.connectionStartParams = action.payload; const { nodeId, handleId, handleType } = action.payload; @@ -366,6 +377,7 @@ const nodesSlice = createSlice({ target: edge.target, type: 'collapsed', data: { count: 1 }, + updatable: false, }); } } @@ -388,6 +400,7 @@ const nodesSlice = createSlice({ target: edge.target, type: 'collapsed', data: { count: 1 }, + updatable: false, }); } } @@ -400,6 +413,9 @@ const nodesSlice = createSlice({ } } }, + edgeDeleted: (state, action: PayloadAction) => { + state.edges = state.edges.filter((e) => e.id !== action.payload); + }, edgesDeleted: (state, action: PayloadAction) => { const edges = action.payload; const collapsedEdges = edges.filter((e) => e.type === 'collapsed'); @@ -495,6 +511,12 @@ const nodesSlice = createSlice({ ) => { fieldValueReducer(state, action); }, + fieldBoardValueChanged: ( + state, + action: FieldValueAction + ) => { + fieldValueReducer(state, action); + }, fieldImageValueChanged: ( state, action: FieldValueAction @@ -869,26 +891,8 @@ const nodesSlice = createSlice({ node.progressImage = progress_image ?? null; } }); - builder.addCase(sessionInvoked.fulfilled, (state) => { - forEach(state.nodeExecutionStates, (nes) => { - nes.status = NodeStatus.PENDING; - nes.error = null; - nes.progress = null; - nes.progressImage = null; - nes.outputs = []; - }); - }); - builder.addCase(sessionCanceled.fulfilled, (state) => { - map(state.nodeExecutionStates, (nes) => { - if (nes.status === NodeStatus.IN_PROGRESS) { - nes.status = NodeStatus.PENDING; - } - }); - }); builder.addCase(appSocketQueueItemStatusChanged, (state, action) => { - if ( - ['completed', 'canceled', 'failed'].includes(action.payload.data.status) - ) { + if (['in_progress'].includes(action.payload.data.status)) { forEach(state.nodeExecutionStates, (nes) => { nes.status = NodeStatus.PENDING; nes.error = null; @@ -902,68 +906,72 @@ const nodesSlice = createSlice({ }); export const { - nodesChanged, - edgesChanged, - nodeAdded, - nodesDeleted, + addNodePopoverClosed, + addNodePopoverOpened, + addNodePopoverToggled, + connectionEnded, connectionMade, connectionStarted, - connectionEnded, - shouldShowFieldTypeLegendChanged, - shouldShowMinimapPanelChanged, - nodeTemplatesBuilt, - nodeEditorReset, - imageCollectionFieldValueChanged, - fieldStringValueChanged, - fieldNumberValueChanged, + edgeDeleted, + edgesChanged, + edgesDeleted, + edgeUpdated, + fieldBoardValueChanged, fieldBooleanValueChanged, - fieldImageValueChanged, fieldColorValueChanged, - fieldMainModelValueChanged, - fieldVaeModelValueChanged, - fieldLoRAModelValueChanged, - fieldEnumModelValueChanged, fieldControlNetModelValueChanged, + fieldEnumModelValueChanged, + fieldImageValueChanged, fieldIPAdapterModelValueChanged, + fieldLabelChanged, + fieldLoRAModelValueChanged, + fieldMainModelValueChanged, + fieldNumberValueChanged, fieldRefinerModelValueChanged, fieldSchedulerValueChanged, + fieldStringValueChanged, + fieldVaeModelValueChanged, + imageCollectionFieldValueChanged, + mouseOverFieldChanged, + mouseOverNodeChanged, + nodeAdded, + nodeEditorReset, + nodeEmbedWorkflowChanged, + nodeExclusivelySelected, + nodeIsIntermediateChanged, nodeIsOpenChanged, nodeLabelChanged, nodeNotesChanged, - edgesDeleted, - shouldValidateGraphChanged, - shouldAnimateEdgesChanged, nodeOpacityChanged, - shouldSnapToGridChanged, - shouldColorEdgesChanged, - selectedNodesChanged, - selectedEdgesChanged, - workflowNameChanged, - workflowDescriptionChanged, - workflowTagsChanged, - workflowAuthorChanged, - workflowNotesChanged, - workflowVersionChanged, - workflowContactChanged, - workflowLoaded, + nodesChanged, + nodesDeleted, + nodeTemplatesBuilt, + nodeUseCacheChanged, notesNodeValueChanged, + selectedAll, + selectedEdgesChanged, + selectedNodesChanged, + selectionCopied, + selectionModeChanged, + selectionPasted, + shouldAnimateEdgesChanged, + shouldColorEdgesChanged, + shouldShowFieldTypeLegendChanged, + shouldShowMinimapPanelChanged, + shouldSnapToGridChanged, + shouldValidateGraphChanged, + viewportChanged, + workflowAuthorChanged, + workflowContactChanged, + workflowDescriptionChanged, workflowExposedFieldAdded, workflowExposedFieldRemoved, - fieldLabelChanged, - viewportChanged, - mouseOverFieldChanged, - selectionCopied, - selectionPasted, - selectedAll, - addNodePopoverOpened, - addNodePopoverClosed, - addNodePopoverToggled, - selectionModeChanged, - nodeEmbedWorkflowChanged, - nodeIsIntermediateChanged, - mouseOverNodeChanged, - nodeExclusivelySelected, - nodeUseCacheChanged, + workflowLoaded, + workflowNameChanged, + workflowNotesChanged, + workflowTagsChanged, + workflowVersionChanged, + edgeAdded, } = nodesSlice.actions; export default nodesSlice.reducer; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts b/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts index ac157bb476..6343240a88 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts @@ -1,15 +1,10 @@ import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; import { getIsGraphAcyclic } from 'features/nodes/hooks/useIsValidConnection'; -import { - COLLECTION_MAP, - COLLECTION_TYPES, - POLYMORPHIC_TO_SINGLE_MAP, - POLYMORPHIC_TYPES, -} from 'features/nodes/types/constants'; import { FieldType } from 'features/nodes/types/types'; -import { HandleType } from 'reactflow'; import i18n from 'i18next'; +import { HandleType } from 'reactflow'; +import { validateSourceAndTargetTypes } from './validateSourceAndTargetTypes'; /** * NOTE: The logic here must be duplicated in `invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts` @@ -60,9 +55,29 @@ export const makeConnectionErrorSelector = ( return i18n.t('nodes.cannotConnectInputToInput'); } + // we have to figure out which is the target and which is the source + const target = handleType === 'target' ? nodeId : connectionNodeId; + const targetHandle = + handleType === 'target' ? fieldName : connectionFieldName; + const source = handleType === 'source' ? nodeId : connectionNodeId; + const sourceHandle = + handleType === 'source' ? fieldName : connectionFieldName; + if ( edges.find((edge) => { - return edge.target === nodeId && edge.targetHandle === fieldName; + edge.target === target && + edge.targetHandle === targetHandle && + edge.source === source && + edge.sourceHandle === sourceHandle; + }) + ) { + // We already have a connection from this source to this target + return i18n.t('nodes.cannotDuplicateConnection'); + } + + if ( + edges.find((edge) => { + return edge.target === target && edge.targetHandle === targetHandle; }) && // except CollectionItem inputs can have multiples targetType !== 'CollectionItem' @@ -70,64 +85,8 @@ export const makeConnectionErrorSelector = ( return i18n.t('nodes.inputMayOnlyHaveOneConnection'); } - /** - * Connection types must be the same for a connection, with exceptions: - * - CollectionItem can connect to any non-Collection - * - Non-Collections can connect to CollectionItem - * - Anything (non-Collections, Collections, Polymorphics) can connect to Polymorphics of the same base type - * - Generic Collection can connect to any other Collection or Polymorphic - * - Any Collection can connect to a Generic Collection - */ - - if (sourceType !== targetType) { - const isCollectionItemToNonCollection = - sourceType === 'CollectionItem' && - !COLLECTION_TYPES.includes(targetType); - - const isNonCollectionToCollectionItem = - targetType === 'CollectionItem' && - !COLLECTION_TYPES.includes(sourceType) && - !POLYMORPHIC_TYPES.includes(sourceType); - - const isAnythingToPolymorphicOfSameBaseType = - POLYMORPHIC_TYPES.includes(targetType) && - (() => { - if (!POLYMORPHIC_TYPES.includes(targetType)) { - return false; - } - const baseType = - POLYMORPHIC_TO_SINGLE_MAP[ - targetType as keyof typeof POLYMORPHIC_TO_SINGLE_MAP - ]; - - const collectionType = - COLLECTION_MAP[baseType as keyof typeof COLLECTION_MAP]; - - return sourceType === baseType || sourceType === collectionType; - })(); - - const isGenericCollectionToAnyCollectionOrPolymorphic = - sourceType === 'Collection' && - (COLLECTION_TYPES.includes(targetType) || - POLYMORPHIC_TYPES.includes(targetType)); - - const isCollectionToGenericCollection = - targetType === 'Collection' && COLLECTION_TYPES.includes(sourceType); - - const isIntToFloat = sourceType === 'integer' && targetType === 'float'; - - if ( - !( - isCollectionItemToNonCollection || - isNonCollectionToCollectionItem || - isAnythingToPolymorphicOfSameBaseType || - isGenericCollectionToAnyCollectionOrPolymorphic || - isCollectionToGenericCollection || - isIntToFloat - ) - ) { - return i18n.t('nodes.fieldTypesMustMatch'); - } + if (!validateSourceAndTargetTypes(sourceType, targetType)) { + return i18n.t('nodes.fieldTypesMustMatch'); } const isGraphAcyclic = getIsGraphAcyclic( diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateSourceAndTargetTypes.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateSourceAndTargetTypes.ts new file mode 100644 index 0000000000..4f0be3329a --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateSourceAndTargetTypes.ts @@ -0,0 +1,74 @@ +import { + COLLECTION_MAP, + COLLECTION_TYPES, + POLYMORPHIC_TO_SINGLE_MAP, + POLYMORPHIC_TYPES, +} from 'features/nodes/types/constants'; +import { FieldType } from 'features/nodes/types/types'; + +export const validateSourceAndTargetTypes = ( + sourceType: FieldType, + targetType: FieldType +) => { + if (sourceType === targetType) { + return true; + } + + /** + * Connection types must be the same for a connection, with exceptions: + * - CollectionItem can connect to any non-Collection + * - Non-Collections can connect to CollectionItem + * - Anything (non-Collections, Collections, Polymorphics) can connect to Polymorphics of the same base type + * - Generic Collection can connect to any other Collection or Polymorphic + * - Any Collection can connect to a Generic Collection + */ + + const isCollectionItemToNonCollection = + sourceType === 'CollectionItem' && !COLLECTION_TYPES.includes(targetType); + + const isNonCollectionToCollectionItem = + targetType === 'CollectionItem' && + !COLLECTION_TYPES.includes(sourceType) && + !POLYMORPHIC_TYPES.includes(sourceType); + + const isAnythingToPolymorphicOfSameBaseType = + POLYMORPHIC_TYPES.includes(targetType) && + (() => { + if (!POLYMORPHIC_TYPES.includes(targetType)) { + return false; + } + const baseType = + POLYMORPHIC_TO_SINGLE_MAP[ + targetType as keyof typeof POLYMORPHIC_TO_SINGLE_MAP + ]; + + const collectionType = + COLLECTION_MAP[baseType as keyof typeof COLLECTION_MAP]; + + return sourceType === baseType || sourceType === collectionType; + })(); + + const isGenericCollectionToAnyCollectionOrPolymorphic = + sourceType === 'Collection' && + (COLLECTION_TYPES.includes(targetType) || + POLYMORPHIC_TYPES.includes(targetType)); + + const isCollectionToGenericCollection = + targetType === 'Collection' && COLLECTION_TYPES.includes(sourceType); + + const isIntToFloat = sourceType === 'integer' && targetType === 'float'; + + const isIntOrFloatToString = + (sourceType === 'integer' || sourceType === 'float') && + targetType === 'string'; + + return ( + isCollectionItemToNonCollection || + isNonCollectionToCollectionItem || + isAnythingToPolymorphicOfSameBaseType || + isGenericCollectionToAnyCollectionOrPolymorphic || + isCollectionToGenericCollection || + isIntToFloat || + isIntOrFloatToString + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/types/constants.ts b/invokeai/frontend/web/src/features/nodes/types/constants.ts index a54f84d3f0..c41d8369b1 100644 --- a/invokeai/frontend/web/src/features/nodes/types/constants.ts +++ b/invokeai/frontend/web/src/features/nodes/types/constants.ts @@ -1,4 +1,9 @@ -import { FieldType, FieldUIConfig } from './types'; +import { + FieldType, + FieldTypeMap, + FieldTypeMapWithNumber, + FieldUIConfig, +} from './types'; import { t } from 'i18next'; export const HANDLE_TOOLTIP_OPEN_DELAY = 500; @@ -28,7 +33,7 @@ export const COLLECTION_TYPES: FieldType[] = [ 'ColorCollection', ]; -export const POLYMORPHIC_TYPES = [ +export const POLYMORPHIC_TYPES: FieldType[] = [ 'IntegerPolymorphic', 'BooleanPolymorphic', 'FloatPolymorphic', @@ -40,7 +45,7 @@ export const POLYMORPHIC_TYPES = [ 'ColorPolymorphic', ]; -export const MODEL_TYPES = [ +export const MODEL_TYPES: FieldType[] = [ 'IPAdapterModelField', 'ControlNetModelField', 'LoRAModelField', @@ -54,7 +59,7 @@ export const MODEL_TYPES = [ 'ClipField', ]; -export const COLLECTION_MAP = { +export const COLLECTION_MAP: FieldTypeMapWithNumber = { integer: 'IntegerCollection', boolean: 'BooleanCollection', number: 'FloatCollection', @@ -71,7 +76,7 @@ export const isCollectionItemType = ( ): itemType is keyof typeof COLLECTION_MAP => Boolean(itemType && itemType in COLLECTION_MAP); -export const SINGLE_TO_POLYMORPHIC_MAP = { +export const SINGLE_TO_POLYMORPHIC_MAP: FieldTypeMapWithNumber = { integer: 'IntegerPolymorphic', boolean: 'BooleanPolymorphic', number: 'FloatPolymorphic', @@ -84,7 +89,7 @@ export const SINGLE_TO_POLYMORPHIC_MAP = { ColorField: 'ColorPolymorphic', }; -export const POLYMORPHIC_TO_SINGLE_MAP = { +export const POLYMORPHIC_TO_SINGLE_MAP: FieldTypeMap = { IntegerPolymorphic: 'integer', BooleanPolymorphic: 'boolean', FloatPolymorphic: 'float', @@ -96,7 +101,7 @@ export const POLYMORPHIC_TO_SINGLE_MAP = { ColorPolymorphic: 'ColorField', }; -export const TYPES_WITH_INPUT_COMPONENTS = [ +export const TYPES_WITH_INPUT_COMPONENTS: FieldType[] = [ 'string', 'StringPolymorphic', 'boolean', @@ -116,6 +121,8 @@ export const TYPES_WITH_INPUT_COMPONENTS = [ 'ColorField', 'SDXLMainModelField', 'Scheduler', + 'IPAdapterModelField', + 'BoardField', ]; export const isPolymorphicItemType = ( @@ -239,6 +246,11 @@ export const FIELDS: Record = { description: t('nodes.imageFieldDescription'), title: t('nodes.imageField'), }, + BoardField: { + color: 'purple.500', + description: t('nodes.imageFieldDescription'), + title: t('nodes.imageField'), + }, ImagePolymorphic: { color: 'purple.500', description: t('nodes.imagePolymorphicDescription'), diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts index 2a3e5a762b..eb8baf513e 100644 --- a/invokeai/frontend/web/src/features/nodes/types/types.ts +++ b/invokeai/frontend/web/src/features/nodes/types/types.ts @@ -72,6 +72,7 @@ export type FieldUIConfig = { // TODO: Get this from the OpenAPI schema? may be tricky... export const zFieldType = z.enum([ + 'BoardField', 'boolean', 'BooleanCollection', 'BooleanPolymorphic', @@ -119,6 +120,10 @@ export const zFieldType = z.enum([ ]); export type FieldType = z.infer; +export type FieldTypeMap = { [key in FieldType]?: FieldType }; +export type FieldTypeMapWithNumber = { + [key in FieldType | 'number']?: FieldType; +}; export const zReservedFieldType = z.enum([ 'WorkflowField', @@ -187,6 +192,11 @@ export const zImageField = z.object({ }); export type ImageField = z.infer; +export const zBoardField = z.object({ + board_id: z.string().trim().min(1), +}); +export type BoardField = z.infer; + export const zLatentsField = z.object({ latents_name: z.string().trim().min(1), seed: z.number().int().optional(), @@ -494,6 +504,12 @@ export const zImageInputFieldValue = zInputFieldValueBase.extend({ }); export type ImageInputFieldValue = z.infer; +export const zBoardInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('BoardField'), + value: zBoardField.optional(), +}); +export type BoardInputFieldValue = z.infer; + export const zImagePolymorphicInputFieldValue = zInputFieldValueBase.extend({ type: z.literal('ImagePolymorphic'), value: zImageField.optional(), @@ -630,6 +646,7 @@ export type SchedulerInputFieldValue = z.infer< >; export const zInputFieldValue = z.discriminatedUnion('type', [ + zBoardInputFieldValue, zBooleanCollectionInputFieldValue, zBooleanInputFieldValue, zBooleanPolymorphicInputFieldValue, @@ -770,6 +787,11 @@ export type BooleanPolymorphicInputFieldTemplate = Omit< type: 'BooleanPolymorphic'; }; +export type BoardInputFieldTemplate = InputFieldTemplateBase & { + default: BoardField; + type: 'BoardField'; +}; + export type ImageInputFieldTemplate = InputFieldTemplateBase & { default: ImageField; type: 'ImageField'; @@ -952,6 +974,7 @@ export type WorkflowInputFieldTemplate = InputFieldTemplateBase & { * maximum length, pattern to match, etc). */ export type InputFieldTemplate = + | BoardInputFieldTemplate | BooleanCollectionInputFieldTemplate | BooleanPolymorphicInputFieldTemplate | BooleanInputFieldTemplate @@ -1118,6 +1141,10 @@ const zLoRAMetadataItem = z.object({ export type LoRAMetadataItem = z.infer; +const zControlNetMetadataItem = zControlField.deepPartial(); + +export type ControlNetMetadataItem = z.infer; + export const zCoreMetadata = z .object({ app_version: z.string().nullish().catch(null), @@ -1199,6 +1226,7 @@ export const zInvocationNodeData = z.object({ notes: z.string(), embedWorkflow: z.boolean(), isIntermediate: z.boolean(), + useCache: z.boolean().optional(), version: zSemVer.optional(), }); diff --git a/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts b/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts index aaec058235..cf5f7c5523 100644 --- a/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts +++ b/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts @@ -62,6 +62,8 @@ import { ConditioningField, IPAdapterInputFieldTemplate, IPAdapterModelInputFieldTemplate, + BoardInputFieldTemplate, + InputFieldTemplate, } from '../types/types'; import { ControlField } from 'services/api/types'; @@ -450,6 +452,19 @@ const buildIPAdapterModelInputFieldTemplate = ({ return template; }; +const buildBoardInputFieldTemplate = ({ + schemaObject, + baseField, +}: BuildInputFieldArg): BoardInputFieldTemplate => { + const template: BoardInputFieldTemplate = { + ...baseField, + type: 'BoardField', + default: schemaObject.default ?? undefined, + }; + + return template; +}; + const buildImageInputFieldTemplate = ({ schemaObject, baseField, @@ -851,7 +866,10 @@ export const getFieldType = ( return; }; -const TEMPLATE_BUILDER_MAP = { +const TEMPLATE_BUILDER_MAP: { + [key in FieldType]?: (arg: BuildInputFieldArg) => InputFieldTemplate; +} = { + BoardField: buildBoardInputFieldTemplate, boolean: buildBooleanInputFieldTemplate, BooleanCollection: buildBooleanCollectionInputFieldTemplate, BooleanPolymorphic: buildBooleanPolymorphicInputFieldTemplate, @@ -937,7 +955,13 @@ export const buildInputFieldTemplate = ( return; } - return TEMPLATE_BUILDER_MAP[fieldType]({ + const builder = TEMPLATE_BUILDER_MAP[fieldType]; + + if (!builder) { + return; + } + + return builder({ schemaObject: fieldSchema, baseField, }); diff --git a/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts b/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts index c29aaf262f..9c90595c70 100644 --- a/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts +++ b/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts @@ -1,7 +1,10 @@ -import { InputFieldTemplate, InputFieldValue } from '../types/types'; +import { FieldType, InputFieldTemplate, InputFieldValue } from '../types/types'; -const FIELD_VALUE_FALLBACK_MAP = { +const FIELD_VALUE_FALLBACK_MAP: { + [key in FieldType]: InputFieldValue['value']; +} = { enum: '', + BoardField: undefined, boolean: false, BooleanCollection: [], BooleanPolymorphic: false, diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addControlNetToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addControlNetToLinearGraph.ts index 491c6547ba..1df90624ef 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addControlNetToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addControlNetToLinearGraph.ts @@ -8,7 +8,11 @@ import { MetadataAccumulatorInvocation, } from 'services/api/types'; import { NonNullableGraph } from '../../types/types'; -import { CONTROL_NET_COLLECT, METADATA_ACCUMULATOR } from './constants'; +import { + CANVAS_COHERENCE_DENOISE_LATENTS, + CONTROL_NET_COLLECT, + METADATA_ACCUMULATOR, +} from './constants'; export const addControlNetToLinearGraph = ( state: RootState, @@ -100,6 +104,16 @@ export const addControlNetToLinearGraph = ( field: 'item', }, }); + + if (CANVAS_COHERENCE_DENOISE_LATENTS in graph.nodes) { + graph.edges.push({ + source: { node_id: controlNetNode.id, field: 'control' }, + destination: { + node_id: CANVAS_COHERENCE_DENOISE_LATENTS, + field: 'control', + }, + }); + } }); } } diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addIPAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addIPAdapterToLinearGraph.ts index da67b1d34d..d645b274ec 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addIPAdapterToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addIPAdapterToLinearGraph.ts @@ -1,7 +1,7 @@ import { RootState } from 'app/store/store'; import { IPAdapterInvocation } from 'services/api/types'; import { NonNullableGraph } from '../../types/types'; -import { IP_ADAPTER } from './constants'; +import { CANVAS_COHERENCE_DENOISE_LATENTS, IP_ADAPTER } from './constants'; export const addIPAdapterToLinearGraph = ( state: RootState, @@ -55,5 +55,15 @@ export const addIPAdapterToLinearGraph = ( field: 'ip_adapter', }, }); + + if (CANVAS_COHERENCE_DENOISE_LATENTS in graph.nodes) { + graph.edges.push({ + source: { node_id: ipAdapterNode.id, field: 'ip_adapter' }, + destination: { + node_id: CANVAS_COHERENCE_DENOISE_LATENTS, + field: 'ip_adapter', + }, + }); + } } }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSDXLRefinerToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSDXLRefinerToGraph.ts index 6bd44db197..a6ee6a091d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSDXLRefinerToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSDXLRefinerToGraph.ts @@ -32,7 +32,8 @@ export const addSDXLRefinerToGraph = ( graph: NonNullableGraph, baseNodeId: string, modelLoaderNodeId?: string, - canvasInitImage?: ImageDTO + canvasInitImage?: ImageDTO, + canvasMaskImage?: ImageDTO ): void => { const { refinerModel, @@ -257,8 +258,30 @@ export const addSDXLRefinerToGraph = ( }; } - graph.edges.push( - { + if (graph.id === SDXL_CANVAS_INPAINT_GRAPH) { + if (isUsingScaledDimensions) { + graph.edges.push({ + source: { + node_id: MASK_RESIZE_UP, + field: 'image', + }, + destination: { + node_id: SDXL_REFINER_INPAINT_CREATE_MASK, + field: 'mask', + }, + }); + } else { + graph.nodes[SDXL_REFINER_INPAINT_CREATE_MASK] = { + ...(graph.nodes[ + SDXL_REFINER_INPAINT_CREATE_MASK + ] as CreateDenoiseMaskInvocation), + mask: canvasMaskImage, + }; + } + } + + if (graph.id === SDXL_CANVAS_OUTPAINT_GRAPH) { + graph.edges.push({ source: { node_id: isUsingScaledDimensions ? MASK_RESIZE_UP : MASK_COMBINE, field: 'image', @@ -267,18 +290,19 @@ export const addSDXLRefinerToGraph = ( node_id: SDXL_REFINER_INPAINT_CREATE_MASK, field: 'mask', }, + }); + } + + graph.edges.push({ + source: { + node_id: SDXL_REFINER_INPAINT_CREATE_MASK, + field: 'denoise_mask', }, - { - source: { - node_id: SDXL_REFINER_INPAINT_CREATE_MASK, - field: 'denoise_mask', - }, - destination: { - node_id: SDXL_REFINER_DENOISE_LATENTS, - field: 'denoise_mask', - }, - } - ); + destination: { + node_id: SDXL_REFINER_DENOISE_LATENTS, + field: 'denoise_mask', + }, + }); } if ( diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSaveImageNode.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSaveImageNode.ts index 43d1a19f81..738c69faff 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSaveImageNode.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSaveImageNode.ts @@ -24,12 +24,14 @@ export const addSaveImageNode = ( const activeTabName = activeTabNameSelector(state); const is_intermediate = activeTabName === 'unifiedCanvas' ? !state.canvas.shouldAutoSave : false; + const { autoAddBoardId } = state.gallery; const saveImageNode: SaveImageInvocation = { id: SAVE_IMAGE, type: 'save_image', is_intermediate, use_cache: false, + board: autoAddBoardId === 'none' ? undefined : { board_id: autoAddBoardId }, }; graph.nodes[SAVE_IMAGE] = saveImageNode; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildAdHocUpscaleGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildAdHocUpscaleGraph.ts index 9c53227ead..c612e88598 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildAdHocUpscaleGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildAdHocUpscaleGraph.ts @@ -6,15 +6,18 @@ import { SaveImageInvocation, } from 'services/api/types'; import { REALESRGAN as ESRGAN, SAVE_IMAGE } from './constants'; +import { BoardId } from 'features/gallery/store/types'; type Arg = { image_name: string; esrganModelName: ESRGANModelName; + autoAddBoardId: BoardId; }; export const buildAdHocUpscaleGraph = ({ image_name, esrganModelName, + autoAddBoardId, }: Arg): Graph => { const realesrganNode: ESRGANInvocation = { id: ESRGAN, @@ -28,6 +31,8 @@ export const buildAdHocUpscaleGraph = ({ id: SAVE_IMAGE, type: 'save_image', use_cache: false, + is_intermediate: false, + board: autoAddBoardId === 'none' ? undefined : { board_id: autoAddBoardId }, }; const graph: NonNullableGraph = { diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLImageToImageGraph.ts index 36fc66e559..d958b78a90 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLImageToImageGraph.ts @@ -45,7 +45,6 @@ export const buildCanvasSDXLImageToImageGraph = ( seed, steps, vaePrecision, - clipSkip, shouldUseCpuNoise, seamlessXAxis, seamlessYAxis, @@ -339,7 +338,6 @@ export const buildCanvasSDXLImageToImageGraph = ( vae: undefined, // option; set in addVAEToGraph controlnets: [], // populated in addControlNetToLinearGraph loras: [], // populated in addLoRAsToGraph - clip_skip: clipSkip, strength, init_image: initialImage.image_name, }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLInpaintGraph.ts index 389d510ac7..a245953c8e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLInpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLInpaintGraph.ts @@ -663,7 +663,8 @@ export const buildCanvasSDXLInpaintGraph = ( graph, CANVAS_COHERENCE_DENOISE_LATENTS, modelLoaderNodeId, - canvasInitImage + canvasInitImage, + canvasMaskImage ); if (seamlessXAxis || seamlessYAxis) { modelLoaderNodeId = SDXL_REFINER_SEAMLESS; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLTextToImageGraph.ts index 37245d7b6a..9f9a442b99 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLTextToImageGraph.ts @@ -46,7 +46,6 @@ export const buildCanvasSDXLTextToImageGraph = ( seed, steps, vaePrecision, - clipSkip, shouldUseCpuNoise, seamlessXAxis, seamlessYAxis, @@ -321,7 +320,6 @@ export const buildCanvasSDXLTextToImageGraph = ( vae: undefined, // option; set in addVAEToGraph controlnets: [], // populated in addControlNetToLinearGraph loras: [], // populated in addLoRAsToGraph - clip_skip: clipSkip, }; graph.edges.push({ diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearSDXLImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearSDXLImageToImageGraph.ts index b10f4c5542..bc02fb5a66 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearSDXLImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearSDXLImageToImageGraph.ts @@ -49,7 +49,6 @@ export const buildLinearSDXLImageToImageGraph = ( shouldFitToWidthHeight, width, height, - clipSkip, shouldUseCpuNoise, vaePrecision, seamlessXAxis, @@ -349,7 +348,6 @@ export const buildLinearSDXLImageToImageGraph = ( vae: undefined, controlnets: [], loras: [], - clip_skip: clipSkip, strength: strength, init_image: initialImage.imageName, positive_style_prompt: positiveStylePrompt, diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearSDXLTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearSDXLTextToImageGraph.ts index 73c831081d..22a7dd4192 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearSDXLTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearSDXLTextToImageGraph.ts @@ -38,7 +38,6 @@ export const buildLinearSDXLTextToImageGraph = ( steps, width, height, - clipSkip, shouldUseCpuNoise, vaePrecision, seamlessXAxis, @@ -243,7 +242,6 @@ export const buildLinearSDXLTextToImageGraph = ( vae: undefined, controlnets: [], loras: [], - clip_skip: clipSkip, positive_style_prompt: positiveStylePrompt, negative_style_prompt: negativeStylePrompt, }; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Advanced/ParamAdvancedCollapse.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Advanced/ParamAdvancedCollapse.tsx index dea4bb7b3d..85b6eaa903 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Advanced/ParamAdvancedCollapse.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Advanced/ParamAdvancedCollapse.tsx @@ -13,16 +13,16 @@ import ParamClipSkip from './ParamClipSkip'; const selector = createSelector( stateSelector, (state: RootState) => { - const { clipSkip, seamlessXAxis, seamlessYAxis, shouldUseCpuNoise } = + const { clipSkip, model, seamlessXAxis, seamlessYAxis, shouldUseCpuNoise } = state.generation; - return { clipSkip, seamlessXAxis, seamlessYAxis, shouldUseCpuNoise }; + return { clipSkip, model, seamlessXAxis, seamlessYAxis, shouldUseCpuNoise }; }, defaultSelectorOptions ); export default function ParamAdvancedCollapse() { - const { clipSkip, seamlessXAxis, seamlessYAxis, shouldUseCpuNoise } = + const { clipSkip, model, seamlessXAxis, seamlessYAxis, shouldUseCpuNoise } = useAppSelector(selector); const { t } = useTranslation(); const activeLabel = useMemo(() => { @@ -34,7 +34,7 @@ export default function ParamAdvancedCollapse() { activeLabel.push(t('parameters.gpuNoise')); } - if (clipSkip > 0) { + if (clipSkip > 0 && model && model.base_model !== 'sdxl') { activeLabel.push( t('parameters.clipSkipWithLayerCount', { layerCount: clipSkip }) ); @@ -49,15 +49,19 @@ export default function ParamAdvancedCollapse() { } return activeLabel.join(', '); - }, [clipSkip, seamlessXAxis, seamlessYAxis, shouldUseCpuNoise, t]); + }, [clipSkip, model, seamlessXAxis, seamlessYAxis, shouldUseCpuNoise, t]); return ( - - + {model && model?.base_model !== 'sdxl' && ( + <> + + + + )} diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Advanced/ParamClipSkip.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Advanced/ParamClipSkip.tsx index a7d3d3c655..bff8120b7b 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Advanced/ParamClipSkip.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Advanced/ParamClipSkip.tsx @@ -1,6 +1,6 @@ import { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; +import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover'; import IAISlider from 'common/components/IAISlider'; import { setClipSkip } from 'features/parameters/store/generationSlice'; import { clipSkipMap } from 'features/parameters/types/constants'; @@ -42,8 +42,12 @@ export default function ParamClipSkip() { return clipSkipMap[model.base_model].markers; }, [model]); + if (model?.base_model === 'sdxl') { + return null; + } + return ( - + - - - - - {t('parameters.aspectRatio')} - - - - - - } - fontSize={20} - onClick={handleToggleSize} - /> - } - isChecked={shouldLockAspectRatio} - onClick={handleLockRatio} - /> - + + + {t('parameters.aspectRatio')} + + + } + fontSize={20} + onClick={handleToggleSize} + /> + } + isChecked={shouldLockAspectRatio} + onClick={handleLockRatio} + /> + + diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx index 62bfca901c..e3d6501e1d 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx @@ -1,6 +1,6 @@ import type { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; +import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover'; import { IAISelectDataType } from 'common/components/IAIMantineSearchableSelect'; import IAIMantineSelect from 'common/components/IAIMantineSelect'; import { setCanvasCoherenceMode } from 'features/parameters/store/generationSlice'; @@ -31,7 +31,7 @@ const ParamCanvasCoherenceMode = () => { }; return ( - + { const { t } = useTranslation(); return ( - + { const { t } = useTranslation(); return ( - + + + { return ( - + - + diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamInfillMethod.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamInfillMethod.tsx index 7a4ac1601d..cf4c9cdb0c 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamInfillMethod.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamInfillMethod.tsx @@ -2,7 +2,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; +import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover'; import IAIMantineSelect from 'common/components/IAIMantineSelect'; import { setInfillMethod } from 'features/parameters/store/generationSlice'; @@ -40,7 +40,7 @@ const ParamInfillMethod = () => { ); return ( - + { }; return ( - + { - const { controlNets, isEnabled, isIPAdapterEnabled } = controlNet; + const { controlNets, isEnabled, isIPAdapterEnabled, ipAdapterInfo } = + controlNet; const validControlNets = getValidControlNets(controlNets); - + const isIPAdapterValid = ipAdapterInfo.model && ipAdapterInfo.adapterImage; let activeLabel = undefined; if (isEnabled && validControlNets.length > 0) { activeLabel = `${validControlNets.length} ControlNet`; } - if (isIPAdapterEnabled) { + if (isIPAdapterEnabled && isIPAdapterValid) { if (activeLabel) { activeLabel = `${activeLabel}, IP Adapter`; } else { diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamAspectRatio.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamAspectRatio.tsx index 05e0b09cee..3a5cc264e7 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamAspectRatio.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamAspectRatio.tsx @@ -1,4 +1,4 @@ -import { ButtonGroup, Flex } from '@chakra-ui/react'; +import { ButtonGroup } from '@chakra-ui/react'; import { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIButton from 'common/components/IAIButton'; @@ -29,25 +29,23 @@ export default function ParamAspectRatio() { const activeTabName = useAppSelector(activeTabNameSelector); return ( - - - {aspectRatios.map((ratio) => ( - { - dispatch(setAspectRatio(ratio.value)); - dispatch(setShouldLockAspectRatio(false)); - }} - > - {ratio.name} - - ))} - - + + {aspectRatios.map((ratio) => ( + { + dispatch(setAspectRatio(ratio.value)); + dispatch(setShouldLockAspectRatio(false)); + }} + > + {ratio.name} + + ))} + ); } diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamCFGScale.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamCFGScale.tsx index 51d6edc2dc..233a32ff09 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamCFGScale.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamCFGScale.tsx @@ -2,7 +2,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; +import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover'; import IAINumberInput from 'common/components/IAINumberInput'; import IAISlider from 'common/components/IAISlider'; import { setCfgScale } from 'features/parameters/store/generationSlice'; @@ -54,7 +54,7 @@ const ParamCFGScale = () => { ); return shouldUseSliders ? ( - + { /> ) : ( - + { }, [dispatch, initial]); return asSlider || shouldUseSliders ? ( - + { /> ) : ( - + { const negativePrompt = useAppSelector( @@ -82,7 +82,10 @@ const ParamNegativeConditioning = () => { onClose={onClose} onSelect={handleSelectEmbedding} > - + { onClose={onClose} onSelect={handleSelectEmbedding} > - + { ); return ( - + - - - - - {t('parameters.aspectRatio')} - - - - - - } - fontSize={20} - isDisabled={ - activeTabName === 'img2img' ? !shouldFitToWidthHeight : false - } - onClick={handleToggleSize} - /> - } - isChecked={shouldLockAspectRatio} - isDisabled={ - activeTabName === 'img2img' ? !shouldFitToWidthHeight : false - } - onClick={handleLockRatio} - /> - + + + {t('parameters.aspectRatio')} + + + } + fontSize={20} + isDisabled={ + activeTabName === 'img2img' ? !shouldFitToWidthHeight : false + } + onClick={handleToggleSize} + /> + } + isChecked={shouldLockAspectRatio} + isDisabled={ + activeTabName === 'img2img' ? !shouldFitToWidthHeight : false + } + onClick={handleLockRatio} + /> + + { }, [dispatch]); return shouldUseSliders ? ( - + { /> ) : ( - + { }, [dispatch, initial]); return ( - - + + { withReset sliderNumberInputProps={{ max: inputMax }} /> - - + + ); }; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/MainModel/ParamMainModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/MainModel/ParamMainModelSelect.tsx index 9c957523bc..f4d7421eed 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/MainModel/ParamMainModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/MainModel/ParamMainModelSelect.tsx @@ -21,7 +21,7 @@ import { useGetOnnxModelsQuery, } from 'services/api/endpoints/models'; import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus'; -import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; +import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover'; const selector = createSelector( stateSelector, @@ -119,8 +119,8 @@ const ParamMainModelSelect = () => { data={[]} /> ) : ( - - + + { onChange={handleChangeModel} w="100%" /> - {isSyncModelEnabled && ( - - - - )} - - + + {isSyncModelEnabled && ( + + + + )} + ); }; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Noise/ParamCpuNoise.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Noise/ParamCpuNoise.tsx index b3c8aea415..0f98d9f384 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Noise/ParamCpuNoise.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Noise/ParamCpuNoise.tsx @@ -1,5 +1,5 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; +import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover'; import IAISwitch from 'common/components/IAISwitch'; import { shouldUseCpuNoiseChanged } from 'features/parameters/store/generationSlice'; import { ChangeEvent, useCallback } from 'react'; @@ -20,7 +20,7 @@ export const ParamCpuNoiseToggle = () => { ); return ( - + { return ( - + diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/SubParametersWrapper.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/SubParametersWrapper.tsx index 65b565e926..2098503a1f 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/SubParametersWrapper.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/SubParametersWrapper.tsx @@ -1,40 +1,28 @@ -import { Flex, Text } from '@chakra-ui/react'; -import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; +import { Flex, Text, forwardRef } from '@chakra-ui/react'; import { ReactNode, memo } from 'react'; type SubParameterWrapperProps = { - children: ReactNode | ReactNode[]; + children: ReactNode; label?: string; - headerInfoPopover?: string; }; -const SubParametersWrapper = (props: SubParameterWrapperProps) => ( - - {props.headerInfoPopover && props.label && ( - - - {props.label} - - - )} - {!props.headerInfoPopover && props.label && ( +const SubParametersWrapper = forwardRef( + (props: SubParameterWrapperProps, ref) => ( + ( > {props.label} - )} - {props.children} - + {props.children} + + ) ); SubParametersWrapper.displayName = 'SubSettingsWrapper'; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/VAEModel/ParamVAEModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/VAEModel/ParamVAEModelSelect.tsx index c551562962..ee8979469b 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/VAEModel/ParamVAEModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/VAEModel/ParamVAEModelSelect.tsx @@ -15,7 +15,7 @@ import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectI import { vaeSelected } from 'features/parameters/store/generationSlice'; import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; import { modelIdToVAEModelParam } from 'features/parameters/util/modelIdToVAEModelParam'; -import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; +import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover'; const selector = createSelector( stateSelector, @@ -94,7 +94,7 @@ const ParamVAEModelSelect = () => { ); return ( - + { ); return ( - + { +export const usePreselectedImage = (imageName?: string) => { const dispatch = useAppDispatch(); - const [imageNameForDto, setImageNameForDto] = useState(); - const [imageNameForMetadata, setImageNameForMetadata] = useState< - string | undefined - >(); + const { recallAllParameters } = useRecallParameters(); const toaster = useAppToaster(); const { currentData: selectedImageDto } = useGetImageDTOQuery( - imageNameForDto ?? skipToken + imageName ?? skipToken ); const { currentData: selectedImageMetadata } = useGetImageMetadataQuery( - imageNameForMetadata ?? skipToken + imageName ?? skipToken ); - const handlePreselectedImage = useCallback( - (selectedImage?: SelectedImage) => { - if (!selectedImage) { - return; - } + const handleSendToCanvas = useCallback(() => { + if (selectedImageDto) { + dispatch(setInitialCanvasImage(selectedImageDto)); + dispatch(setActiveTab('unifiedCanvas')); + toaster({ + title: t('toast.sentToUnifiedCanvas'), + status: 'info', + duration: 2500, + isClosable: true, + }); + } + }, [dispatch, toaster, selectedImageDto]); - if (selectedImage.action === 'sendToCanvas') { - setImageNameForDto(selectedImage?.imageName); - if (selectedImageDto) { - dispatch(setInitialCanvasImage(selectedImageDto)); - dispatch(setActiveTab('unifiedCanvas')); - toaster({ - title: t('toast.sentToUnifiedCanvas'), - status: 'info', - duration: 2500, - isClosable: true, - }); - } - } + const handleSendToImg2Img = useCallback(() => { + if (selectedImageDto) { + dispatch(initialImageSelected(selectedImageDto)); + } + }, [dispatch, selectedImageDto]); - if (selectedImage.action === 'sendToImg2Img') { - setImageNameForDto(selectedImage?.imageName); - if (selectedImageDto) { - dispatch(initialImageSelected(selectedImageDto)); - } - } + const handleUseAllMetadata = useCallback(() => { + if (selectedImageMetadata) { + recallAllParameters(selectedImageMetadata.metadata as CoreMetadata); + } + // disabled because `recallAllParameters` changes the model, but its dep to prepare LoRAs has model as a dep. this introduces circular logic that causes infinite re-renders + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedImageMetadata]); - if (selectedImage.action === 'useAllParameters') { - setImageNameForMetadata(selectedImage?.imageName); - if (selectedImageMetadata) { - recallAllParameters(selectedImageMetadata.metadata as CoreMetadata); - } - } - }, - [ - dispatch, - selectedImageDto, - selectedImageMetadata, - recallAllParameters, - toaster, - ] - ); - - return { handlePreselectedImage }; + return { handleSendToCanvas, handleSendToImg2Img, handleUseAllMetadata }; }; diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts b/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts index bc850df0d0..d8561ab122 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts @@ -2,7 +2,11 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppToaster } from 'app/components/Toaster'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { CoreMetadata, LoRAMetadataItem } from 'features/nodes/types/types'; +import { + CoreMetadata, + LoRAMetadataItem, + ControlNetMetadataItem, +} from 'features/nodes/types/types'; import { refinerModelChanged, setNegativeStylePromptSDXL, @@ -18,10 +22,19 @@ import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { ImageDTO } from 'services/api/types'; import { + controlNetModelsAdapter, loraModelsAdapter, + useGetControlNetModelsQuery, useGetLoRAModelsQuery, } from '../../../services/api/endpoints/models'; -import { loraRecalled } from '../../lora/store/loraSlice'; +import { + ControlNetConfig, + controlNetEnabled, + controlNetRecalled, + controlNetReset, + initialControlNet, +} from '../../controlNet/store/controlNetSlice'; +import { loraRecalled, lorasCleared } from '../../lora/store/loraSlice'; import { initialImageSelected, modelSelected } from '../store/actions'; import { setCfgScale, @@ -38,6 +51,7 @@ import { isValidCfgScale, isValidHeight, isValidLoRAModel, + isValidControlNetModel, isValidMainModel, isValidNegativePrompt, isValidPositivePrompt, @@ -53,6 +67,11 @@ import { isValidStrength, isValidWidth, } from '../types/parameterSchemas'; +import { v4 as uuidv4 } from 'uuid'; +import { + CONTROLNET_PROCESSORS, + CONTROLNET_MODEL_DEFAULT_PROCESSORS, +} from 'features/controlNet/store/constants'; const selector = createSelector(stateSelector, ({ generation }) => { const { model } = generation; @@ -390,6 +409,121 @@ export const useRecallParameters = () => { [prepareLoRAMetadataItem, dispatch, parameterSetToast, parameterNotSetToast] ); + /** + * Recall ControlNet with toast + */ + + const { controlnets } = useGetControlNetModelsQuery(undefined, { + selectFromResult: (result) => ({ + controlnets: result.data + ? controlNetModelsAdapter.getSelectors().selectAll(result.data) + : [], + }), + }); + + const prepareControlNetMetadataItem = useCallback( + (controlnetMetadataItem: ControlNetMetadataItem) => { + if (!isValidControlNetModel(controlnetMetadataItem.control_model)) { + return { controlnet: null, error: 'Invalid ControlNet model' }; + } + + const { + image, + control_model, + control_weight, + begin_step_percent, + end_step_percent, + control_mode, + resize_mode, + } = controlnetMetadataItem; + + const matchingControlNetModel = controlnets.find( + (c) => + c.base_model === control_model.base_model && + c.model_name === control_model.model_name + ); + + if (!matchingControlNetModel) { + return { controlnet: null, error: 'ControlNet model is not installed' }; + } + + const isCompatibleBaseModel = + matchingControlNetModel?.base_model === model?.base_model; + + if (!isCompatibleBaseModel) { + return { + controlnet: null, + error: 'ControlNet incompatible with currently-selected model', + }; + } + + const controlNetId = uuidv4(); + + let processorType = initialControlNet.processorType; + for (const modelSubstring in CONTROLNET_MODEL_DEFAULT_PROCESSORS) { + if (matchingControlNetModel.model_name.includes(modelSubstring)) { + processorType = + CONTROLNET_MODEL_DEFAULT_PROCESSORS[modelSubstring] || + initialControlNet.processorType; + break; + } + } + const processorNode = CONTROLNET_PROCESSORS[processorType].default; + + const controlnet: ControlNetConfig = { + isEnabled: true, + model: matchingControlNetModel, + weight: + typeof control_weight === 'number' + ? control_weight + : initialControlNet.weight, + beginStepPct: begin_step_percent || initialControlNet.beginStepPct, + endStepPct: end_step_percent || initialControlNet.endStepPct, + controlMode: control_mode || initialControlNet.controlMode, + resizeMode: resize_mode || initialControlNet.resizeMode, + controlImage: image?.image_name || null, + processedControlImage: image?.image_name || null, + processorType, + processorNode: + processorNode.type !== 'none' + ? processorNode + : initialControlNet.processorNode, + shouldAutoConfig: true, + controlNetId, + }; + + return { controlnet, error: null }; + }, + [controlnets, model?.base_model] + ); + + const recallControlNet = useCallback( + (controlnetMetadataItem: ControlNetMetadataItem) => { + const result = prepareControlNetMetadataItem(controlnetMetadataItem); + + if (!result.controlnet) { + parameterNotSetToast(result.error); + return; + } + + dispatch( + controlNetRecalled({ + ...result.controlnet, + }) + ); + + dispatch(controlNetEnabled()); + + parameterSetToast(); + }, + [ + prepareControlNetMetadataItem, + dispatch, + parameterSetToast, + parameterNotSetToast, + ] + ); + /* * Sets image as initial image with toast */ @@ -428,6 +562,7 @@ export const useRecallParameters = () => { refiner_negative_aesthetic_score, refiner_start, loras, + controlnets, } = metadata; if (isValidCfgScale(cfg_scale)) { @@ -509,6 +644,7 @@ export const useRecallParameters = () => { dispatch(setRefinerStart(refiner_start)); } + dispatch(lorasCleared()); loras?.forEach((lora) => { const result = prepareLoRAMetadataItem(lora); if (result.lora) { @@ -516,6 +652,15 @@ export const useRecallParameters = () => { } }); + dispatch(controlNetReset()); + dispatch(controlNetEnabled()); + controlnets?.forEach((controlnet) => { + const result = prepareControlNetMetadataItem(controlnet); + if (result.controlnet) { + dispatch(controlNetRecalled(result.controlnet)); + } + }); + allParameterSetToast(); }, [ @@ -523,6 +668,7 @@ export const useRecallParameters = () => { allParameterSetToast, dispatch, prepareLoRAMetadataItem, + prepareControlNetMetadataItem, ] ); @@ -541,6 +687,7 @@ export const useRecallParameters = () => { recallHeight, recallStrength, recallLoRA, + recallControlNet, recallAllParameters, sendToImageToImage, }; diff --git a/invokeai/frontend/web/src/features/queue/components/ClearInvocationCacheButton.tsx b/invokeai/frontend/web/src/features/queue/components/ClearInvocationCacheButton.tsx new file mode 100644 index 0000000000..941bc88125 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/components/ClearInvocationCacheButton.tsx @@ -0,0 +1,22 @@ +import IAIButton from 'common/components/IAIButton'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useClearInvocationCache } from '../hooks/useClearInvocationCache'; + +const ClearInvocationCacheButton = () => { + const { t } = useTranslation(); + const { clearInvocationCache, isDisabled, isLoading } = + useClearInvocationCache(); + + return ( + + {t('invocationCache.clear')} + + ); +}; + +export default memo(ClearInvocationCacheButton); diff --git a/invokeai/frontend/web/src/features/queue/components/InvocationCacheStatus.tsx b/invokeai/frontend/web/src/features/queue/components/InvocationCacheStatus.tsx new file mode 100644 index 0000000000..1720f81285 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/components/InvocationCacheStatus.tsx @@ -0,0 +1,44 @@ +import { ButtonGroup } from '@chakra-ui/react'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useGetInvocationCacheStatusQuery } from 'services/api/endpoints/appInfo'; +import ClearInvocationCacheButton from './ClearInvocationCacheButton'; +import ToggleInvocationCacheButton from './ToggleInvocationCacheButton'; +import StatusStatGroup from './common/StatusStatGroup'; +import StatusStatItem from './common/StatusStatItem'; + +const InvocationCacheStatus = () => { + const { t } = useTranslation(); + const { data: cacheStatus } = useGetInvocationCacheStatusQuery(undefined); + + return ( + + + + + + + + + + + ); +}; + +export default memo(InvocationCacheStatus); diff --git a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemSkeleton.tsx b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemSkeleton.tsx new file mode 100644 index 0000000000..529c46af74 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemSkeleton.tsx @@ -0,0 +1,41 @@ +import { Flex, Skeleton } from '@chakra-ui/react'; +import { memo } from 'react'; +import { COLUMN_WIDTHS } from './constants'; + +const QueueItemSkeleton = () => { + return ( + + + +   + + + + +   + + + + +   + + + + +   + + + + +   + + + + ); +}; + +export default memo(QueueItemSkeleton); diff --git a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueList.tsx b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueList.tsx index dea1443489..e136e6df6c 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueList.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueList.tsx @@ -3,6 +3,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { IAINoContentFallbackWithSpinner } from 'common/components/IAIImageFallback'; import { listCursorChanged, listPriorityChanged, @@ -85,7 +86,7 @@ const QueueList = () => { return () => osInstance()?.destroy(); }, [scroller, initialize, osInstance]); - const { data: listQueueItemsData } = useListQueueItemsQuery({ + const { data: listQueueItemsData, isLoading } = useListQueueItemsQuery({ cursor: listCursor, priority: listPriority, }); @@ -125,36 +126,40 @@ const QueueList = () => { [openQueueItems, toggleQueueItem] ); + if (isLoading) { + return ; + } + + if (!queueItems.length) { + return ( + + + {t('queue.queueEmpty')} + + + ); + } + return ( - {queueItems.length ? ( - <> - - - - data={queueItems} - endReached={handleLoadMore} - scrollerRef={setScroller as TableVirtuosoScrollerRef} - itemContent={itemContent} - computeItemKey={computeItemKey} - components={components} - context={context} - /> - - - ) : ( - - - {t('queue.queueEmpty')} - - - )} + + + + data={queueItems} + endReached={handleLoadMore} + scrollerRef={setScroller as TableVirtuosoScrollerRef} + itemContent={itemContent} + computeItemKey={computeItemKey} + components={components} + context={context} + /> + ); }; diff --git a/invokeai/frontend/web/src/features/queue/components/QueueStatus.tsx b/invokeai/frontend/web/src/features/queue/components/QueueStatus.tsx index 72dfdd77ad..a91d3c4679 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueStatus.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueStatus.tsx @@ -1,38 +1,39 @@ -import { Stat, StatGroup, StatLabel, StatNumber } from '@chakra-ui/react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetQueueStatusQuery } from 'services/api/endpoints/queue'; +import StatusStatGroup from './common/StatusStatGroup'; +import StatusStatItem from './common/StatusStatItem'; const QueueStatus = () => { const { data: queueStatus } = useGetQueueStatusQuery(); const { t } = useTranslation(); return ( - - - {t('queue.in_progress')} - {queueStatus?.queue.in_progress ?? 0} - - - {t('queue.pending')} - {queueStatus?.queue.pending ?? 0} - - - {t('queue.completed')} - {queueStatus?.queue.completed ?? 0} - - - {t('queue.failed')} - {queueStatus?.queue.failed ?? 0} - - - {t('queue.canceled')} - {queueStatus?.queue.canceled ?? 0} - - - {t('queue.total')} - {queueStatus?.queue.total} - - + + + + + + + + ); }; diff --git a/invokeai/frontend/web/src/features/queue/components/QueueTabContent.tsx b/invokeai/frontend/web/src/features/queue/components/QueueTabContent.tsx index 254e34c453..23e2fe399f 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueTabContent.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueTabContent.tsx @@ -1,16 +1,14 @@ -import { Box, ButtonGroup, Flex } from '@chakra-ui/react'; +import { Box, Flex } from '@chakra-ui/react'; import { memo } from 'react'; -import ClearQueueButton from './ClearQueueButton'; -import PauseProcessorButton from './PauseProcessorButton'; -import PruneQueueButton from './PruneQueueButton'; +import { useFeatureStatus } from '../../system/hooks/useFeatureStatus'; +import InvocationCacheStatus from './InvocationCacheStatus'; import QueueList from './QueueList/QueueList'; import QueueStatus from './QueueStatus'; -import ResumeProcessorButton from './ResumeProcessorButton'; -import { useFeatureStatus } from '../../system/hooks/useFeatureStatus'; +import QueueTabQueueControls from './QueueTabQueueControls'; const QueueTabContent = () => { - const isPauseEnabled = useFeatureStatus('pauseQueue').isFeatureEnabled; - const isResumeEnabled = useFeatureStatus('resumeQueue').isFeatureEnabled; + const isInvocationCacheEnabled = + useFeatureStatus('invocationCache').isFeatureEnabled; return ( { gap={2} > - - {isPauseEnabled || isResumeEnabled ? ( - - {isResumeEnabled ? : <>} - {isPauseEnabled ? : <>} - - ) : ( - <> - )} - - - - - - - - - {/* - - */} + + + {isInvocationCacheEnabled && } diff --git a/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx b/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx new file mode 100644 index 0000000000..f8bf5d64d5 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx @@ -0,0 +1,30 @@ +import { ButtonGroup, Flex } from '@chakra-ui/react'; +import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; +import { memo } from 'react'; +import ClearQueueButton from './ClearQueueButton'; +import PauseProcessorButton from './PauseProcessorButton'; +import PruneQueueButton from './PruneQueueButton'; +import ResumeProcessorButton from './ResumeProcessorButton'; + +const QueueTabQueueControls = () => { + const isPauseEnabled = useFeatureStatus('pauseQueue').isFeatureEnabled; + const isResumeEnabled = useFeatureStatus('resumeQueue').isFeatureEnabled; + return ( + + {isPauseEnabled || isResumeEnabled ? ( + + {isResumeEnabled ? : <>} + {isPauseEnabled ? : <>} + + ) : ( + <> + )} + + + + + + ); +}; + +export default memo(QueueTabQueueControls); diff --git a/invokeai/frontend/web/src/features/queue/components/ToggleInvocationCacheButton.tsx b/invokeai/frontend/web/src/features/queue/components/ToggleInvocationCacheButton.tsx new file mode 100644 index 0000000000..94b0fc82ae --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/components/ToggleInvocationCacheButton.tsx @@ -0,0 +1,47 @@ +import IAIButton from 'common/components/IAIButton'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useGetInvocationCacheStatusQuery } from 'services/api/endpoints/appInfo'; +import { useDisableInvocationCache } from '../hooks/useDisableInvocationCache'; +import { useEnableInvocationCache } from '../hooks/useEnableInvocationCache'; + +const ToggleInvocationCacheButton = () => { + const { t } = useTranslation(); + const { data: cacheStatus } = useGetInvocationCacheStatusQuery(); + + const { + enableInvocationCache, + isDisabled: isEnableDisabled, + isLoading: isEnableLoading, + } = useEnableInvocationCache(); + + const { + disableInvocationCache, + isDisabled: isDisableDisabled, + isLoading: isDisableLoading, + } = useDisableInvocationCache(); + + if (cacheStatus?.enabled) { + return ( + + {t('invocationCache.disable')} + + ); + } + + return ( + + {t('invocationCache.enable')} + + ); +}; + +export default memo(ToggleInvocationCacheButton); diff --git a/invokeai/frontend/web/src/features/queue/components/VerticalQueueControls.tsx b/invokeai/frontend/web/src/features/queue/components/VerticalQueueControls.tsx deleted file mode 100644 index 665eaf190a..0000000000 --- a/invokeai/frontend/web/src/features/queue/components/VerticalQueueControls.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { ButtonGroup, ButtonGroupProps, Flex } from '@chakra-ui/react'; -import { memo } from 'react'; -import ClearQueueButton from './ClearQueueButton'; -import PauseProcessorButton from './PauseProcessorButton'; -import PruneQueueButton from './PruneQueueButton'; -import ResumeProcessorButton from './ResumeProcessorButton'; - -type Props = ButtonGroupProps & { - asIconButtons?: boolean; -}; - -const VerticalQueueControls = ({ asIconButtons, ...rest }: Props) => { - return ( - - - - - - - - - - - ); -}; - -export default memo(VerticalQueueControls); diff --git a/invokeai/frontend/web/src/features/queue/components/common/StatusStatGroup.tsx b/invokeai/frontend/web/src/features/queue/components/common/StatusStatGroup.tsx new file mode 100644 index 0000000000..b083715472 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/components/common/StatusStatGroup.tsx @@ -0,0 +1,22 @@ +import { StatGroup, StatGroupProps } from '@chakra-ui/react'; +import { memo } from 'react'; + +const StatusStatGroup = ({ children, ...rest }: StatGroupProps) => ( + + {children} + +); + +export default memo(StatusStatGroup); diff --git a/invokeai/frontend/web/src/features/queue/components/common/StatusStatItem.tsx b/invokeai/frontend/web/src/features/queue/components/common/StatusStatItem.tsx new file mode 100644 index 0000000000..92100d4040 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/components/common/StatusStatItem.tsx @@ -0,0 +1,47 @@ +import { + ChakraProps, + Stat, + StatLabel, + StatNumber, + StatProps, +} from '@chakra-ui/react'; +import { memo } from 'react'; + +const sx: ChakraProps['sx'] = { + '&[aria-disabled="true"]': { + color: 'base.400', + _dark: { + color: 'base.500', + }, + }, +}; + +type Props = Omit & { + label: string; + value: string | number; + isDisabled?: boolean; +}; + +const StatusStatItem = ({ + label, + value, + isDisabled = false, + ...rest +}: Props) => ( + + + {label} + + {value} + +); + +export default memo(StatusStatItem); diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts index 308695dd67..1b07221a74 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts @@ -1,5 +1,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { addToast } from 'features/system/store/systemSlice'; +import { isNil } from 'lodash-es'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -40,7 +41,7 @@ export const useCancelCurrentQueueItem = () => { }, [currentQueueItemId, dispatch, t, trigger]); const isDisabled = useMemo( - () => !isConnected || !currentQueueItemId, + () => !isConnected || isNil(currentQueueItemId), [isConnected, currentQueueItemId] ); diff --git a/invokeai/frontend/web/src/features/queue/hooks/useClearInvocationCache.ts b/invokeai/frontend/web/src/features/queue/hooks/useClearInvocationCache.ts new file mode 100644 index 0000000000..ce2fc0e46b --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/hooks/useClearInvocationCache.ts @@ -0,0 +1,48 @@ +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { addToast } from 'features/system/store/systemSlice'; +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + useClearInvocationCacheMutation, + useGetInvocationCacheStatusQuery, +} from 'services/api/endpoints/appInfo'; + +export const useClearInvocationCache = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { data: cacheStatus } = useGetInvocationCacheStatusQuery(); + const isConnected = useAppSelector((state) => state.system.isConnected); + const [trigger, { isLoading }] = useClearInvocationCacheMutation({ + fixedCacheKey: 'clearInvocationCache', + }); + + const isDisabled = useMemo( + () => !cacheStatus?.size || !isConnected, + [cacheStatus?.size, isConnected] + ); + + const clearInvocationCache = useCallback(async () => { + if (isDisabled) { + return; + } + + try { + await trigger().unwrap(); + dispatch( + addToast({ + title: t('invocationCache.clearSucceeded'), + status: 'success', + }) + ); + } catch { + dispatch( + addToast({ + title: t('invocationCache.clearFailed'), + status: 'error', + }) + ); + } + }, [isDisabled, trigger, dispatch, t]); + + return { clearInvocationCache, isLoading, cacheStatus, isDisabled }; +}; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useDisableInvocationCache.ts b/invokeai/frontend/web/src/features/queue/hooks/useDisableInvocationCache.ts new file mode 100644 index 0000000000..8d3288aad2 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/hooks/useDisableInvocationCache.ts @@ -0,0 +1,48 @@ +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { addToast } from 'features/system/store/systemSlice'; +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + useDisableInvocationCacheMutation, + useGetInvocationCacheStatusQuery, +} from 'services/api/endpoints/appInfo'; + +export const useDisableInvocationCache = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { data: cacheStatus } = useGetInvocationCacheStatusQuery(); + const isConnected = useAppSelector((state) => state.system.isConnected); + const [trigger, { isLoading }] = useDisableInvocationCacheMutation({ + fixedCacheKey: 'disableInvocationCache', + }); + + const isDisabled = useMemo( + () => !cacheStatus?.enabled || !isConnected || cacheStatus?.max_size === 0, + [cacheStatus?.enabled, cacheStatus?.max_size, isConnected] + ); + + const disableInvocationCache = useCallback(async () => { + if (isDisabled) { + return; + } + + try { + await trigger().unwrap(); + dispatch( + addToast({ + title: t('invocationCache.disableSucceeded'), + status: 'success', + }) + ); + } catch { + dispatch( + addToast({ + title: t('invocationCache.disableFailed'), + status: 'error', + }) + ); + } + }, [isDisabled, trigger, dispatch, t]); + + return { disableInvocationCache, isLoading, cacheStatus, isDisabled }; +}; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnableInvocationCache.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnableInvocationCache.ts new file mode 100644 index 0000000000..2ffef29b19 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnableInvocationCache.ts @@ -0,0 +1,48 @@ +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { addToast } from 'features/system/store/systemSlice'; +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + useEnableInvocationCacheMutation, + useGetInvocationCacheStatusQuery, +} from 'services/api/endpoints/appInfo'; + +export const useEnableInvocationCache = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { data: cacheStatus } = useGetInvocationCacheStatusQuery(); + const isConnected = useAppSelector((state) => state.system.isConnected); + const [trigger, { isLoading }] = useEnableInvocationCacheMutation({ + fixedCacheKey: 'enableInvocationCache', + }); + + const isDisabled = useMemo( + () => cacheStatus?.enabled || !isConnected || cacheStatus?.max_size === 0, + [cacheStatus?.enabled, cacheStatus?.max_size, isConnected] + ); + + const enableInvocationCache = useCallback(async () => { + if (isDisabled) { + return; + } + + try { + await trigger().unwrap(); + dispatch( + addToast({ + title: t('invocationCache.enableSucceeded'), + status: 'success', + }) + ); + } catch { + dispatch( + addToast({ + title: t('invocationCache.enableFailed'), + status: 'error', + }) + ); + } + }, [isDisabled, trigger, dispatch, t]); + + return { enableInvocationCache, isLoading, cacheStatus, isDisabled }; +}; diff --git a/invokeai/frontend/web/src/features/queue/store/nanoStores.ts b/invokeai/frontend/web/src/features/queue/store/queueNanoStore.ts similarity index 100% rename from invokeai/frontend/web/src/features/queue/store/nanoStores.ts rename to invokeai/frontend/web/src/features/queue/store/queueNanoStore.ts diff --git a/invokeai/frontend/web/src/features/sdxl/components/ParamSDXLImg2ImgDenoisingStrength.tsx b/invokeai/frontend/web/src/features/sdxl/components/ParamSDXLImg2ImgDenoisingStrength.tsx index dc8ae775f9..cc58b8e7dd 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/ParamSDXLImg2ImgDenoisingStrength.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/ParamSDXLImg2ImgDenoisingStrength.tsx @@ -7,7 +7,7 @@ import SubParametersWrapper from 'features/parameters/components/Parameters/SubP import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { setSDXLImg2ImgDenoisingStrength } from '../store/sdxlSlice'; -import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; +import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover'; const selector = createSelector( [stateSelector], @@ -36,8 +36,8 @@ const ParamSDXLImg2ImgDenoisingStrength = () => { }, [dispatch]); return ( - - + + { withSliderMarks withReset /> - - + + ); }; diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts index 7d31838afd..9a110f5f23 100644 --- a/invokeai/frontend/web/src/features/system/store/systemSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/systemSlice.ts @@ -1,9 +1,8 @@ import { UseToastOptions } from '@chakra-ui/react'; import { PayloadAction, createSlice, isAnyOf } from '@reduxjs/toolkit'; import { t } from 'i18next'; -import { get, startCase, truncate, upperFirst } from 'lodash-es'; +import { startCase } from 'lodash-es'; import { LogLevelName } from 'roarr'; -import { isAnySessionRejected } from 'services/api/thunks/session'; import { appSocketConnected, appSocketDisconnected, @@ -20,8 +19,7 @@ import { } from 'services/events/actions'; import { calculateStepPercentage } from '../util/calculateStepPercentage'; import { makeToast } from '../util/makeToast'; -import { SystemState, LANGUAGES } from './types'; -import { zPydanticValidationError } from './zodSchemas'; +import { LANGUAGES, SystemState } from './types'; export const initialSystemState: SystemState = { isInitialized: false, @@ -175,50 +173,6 @@ export const systemSlice = createSlice({ // *** Matchers - must be after all cases *** - /** - * Session Invoked - REJECTED - * Session Created - REJECTED - */ - builder.addMatcher(isAnySessionRejected, (state, action) => { - let errorDescription = undefined; - const duration = 5000; - - if (action.payload?.status === 422) { - const result = zPydanticValidationError.safeParse(action.payload); - if (result.success) { - result.data.error.detail.map((e) => { - state.toastQueue.push( - makeToast({ - title: truncate(upperFirst(e.msg), { length: 128 }), - status: 'error', - description: truncate( - `Path: - ${e.loc.join('.')}`, - { length: 128 } - ), - duration, - }) - ); - }); - return; - } - } else if (action.payload?.error) { - errorDescription = action.payload?.error; - } - - state.toastQueue.push( - makeToast({ - title: t('toast.serverError'), - status: 'error', - description: truncate( - get(errorDescription, 'detail', 'Unknown Error'), - { length: 128 } - ), - duration, - }) - ); - }); - /** * Any server error */ diff --git a/invokeai/frontend/web/src/features/system/store/zodSchemas.ts b/invokeai/frontend/web/src/features/system/store/zodSchemas.ts index 3a3b950019..9d66f5ae88 100644 --- a/invokeai/frontend/web/src/features/system/store/zodSchemas.ts +++ b/invokeai/frontend/web/src/features/system/store/zodSchemas.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; export const zPydanticValidationError = z.object({ status: z.literal(422), - error: z.object({ + data: z.object({ detail: z.array( z.object({ loc: z.array(z.string()), diff --git a/invokeai/frontend/web/src/features/canvas/util/copyBlobToClipboard.ts b/invokeai/frontend/web/src/features/system/util/copyBlobToClipboard.ts similarity index 58% rename from invokeai/frontend/web/src/features/canvas/util/copyBlobToClipboard.ts rename to invokeai/frontend/web/src/features/system/util/copyBlobToClipboard.ts index e944e766b5..cf59f2a687 100644 --- a/invokeai/frontend/web/src/features/canvas/util/copyBlobToClipboard.ts +++ b/invokeai/frontend/web/src/features/system/util/copyBlobToClipboard.ts @@ -1,10 +1,13 @@ /** * Copies a blob to the clipboard by calling navigator.clipboard.write(). */ -export const copyBlobToClipboard = (blob: Blob) => { +export const copyBlobToClipboard = ( + blob: Promise, + type = 'image/png' +) => { navigator.clipboard.write([ new ClipboardItem({ - [blob.type]: blob, + [type]: blob, }), ]); }; diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index fb5756b121..ac7b8aa1c4 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -14,7 +14,7 @@ import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent'; import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup'; -import { InvokeTabName, tabMap } from 'features/ui/store/tabMap'; +import { InvokeTabName } from 'features/ui/store/tabMap'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { ResourceKey } from 'i18next'; import { isEqual } from 'lodash-es'; @@ -110,7 +110,7 @@ export const NO_GALLERY_TABS: InvokeTabName[] = ['modelManager', 'queue']; export const NO_SIDE_PANEL_TABS: InvokeTabName[] = ['modelManager', 'queue']; const InvokeTabs = () => { - const activeTab = useAppSelector(activeTabIndexSelector); + const activeTabIndex = useAppSelector(activeTabIndexSelector); const activeTabName = useAppSelector(activeTabNameSelector); const enabledTabs = useAppSelector(enabledTabsSelector); const { t } = useTranslation(); @@ -150,13 +150,13 @@ const InvokeTabs = () => { const handleTabChange = useCallback( (index: number) => { - const activeTabName = tabMap[index]; - if (!activeTabName) { + const tab = enabledTabs[index]; + if (!tab) { return; } - dispatch(setActiveTab(activeTabName)); + dispatch(setActiveTab(tab.id)); }, - [dispatch] + [dispatch, enabledTabs] ); const { @@ -216,8 +216,8 @@ const InvokeTabs = () => { return ( {layer === 'base' && ( - dispatch(setBrushColor(newColor))} - /> + > + dispatch(setBrushColor(newColor))} + /> + )} {layer === 'mask' && ( - dispatch(setMaskColor(newColor))} - /> + > + dispatch(setMaskColor(newColor))} + /> + )} diff --git a/invokeai/frontend/web/src/features/ui/hooks/useCopyImageToClipboard.ts b/invokeai/frontend/web/src/features/ui/hooks/useCopyImageToClipboard.ts index 314abad081..cefbf3d14e 100644 --- a/invokeai/frontend/web/src/features/ui/hooks/useCopyImageToClipboard.ts +++ b/invokeai/frontend/web/src/features/ui/hooks/useCopyImageToClipboard.ts @@ -1,6 +1,7 @@ import { useAppToaster } from 'app/components/Toaster'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard'; export const useCopyImageToClipboard = () => { const toaster = useAppToaster(); @@ -22,13 +23,13 @@ export const useCopyImageToClipboard = () => { }); } try { - const response = await fetch(image_url); - const blob = await response.blob(); - await navigator.clipboard.write([ - new ClipboardItem({ - [blob.type]: blob, - }), - ]); + const getImageBlob = async () => { + const response = await fetch(image_url); + return await response.blob(); + }; + + copyBlobToClipboard(getImageBlob()); + toaster({ title: t('toast.imageCopied'), status: 'success', diff --git a/invokeai/frontend/web/src/features/ui/store/extraReducers.ts b/invokeai/frontend/web/src/features/ui/store/extraReducers.ts deleted file mode 100644 index 9b134e1476..0000000000 --- a/invokeai/frontend/web/src/features/ui/store/extraReducers.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { InvokeTabName, tabMap } from './tabMap'; -import { UIState } from './uiTypes'; - -export const setActiveTabReducer = ( - state: UIState, - newActiveTab: number | InvokeTabName -) => { - if (typeof newActiveTab === 'number') { - state.activeTab = newActiveTab; - } else { - state.activeTab = tabMap.indexOf(newActiveTab); - } -}; diff --git a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts index 5427fa9d3b..99ee8d80f7 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts @@ -1,27 +1,23 @@ import { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'app/store/store'; -import { isEqual } from 'lodash-es'; - -import { InvokeTabName, tabMap } from './tabMap'; -import { UIState } from './uiTypes'; +import { isEqual, isString } from 'lodash-es'; +import { tabMap } from './tabMap'; export const activeTabNameSelector = createSelector( - (state: RootState) => state.ui, - (ui: UIState) => tabMap[ui.activeTab] as InvokeTabName, - { - memoizeOptions: { - equalityCheck: isEqual, - }, - } + (state: RootState) => state, + /** + * Previously `activeTab` was an integer, but now it's a string. + * Default to first tab in case user has integer. + */ + ({ ui }) => (isString(ui.activeTab) ? ui.activeTab : 'txt2img') ); export const activeTabIndexSelector = createSelector( - (state: RootState) => state.ui, - (ui: UIState) => ui.activeTab, - { - memoizeOptions: { - equalityCheck: isEqual, - }, + (state: RootState) => state, + ({ ui, config }) => { + const tabs = tabMap.filter((t) => !config.disabledTabs.includes(t)); + const idx = tabs.indexOf(ui.activeTab); + return idx === -1 ? 0 : idx; } ); diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index 82c9ef4e77..9782d0bfac 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -2,12 +2,11 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import { initialImageChanged } from 'features/parameters/store/generationSlice'; import { SchedulerParam } from 'features/parameters/types/parameterSchemas'; -import { setActiveTabReducer } from './extraReducers'; import { InvokeTabName } from './tabMap'; import { UIState } from './uiTypes'; export const initialUIState: UIState = { - activeTab: 0, + activeTab: 'txt2img', shouldShowImageDetails: false, shouldUseCanvasBetaLayout: false, shouldShowExistingModelsInSearch: false, @@ -26,7 +25,7 @@ export const uiSlice = createSlice({ initialState: initialUIState, reducers: { setActiveTab: (state, action: PayloadAction) => { - setActiveTabReducer(state, action.payload); + state.activeTab = action.payload; }, setShouldShowImageDetails: (state, action: PayloadAction) => { state.shouldShowImageDetails = action.payload; @@ -73,7 +72,7 @@ export const uiSlice = createSlice({ }, extraReducers(builder) { builder.addCase(initialImageChanged, (state) => { - setActiveTabReducer(state, 'img2img'); + state.activeTab = 'img2img'; }); }, }); diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index 41a359a651..1b9fee6989 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -1,4 +1,5 @@ import { SchedulerParam } from 'features/parameters/types/parameterSchemas'; +import { InvokeTabName } from './tabMap'; export type Coordinates = { x: number; @@ -13,7 +14,7 @@ export type Dimensions = { export type Rect = Coordinates & Dimensions; export interface UIState { - activeTab: number; + activeTab: InvokeTabName; shouldShowImageDetails: boolean; shouldUseCanvasBetaLayout: boolean; shouldShowExistingModelsInSearch: boolean; diff --git a/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts b/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts index 2d3537998d..ffe2afd2f6 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts @@ -1,4 +1,5 @@ import { api } from '..'; +import { paths } from '../schema'; import { AppConfig, AppVersion } from '../types'; export const appInfoApi = api.injectEndpoints({ @@ -19,7 +20,45 @@ export const appInfoApi = api.injectEndpoints({ providesTags: ['AppConfig'], keepUnusedDataFor: 86400000, // 1 day }), + getInvocationCacheStatus: build.query< + paths['/api/v1/app/invocation_cache/status']['get']['responses']['200']['content']['application/json'], + void + >({ + query: () => ({ + url: `app/invocation_cache/status`, + method: 'GET', + }), + providesTags: ['InvocationCacheStatus'], + }), + clearInvocationCache: build.mutation({ + query: () => ({ + url: `app/invocation_cache`, + method: 'DELETE', + }), + invalidatesTags: ['InvocationCacheStatus'], + }), + enableInvocationCache: build.mutation({ + query: () => ({ + url: `app/invocation_cache/enable`, + method: 'PUT', + }), + invalidatesTags: ['InvocationCacheStatus'], + }), + disableInvocationCache: build.mutation({ + query: () => ({ + url: `app/invocation_cache/disable`, + method: 'PUT', + }), + invalidatesTags: ['InvocationCacheStatus'], + }), }), }); -export const { useGetAppVersionQuery, useGetAppConfigQuery } = appInfoApi; +export const { + useGetAppVersionQuery, + useGetAppConfigQuery, + useClearInvocationCacheMutation, + useDisableInvocationCacheMutation, + useEnableInvocationCacheMutation, + useGetInvocationCacheStatusQuery, +} = appInfoApi; diff --git a/invokeai/frontend/web/src/services/api/endpoints/queue.ts b/invokeai/frontend/web/src/services/api/endpoints/queue.ts index 6d436b231f..4393ab7e81 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/queue.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/queue.ts @@ -4,7 +4,7 @@ import { ThunkDispatch, createEntityAdapter, } from '@reduxjs/toolkit'; -import { $queueId } from 'features/queue/store/nanoStores'; +import { $queueId } from 'features/queue/store/queueNanoStore'; import { listParamsReset } from 'features/queue/store/queueSlice'; import queryString from 'query-string'; import { ApiTagDescription, api } from '..'; diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index 8b4f886bb0..b39b11af29 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -24,6 +24,7 @@ export const tagTypes = [ 'SessionQueueStatus', 'SessionProcessorStatus', 'BatchStatus', + 'InvocationCacheStatus', ]; export type ApiTagDescription = TagDescription<(typeof tagTypes)[number]>; export const LIST_TAG = 'LIST'; diff --git a/invokeai/frontend/web/src/services/api/schema.d.ts b/invokeai/frontend/web/src/services/api/schema.d.ts index 26cb798594..cc47c0766d 100644 --- a/invokeai/frontend/web/src/services/api/schema.d.ts +++ b/invokeai/frontend/web/src/services/api/schema.d.ts @@ -317,6 +317,34 @@ export type paths = { */ post: operations["set_log_level"]; }; + "/api/v1/app/invocation_cache": { + /** + * Clear Invocation Cache + * @description Clears the invocation cache + */ + delete: operations["clear_invocation_cache"]; + }; + "/api/v1/app/invocation_cache/enable": { + /** + * Enable Invocation Cache + * @description Clears the invocation cache + */ + put: operations["enable_invocation_cache"]; + }; + "/api/v1/app/invocation_cache/disable": { + /** + * Disable Invocation Cache + * @description Clears the invocation cache + */ + put: operations["disable_invocation_cache"]; + }; + "/api/v1/app/invocation_cache/status": { + /** + * Get Invocation Cache Status + * @description Clears the invocation cache + */ + get: operations["get_invocation_cache_status"]; + }; "/api/v1/queue/{queue_id}/enqueue_graph": { /** * Enqueue Graph @@ -776,6 +804,17 @@ export type components = { */ image_count: number; }; + /** + * BoardField + * @description A board primitive field + */ + BoardField: { + /** + * Board Id + * @description The id of the board + */ + board_id: string; + }; /** Body_add_image_to_board */ Body_add_image_to_board: { /** @@ -1501,6 +1540,51 @@ export type components = { */ type: "color"; }; + /** + * Color Map Processor + * @description Generates a color map from the provided image + */ + ColorMapImageProcessorInvocation: { + /** + * Id + * @description The id of this instance of an invocation. Must be unique among all instances of invocations. + */ + id: string; + /** + * Is Intermediate + * @description Whether or not this is an intermediate invocation. + * @default false + */ + is_intermediate?: boolean; + /** + * Workflow + * @description The workflow to save with the image + */ + workflow?: string; + /** + * Use Cache + * @description Whether or not to use the cache + * @default true + */ + use_cache?: boolean; + /** + * Image + * @description The image to process + */ + image?: components["schemas"]["ImageField"]; + /** + * Type + * @default color_map_image_processor + * @enum {string} + */ + type: "color_map_image_processor"; + /** + * Color Map Tile Size + * @description Tile size + * @default 64 + */ + color_map_tile_size?: number; + }; /** * ColorOutput * @description Base class for nodes that output a single color @@ -2020,7 +2104,7 @@ export type components = { * Clip Skip * @description The number of skipped CLIP layers */ - clip_skip: number; + clip_skip?: number; /** * Model * @description The main model used for inference @@ -2309,10 +2393,7 @@ export type components = { * @enum {string} */ scheduler?: "ddim" | "ddpm" | "deis" | "lms" | "lms_k" | "pndm" | "heun" | "heun_k" | "euler" | "euler_k" | "euler_a" | "kdpm_2" | "kdpm_2_a" | "dpmpp_2s" | "dpmpp_2s_k" | "dpmpp_2m" | "dpmpp_2m_k" | "dpmpp_2m_sde" | "dpmpp_2m_sde_k" | "dpmpp_sde" | "dpmpp_sde_k" | "unipc"; - /** - * Control - * @description ControlNet(s) to apply - */ + /** Control */ control?: components["schemas"]["ControlField"] | components["schemas"]["ControlField"][]; /** * IP-Adapter @@ -2899,7 +2980,7 @@ export type components = { * @description The nodes in this graph */ nodes?: { - [key: string]: components["schemas"]["BooleanInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["ONNXPromptInvocation"] | components["schemas"]["ONNXTextToLatentsInvocation"] | components["schemas"]["ONNXLatentsToImageInvocation"] | components["schemas"]["OnnxModelLoaderInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"]; + [key: string]: components["schemas"]["BooleanInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ONNXPromptInvocation"] | components["schemas"]["ONNXTextToLatentsInvocation"] | components["schemas"]["ONNXLatentsToImageInvocation"] | components["schemas"]["OnnxModelLoaderInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"]; }; /** * Edges @@ -2942,7 +3023,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["BooleanOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["VaeLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["MetadataAccumulatorOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["ONNXModelLoaderOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["String2Output"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["CollectInvocationOutput"]; + [key: string]: components["schemas"]["BooleanOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["VaeLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["MetadataAccumulatorOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["ONNXModelLoaderOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["String2Output"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["CollectInvocationOutput"]; }; /** * Errors @@ -4718,6 +4799,34 @@ export type components = { */ type: "integer_output"; }; + /** InvocationCacheStatus */ + InvocationCacheStatus: { + /** + * Size + * @description The current size of the invocation cache + */ + size: number; + /** + * Hits + * @description The number of cache hits + */ + hits: number; + /** + * Misses + * @description The number of cache misses + */ + misses: number; + /** + * Enabled + * @description Whether the invocation cache is enabled + */ + enabled: boolean; + /** + * Max Size + * @description The maximum size of the invocation cache + */ + max_size: number; + }; /** * IterateInvocation * @description Iterates over a list of items @@ -7499,9 +7608,14 @@ export type components = { use_cache?: boolean; /** * Image - * @description The image to load + * @description The image to process */ image?: components["schemas"]["ImageField"]; + /** + * Board + * @description The board to save the image to + */ + board?: components["schemas"]["BoardField"]; /** * Metadata * @description Optional core metadata to be written to image @@ -7809,16 +7923,6 @@ export type components = { * @description The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed. */ session_id: string; - /** - * Field Values - * @description The field values that were used for this queue item - */ - field_values?: components["schemas"]["NodeFieldValue"][]; - /** - * Queue Id - * @description The id of the queue with which this item is associated - */ - queue_id: string; /** * Error * @description The error message if this queue item errored @@ -7844,6 +7948,16 @@ export type components = { * @description When this queue item was completed */ completed_at?: string; + /** + * Queue Id + * @description The id of the queue with which this item is associated + */ + queue_id: string; + /** + * Field Values + * @description The field values that were used for this queue item + */ + field_values?: components["schemas"]["NodeFieldValue"][]; /** * Session * @description The fully-populated session to be executed @@ -7883,16 +7997,6 @@ export type components = { * @description The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed. */ session_id: string; - /** - * Field Values - * @description The field values that were used for this queue item - */ - field_values?: components["schemas"]["NodeFieldValue"][]; - /** - * Queue Id - * @description The id of the queue with which this item is associated - */ - queue_id: string; /** * Error * @description The error message if this queue item errored @@ -7918,6 +8022,16 @@ export type components = { * @description When this queue item was completed */ completed_at?: string; + /** + * Queue Id + * @description The id of the queue with which this item is associated + */ + queue_id: string; + /** + * Field Values + * @description The field values that were used for this queue item + */ + field_values?: components["schemas"]["NodeFieldValue"][]; }; /** SessionQueueStatus */ SessionQueueStatus: { @@ -8999,7 +9113,7 @@ export type components = { * If a field should be provided a data type that does not exactly match the python type of the field, use this to provide the type that should be used instead. See the node development docs for detail on adding a new field type, which involves client-side changes. * @enum {string} */ - UIType: "boolean" | "ColorField" | "ConditioningField" | "ControlField" | "float" | "ImageField" | "integer" | "LatentsField" | "string" | "BooleanCollection" | "ColorCollection" | "ConditioningCollection" | "ControlCollection" | "FloatCollection" | "ImageCollection" | "IntegerCollection" | "LatentsCollection" | "StringCollection" | "BooleanPolymorphic" | "ColorPolymorphic" | "ConditioningPolymorphic" | "ControlPolymorphic" | "FloatPolymorphic" | "ImagePolymorphic" | "IntegerPolymorphic" | "LatentsPolymorphic" | "StringPolymorphic" | "MainModelField" | "SDXLMainModelField" | "SDXLRefinerModelField" | "ONNXModelField" | "VaeModelField" | "LoRAModelField" | "ControlNetModelField" | "IPAdapterModelField" | "UNetField" | "VaeField" | "ClipField" | "Collection" | "CollectionItem" | "enum" | "Scheduler" | "WorkflowField" | "IsIntermediate" | "MetadataField"; + UIType: "boolean" | "ColorField" | "ConditioningField" | "ControlField" | "float" | "ImageField" | "integer" | "LatentsField" | "string" | "BooleanCollection" | "ColorCollection" | "ConditioningCollection" | "ControlCollection" | "FloatCollection" | "ImageCollection" | "IntegerCollection" | "LatentsCollection" | "StringCollection" | "BooleanPolymorphic" | "ColorPolymorphic" | "ConditioningPolymorphic" | "ControlPolymorphic" | "FloatPolymorphic" | "ImagePolymorphic" | "IntegerPolymorphic" | "LatentsPolymorphic" | "StringPolymorphic" | "MainModelField" | "SDXLMainModelField" | "SDXLRefinerModelField" | "ONNXModelField" | "VaeModelField" | "LoRAModelField" | "ControlNetModelField" | "IPAdapterModelField" | "UNetField" | "VaeField" | "ClipField" | "Collection" | "CollectionItem" | "enum" | "Scheduler" | "WorkflowField" | "IsIntermediate" | "MetadataField" | "BoardField"; /** * UIComponent * @description The type of UI component to use for a field, used to override the default components, which are inferred from the field type. @@ -9043,17 +9157,11 @@ export type components = { ui_order?: number; }; /** - * IPAdapterModelFormat + * CLIPVisionModelFormat * @description An enumeration. * @enum {string} */ - IPAdapterModelFormat: "invokeai"; - /** - * ControlNetModelFormat - * @description An enumeration. - * @enum {string} - */ - ControlNetModelFormat: "checkpoint" | "diffusers"; + CLIPVisionModelFormat: "diffusers"; /** * StableDiffusionOnnxModelFormat * @description An enumeration. @@ -9066,24 +9174,30 @@ export type components = { * @enum {string} */ StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; - /** - * StableDiffusion1ModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; - /** - * CLIPVisionModelFormat - * @description An enumeration. - * @enum {string} - */ - CLIPVisionModelFormat: "diffusers"; /** * StableDiffusionXLModelFormat * @description An enumeration. * @enum {string} */ StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; + /** + * IPAdapterModelFormat + * @description An enumeration. + * @enum {string} + */ + IPAdapterModelFormat: "invokeai"; + /** + * ControlNetModelFormat + * @description An enumeration. + * @enum {string} + */ + ControlNetModelFormat: "checkpoint" | "diffusers"; + /** + * StableDiffusion1ModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; }; responses: never; parameters: never; @@ -9210,7 +9324,7 @@ export type operations = { }; requestBody: { content: { - "application/json": components["schemas"]["BooleanInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["ONNXPromptInvocation"] | components["schemas"]["ONNXTextToLatentsInvocation"] | components["schemas"]["ONNXLatentsToImageInvocation"] | components["schemas"]["OnnxModelLoaderInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"]; + "application/json": components["schemas"]["BooleanInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ONNXPromptInvocation"] | components["schemas"]["ONNXTextToLatentsInvocation"] | components["schemas"]["ONNXLatentsToImageInvocation"] | components["schemas"]["OnnxModelLoaderInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"]; }; }; responses: { @@ -9252,7 +9366,7 @@ export type operations = { }; requestBody: { content: { - "application/json": components["schemas"]["BooleanInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["ONNXPromptInvocation"] | components["schemas"]["ONNXTextToLatentsInvocation"] | components["schemas"]["ONNXLatentsToImageInvocation"] | components["schemas"]["OnnxModelLoaderInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"]; + "application/json": components["schemas"]["BooleanInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ONNXPromptInvocation"] | components["schemas"]["ONNXTextToLatentsInvocation"] | components["schemas"]["ONNXLatentsToImageInvocation"] | components["schemas"]["OnnxModelLoaderInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"]; }; }; responses: { @@ -10505,6 +10619,62 @@ export type operations = { }; }; }; + /** + * Clear Invocation Cache + * @description Clears the invocation cache + */ + clear_invocation_cache: { + responses: { + /** @description The operation was successful */ + 200: { + content: { + "application/json": unknown; + }; + }; + }; + }; + /** + * Enable Invocation Cache + * @description Clears the invocation cache + */ + enable_invocation_cache: { + responses: { + /** @description The operation was successful */ + 200: { + content: { + "application/json": unknown; + }; + }; + }; + }; + /** + * Disable Invocation Cache + * @description Clears the invocation cache + */ + disable_invocation_cache: { + responses: { + /** @description The operation was successful */ + 200: { + content: { + "application/json": unknown; + }; + }; + }; + }; + /** + * Get Invocation Cache Status + * @description Clears the invocation cache + */ + get_invocation_cache_status: { + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["InvocationCacheStatus"]; + }; + }; + }; + }; /** * Enqueue Graph * @description Enqueues a graph for single execution. diff --git a/invokeai/frontend/web/src/services/api/thunks/session.ts b/invokeai/frontend/web/src/services/api/thunks/session.ts deleted file mode 100644 index 2404329fac..0000000000 --- a/invokeai/frontend/web/src/services/api/thunks/session.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { createAsyncThunk, isAnyOf } from '@reduxjs/toolkit'; -import { $queueId } from 'features/queue/store/nanoStores'; -import { isObject } from 'lodash-es'; -import { $client } from 'services/api/client'; -import { paths } from 'services/api/schema'; -import { O } from 'ts-toolbelt'; - -type CreateSessionArg = { - graph: NonNullable< - paths['/api/v1/sessions/']['post']['requestBody'] - >['content']['application/json']; -}; - -type CreateSessionResponse = O.Required< - NonNullable< - paths['/api/v1/sessions/']['post']['requestBody'] - >['content']['application/json'], - 'id' ->; - -type CreateSessionThunkConfig = { - rejectValue: { arg: CreateSessionArg; status: number; error: unknown }; -}; - -/** - * `SessionsService.createSession()` thunk - */ -export const sessionCreated = createAsyncThunk< - CreateSessionResponse, - CreateSessionArg, - CreateSessionThunkConfig ->('api/sessionCreated', async (arg, { rejectWithValue }) => { - const { graph } = arg; - const { POST } = $client.get(); - const { data, error, response } = await POST('/api/v1/sessions/', { - body: graph, - params: { query: { queue_id: $queueId.get() } }, - }); - - if (error) { - return rejectWithValue({ arg, status: response.status, error }); - } - - return data; -}); - -type InvokedSessionArg = { - session_id: paths['/api/v1/sessions/{session_id}/invoke']['put']['parameters']['path']['session_id']; -}; - -type InvokedSessionResponse = - paths['/api/v1/sessions/{session_id}/invoke']['put']['responses']['200']['content']['application/json']; - -type InvokedSessionThunkConfig = { - rejectValue: { - arg: InvokedSessionArg; - error: unknown; - status: number; - }; -}; - -const isErrorWithStatus = (error: unknown): error is { status: number } => - isObject(error) && 'status' in error; - -const isErrorWithDetail = (error: unknown): error is { detail: string } => - isObject(error) && 'detail' in error; - -/** - * `SessionsService.invokeSession()` thunk - */ -export const sessionInvoked = createAsyncThunk< - InvokedSessionResponse, - InvokedSessionArg, - InvokedSessionThunkConfig ->('api/sessionInvoked', async (arg, { rejectWithValue }) => { - const { session_id } = arg; - const { PUT } = $client.get(); - const { error, response } = await PUT( - '/api/v1/sessions/{session_id}/invoke', - { - params: { - query: { queue_id: $queueId.get(), all: true }, - path: { session_id }, - }, - } - ); - - if (error) { - if (isErrorWithStatus(error) && error.status === 403) { - return rejectWithValue({ - arg, - status: response.status, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - error: (error as any).body.detail, - }); - } - if (isErrorWithDetail(error) && response.status === 403) { - return rejectWithValue({ - arg, - status: response.status, - error: error.detail, - }); - } - if (error) { - return rejectWithValue({ arg, status: response.status, error }); - } - } -}); - -type CancelSessionArg = - paths['/api/v1/sessions/{session_id}/invoke']['delete']['parameters']['path']; - -type CancelSessionResponse = - paths['/api/v1/sessions/{session_id}/invoke']['delete']['responses']['200']['content']['application/json']; - -type CancelSessionThunkConfig = { - rejectValue: { - arg: CancelSessionArg; - error: unknown; - }; -}; - -/** - * `SessionsService.cancelSession()` thunk - */ -export const sessionCanceled = createAsyncThunk< - CancelSessionResponse, - CancelSessionArg, - CancelSessionThunkConfig ->('api/sessionCanceled', async (arg, { rejectWithValue }) => { - const { session_id } = arg; - const { DELETE } = $client.get(); - const { data, error } = await DELETE('/api/v1/sessions/{session_id}/invoke', { - params: { - path: { session_id }, - }, - }); - - if (error) { - return rejectWithValue({ arg, error }); - } - - return data; -}); - -type ListSessionsArg = { - params: paths['/api/v1/sessions/']['get']['parameters']; -}; - -type ListSessionsResponse = - paths['/api/v1/sessions/']['get']['responses']['200']['content']['application/json']; - -type ListSessionsThunkConfig = { - rejectValue: { - arg: ListSessionsArg; - error: unknown; - }; -}; - -/** - * `SessionsService.listSessions()` thunk - */ -export const listedSessions = createAsyncThunk< - ListSessionsResponse, - ListSessionsArg, - ListSessionsThunkConfig ->('api/listSessions', async (arg, { rejectWithValue }) => { - const { params } = arg; - const { GET } = $client.get(); - const { data, error } = await GET('/api/v1/sessions/', { - params, - }); - - if (error) { - return rejectWithValue({ arg, error }); - } - - return data; -}); - -export const isAnySessionRejected = isAnyOf( - sessionCreated.rejected, - sessionInvoked.rejected -); diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 669d357527..afa2cd7bd7 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -152,6 +152,8 @@ export type SaveImageInvocation = s['SaveImageInvocation']; export type ControlNetInvocation = s['ControlNetInvocation']; export type IPAdapterInvocation = s['IPAdapterInvocation']; export type CannyImageProcessorInvocation = s['CannyImageProcessorInvocation']; +export type ColorMapImageProcessorInvocation = + s['ColorMapImageProcessorInvocation']; export type ContentShuffleImageProcessorInvocation = s['ContentShuffleImageProcessorInvocation']; export type HedImageProcessorInvocation = s['HedImageProcessorInvocation']; diff --git a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts index 0f4d21d2e1..89487070a6 100644 --- a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts +++ b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts @@ -1,7 +1,7 @@ import { MiddlewareAPI } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import { AppDispatch, RootState } from 'app/store/store'; -import { $queueId } from 'features/queue/store/nanoStores'; +import { $queueId } from 'features/queue/store/queueNanoStore'; import { addToast } from 'features/system/store/systemSlice'; import { makeToast } from 'features/system/util/makeToast'; import { Socket } from 'socket.io-client'; diff --git a/invokeai/frontend/web/src/theme/components/popover.ts b/invokeai/frontend/web/src/theme/components/popover.ts index 55f69e9036..db46f79084 100644 --- a/invokeai/frontend/web/src/theme/components/popover.ts +++ b/invokeai/frontend/web/src/theme/components/popover.ts @@ -31,14 +31,14 @@ const invokeAIContent = defineStyle((props) => { const informationalContent = defineStyle((props) => { return { - [$arrowBg.variable]: mode('colors.base.100', 'colors.base.600')(props), - [$popperBg.variable]: mode('colors.base.100', 'colors.base.600')(props), + [$arrowBg.variable]: mode('colors.base.100', 'colors.base.700')(props), + [$popperBg.variable]: mode('colors.base.100', 'colors.base.700')(props), [$arrowShadowColor.variable]: mode( 'colors.base.400', 'colors.base.400' )(props), - p: 0, - bg: mode('base.100', 'base.600')(props), + p: 4, + bg: mode('base.100', 'base.700')(props), border: 'none', shadow: 'dark-lg', }; @@ -46,6 +46,7 @@ const informationalContent = defineStyle((props) => { const invokeAI = definePartsStyle((props) => ({ content: invokeAIContent(props), + body: { padding: 0 }, })); const informational = definePartsStyle((props) => ({ diff --git a/invokeai/frontend/web/src/theme/theme.ts b/invokeai/frontend/web/src/theme/theme.ts index c2376a5777..ae38aefca0 100644 --- a/invokeai/frontend/web/src/theme/theme.ts +++ b/invokeai/frontend/web/src/theme/theme.ts @@ -1,5 +1,4 @@ -import { ThemeOverride } from '@chakra-ui/react'; - +import { ThemeOverride, ToastProviderProps } from '@chakra-ui/react'; import { InvokeAIColors } from './colors/colors'; import { accordionTheme } from './components/accordion'; import { buttonTheme } from './components/button'; @@ -76,6 +75,7 @@ export const theme: ThemeOverride = { direction: 'ltr', fonts: { body: `'Inter Variable', sans-serif`, + heading: `'Inter Variable', sans-serif`, }, shadows: { light: { @@ -148,3 +148,7 @@ export const theme: ThemeOverride = { Tooltip: tooltipTheme, }, }; + +export const TOAST_OPTIONS: ToastProviderProps = { + defaultOptions: { isClosable: true }, +}; diff --git a/pyproject.toml b/pyproject.toml index e9d24b7fa0..21cda5eebd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,11 +67,12 @@ dependencies = [ 'pyperclip', "pyreadline3", "python-multipart", + "python-socketio", "pytorch-lightning", "realesrgan", "requests~=2.28.2", "rich~=13.3", - "safetensors==0.3.1", + "safetensors~=0.3.1", "scikit-image~=0.21.0", "semver~=3.0.1", "send2trash", diff --git a/tests/test_config.py b/tests/test_config.py index a950a9c06f..2b2492f6a6 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -121,6 +121,12 @@ def test_env_override(patch_rootdir): conf.parse_args(conf=init1, argv=[]) assert conf.max_cache_size == 20 + # make sure that prefix is respected + del os.environ["INVOKEAI_always_use_cpu"] + os.environ["always_use_cpu"] = "True" + conf.parse_args(conf=init1, argv=[]) + assert conf.always_use_cpu is False + def test_root_resists_cwd(patch_rootdir): from invokeai.app.services.config import InvokeAIAppConfig