From b87f3043ae59d7d155223505074e502a3f5acd09 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Wed, 24 May 2023 23:57:15 -0400 Subject: [PATCH 01/47] add logging configuration --- docs/features/LOGGING.md | 171 ++++++++++++++++++++++++++++ docs/features/index.md | 3 + invokeai/app/services/config.py | 10 +- invokeai/backend/util/__init__.py | 2 + invokeai/backend/util/logging.py | 181 ++++++++++++++++++++++++++++-- tests/test_config.py | 4 + 6 files changed, 357 insertions(+), 14 deletions(-) create mode 100644 docs/features/LOGGING.md diff --git a/docs/features/LOGGING.md b/docs/features/LOGGING.md new file mode 100644 index 0000000000..bda968140b --- /dev/null +++ b/docs/features/LOGGING.md @@ -0,0 +1,171 @@ +--- +title: Controlling Logging +--- + +# :material-image-off: Controlling Logging + +## Controlling How InvokeAI Logs Status Messages + +InvokeAI logs status messages using a configurable logging system. You +can log to the terminal window, to a designated file on the local +machine, to the syslog facility on a Linux or Mac, or to a properly +configured web server. You can configure several logs at the same +time, and control the level of message logged and the logging format +(to a limited extent). + +Three command-line options control logging: + +### `--log_handlers ...` + +This option activates one or more log handlers. Options are "console", +"file", "syslog" and "http". To specify more than one, separate them +by spaces: + +```bash +invokeai-web --log_handlers console syslog=/dev/log file=C:\Users\fred\invokeai.log +``` + +The format of these options is described below. + +### `--log_format {plain|color|legacy|syslog}` + +This controls the format of log messages written to the console. Only +the "console" log handler is currently affected by this setting. + +* "plain" provides formatted messages like this: + +```bash + +[2023-05-24 23:18:2[2023-05-24 23:18:50,352]::[InvokeAI]::DEBUG --> this is a debug message +[2023-05-24 23:18:50,352]::[InvokeAI]::INFO --> this is an informational messages +[2023-05-24 23:18:50,352]::[InvokeAI]::WARNING --> this is a warning +[2023-05-24 23:18:50,352]::[InvokeAI]::ERROR --> this is an error +[2023-05-24 23:18:50,352]::[InvokeAI]::CRITICAL --> this is a critical error +``` + +* "color" produces similar output, but the text will be color coded to +indicate the severity of the message. + +* "legacy" produces output similar to InvokeAI versions 2.3 and earlier: + +```bash +### this is a critical error +*** this is an error +** this is a warning +>> this is an informational messages + | this is a debug message +``` + +* "syslog" produces messages suitable for syslog entries: + +```bash +InvokeAI [2691178] this is a critical error +InvokeAI [2691178] this is an error +InvokeAI [2691178] this is a warning +InvokeAI [2691178] this is an informational messages +InvokeAI [2691178] this is a debug message +``` + +(note that the date, time and hostname will be added by the syslog +system) + +### `--log_level {debug|info|warning|error|critical}` + +Providing this command-line option will cause only messages at the +specified level or above to be emitted. + +## Console logging + +When "console" is provided to `--log_handlers`, messages will be +written to the command line window in which InvokeAI was launched. By +default, the color formatter will be used unless overridden by +`--log_format`. + +## File logging + +When "file" is provided to `--log_handlers`, entries will be written +to the file indicated in the path argument. By default, the "plain" +format will be used: + +```bash +invokeai-web --log_handlers file=/var/log/invokeai.log +``` + +## Syslog logging + +When "syslog" is requested, entries will be sent to the syslog +system. There are a variety of ways to control where the log message +is sent: + +* Send to the local machine using the `/dev/log` socket: + +``` +invokeai-web --log_handlers syslog=/dev/log +``` + +* Send to the local machine using a UDP message: + +``` +invokeai-web --log_handlers syslog=localhost +``` + +* Send to the local machine using a UDP message on a nonstandard + port: + +``` +invokeai-web --log_handlers syslog=localhost:512 +``` + +* Send to a remote machine named "loghost" on the local LAN using + facility LOG_USER and UDP packets: + +``` +invokeai-web --log_handlers syslog=loghost,facility=LOG_USER,socktype=SOCK_DGRAM +``` + +This can be abbreviated `syslog=loghost`, as LOG_USER and SOCK_DGRAM +are defaults. + +* Send to a remote machine named "loghost" using the facility LOCAL0 + and using a TCP socket: + +``` +invokeai-web --log_handlers syslog=loghost,facility=LOG_LOCAL0,socktype=SOCK_STREAM +``` + +If no arguments are specified (just a bare "syslog"), then the logging +system will look for a UNIX socket named `/dev/log`, and if not found +try to send a UDP message to `localhost`. The Macintosh OS used to +support logging to a socket named `/var/run/syslog`, but this feature +has since been disabled. + +## Web logging + +If you have access to a web server that is configured to log messages +when a particular URL is requested, you can log using the "http" +method: + +``` +invokeai-web --log_handlers http=http://my.server/path/to/logger,method=POST +``` + +The optional [,method=] part can be used to specify whether the URL +accepts GET (default) or POST messages. + +Currently password authentication and SSL are not supported. + +## Using the configuration file + +You can set and forget logging options by adding a "Logging" section +to `invokeai.yaml`: + +``` +InvokeAI: + [... other settings...] + Logging: + log_handlers: + - console + - syslog=/dev/log + log_level: info + log_format: color +``` diff --git a/docs/features/index.md b/docs/features/index.md index d9b0e1fd7c..53d380f3fb 100644 --- a/docs/features/index.md +++ b/docs/features/index.md @@ -57,6 +57,9 @@ Personalize models by adding your own style or subjects. ## * [The NSFW Checker](NSFW.md) Prevent InvokeAI from displaying unwanted racy images. +## * [Controlling Logging](LOGGING.md) +Control how InvokeAI logs status messages. + ## * [Miscellaneous](OTHER.md) Run InvokeAI on Google Colab, generate images with repeating patterns, batch process a file of prompts, increase the "creativity" of image diff --git a/invokeai/app/services/config.py b/invokeai/app/services/config.py index 2d87125744..49f3ed1aa7 100644 --- a/invokeai/app/services/config.py +++ b/invokeai/app/services/config.py @@ -143,14 +143,13 @@ two configs are kept in separate sections of the config file: ''' import argparse import pydoc -import typing import os import sys from argparse import ArgumentParser from omegaconf import OmegaConf, DictConfig from pathlib import Path from pydantic import BaseSettings, Field, parse_obj_as -from typing import Any, ClassVar, Dict, List, Literal, Type, Union, get_origin, get_type_hints, get_args +from typing import ClassVar, Dict, List, Literal, Type, Union, get_origin, get_type_hints, get_args INIT_FILE = Path('invokeai.yaml') LEGACY_INIT_FILE = Path('invokeai.init') @@ -168,7 +167,7 @@ class InvokeAISettings(BaseSettings): def parse_args(self, argv: list=sys.argv[1:]): parser = self.get_parser() - opt, _ = parser.parse_known_args(argv) + opt = parser.parse_args(argv) for name in self.__fields__: if name not in self._excluded(): setattr(self, name, getattr(opt,name)) @@ -365,6 +364,11 @@ setting environment variables INVOKEAI_. model : str = Field(default='stable-diffusion-1.5', description='Initial model name', category='Models') embeddings : bool = Field(default=True, description='Load contents of embeddings directory', category='Models') + + log_handlers : List[str] = Field(default=["console"], description='Log handler. Valid options are "console", "file=", "syslog=path|address:host:port", "http="', category="Logging") + # note - would be better to read the log_format values from logging.py, but this creates circular dependencies issues + log_format : Literal[tuple(['plain','color','syslog','legacy'])] = Field(default="color", description='Log format. Use "plain" for text-only, "color" for colorized output, "legacy" for 2.3-style logging and "syslog" for syslog-style', category="Logging") + log_level : Literal[tuple(["debug","info","warning","error","critical"])] = Field(default="debug", description="Emit logging messages at this level or higher", category="Logging") #fmt: on def __init__(self, conf: DictConfig = None, argv: List[str]=None, **kwargs): diff --git a/invokeai/backend/util/__init__.py b/invokeai/backend/util/__init__.py index ca42f86fd6..84720b1854 100644 --- a/invokeai/backend/util/__init__.py +++ b/invokeai/backend/util/__init__.py @@ -17,3 +17,5 @@ from .util import ( instantiate_from_config, url_attachment_name, ) + + diff --git a/invokeai/backend/util/logging.py b/invokeai/backend/util/logging.py index 9d1262d5c6..445fef24df 100644 --- a/invokeai/backend/util/logging.py +++ b/invokeai/backend/util/logging.py @@ -31,7 +31,16 @@ IAILogger.debug('this is a debugging message') """ import logging +import logging.handlers +import socket +import syslog +import sys +import urllib.parse +from abc import abstractmethod +from pathlib import Path + +from invokeai.app.services.config import InvokeAIAppConfig, get_invokeai_config # module level functions def debug(msg, *args, **kwargs): @@ -62,11 +71,77 @@ def getLogger(name: str = None) -> logging.Logger: return InvokeAILogger.getLogger(name) -class InvokeAILogFormatter(logging.Formatter): +_FACILITY_MAP = dict( + LOG_KERN = syslog.LOG_KERN, + LOG_USER = syslog.LOG_USER, + LOG_MAIL = syslog.LOG_MAIL, + LOG_DAEMON = syslog.LOG_DAEMON, + LOG_AUTH = syslog.LOG_AUTH, + LOG_LPR = syslog.LOG_LPR, + LOG_NEWS = syslog.LOG_NEWS, + LOG_UUCP = syslog.LOG_UUCP, + LOG_CRON = syslog.LOG_CRON, + LOG_SYSLOG = syslog.LOG_SYSLOG, + LOG_LOCAL0 = syslog.LOG_LOCAL0, + LOG_LOCAL1 = syslog.LOG_LOCAL1, + LOG_LOCAL2 = syslog.LOG_LOCAL2, + LOG_LOCAL3 = syslog.LOG_LOCAL3, + LOG_LOCAL4 = syslog.LOG_LOCAL4, + LOG_LOCAL5 = syslog.LOG_LOCAL5, + LOG_LOCAL6 = syslog.LOG_LOCAL6, + LOG_LOCAL7 = syslog.LOG_LOCAL7, +) + +_SOCK_MAP = dict( + SOCK_STREAM = socket.SOCK_STREAM, + SOCK_DGRAM = socket.SOCK_DGRAM, +) + +class InvokeAIFormatter(logging.Formatter): + ''' + Base class for logging formatter + + ''' + def format(self, record): + formatter = logging.Formatter(self.log_fmt(record.levelno)) + return formatter.format(record) + + @abstractmethod + def log_fmt(self, levelno: int)->str: + pass + +class InvokeAISyslogFormatter(InvokeAIFormatter): + ''' + Formatting for syslog + ''' + def log_fmt(self, levelno: int)->str: + return '%(name)s [%(process)d] <%(levelname)s> %(message)s' + +class InvokeAILegacyLogFormatter(InvokeAIFormatter): + ''' + Formatting for the InvokeAI Logger (legacy version) + ''' + FORMATS = { + logging.DEBUG: " | %(message)s", + logging.INFO: ">> %(message)s", + logging.WARNING: "** %(message)s", + logging.ERROR: "*** %(message)s", + logging.CRITICAL: "### %(message)s", + } + def log_fmt(self,levelno:int)->str: + return self.FORMATS.get(levelno) + +class InvokeAIPlainLogFormatter(InvokeAIFormatter): + ''' + Custom Formatting for the InvokeAI Logger (plain version) + ''' + def log_fmt(self, levelno: int)->str: + return "[%(asctime)s]::[%(name)s]::%(levelname)s --> %(message)s" + +class InvokeAIColorLogFormatter(InvokeAIFormatter): ''' Custom Formatting for the InvokeAI Logger ''' - # Color Codes grey = "\x1b[38;20m" yellow = "\x1b[33;20m" @@ -88,23 +163,107 @@ class InvokeAILogFormatter(logging.Formatter): logging.CRITICAL: bold_red + log_format + reset } - def format(self, record): - log_fmt = self.FORMATS.get(record.levelno) - formatter = logging.Formatter(log_fmt, datefmt="%d-%m-%Y %H:%M:%S") - return formatter.format(record) + def log_fmt(self, levelno: int)->str: + return self.FORMATS.get(levelno) +LOG_FORMATTERS = { + 'plain': InvokeAIPlainLogFormatter, + 'color': InvokeAIColorLogFormatter, + 'syslog': InvokeAISyslogFormatter, + 'legacy': InvokeAILegacyLogFormatter, +} class InvokeAILogger(object): loggers = dict() @classmethod def getLogger(cls, name: str = 'InvokeAI') -> logging.Logger: + config = get_invokeai_config() + if name not in cls.loggers: logger = logging.getLogger(name) - logger.setLevel(logging.DEBUG) - ch = logging.StreamHandler() - fmt = InvokeAILogFormatter() - ch.setFormatter(fmt) - logger.addHandler(ch) + logger.setLevel(config.log_level.upper()) # yes, strings work here + for ch in cls.getLoggers(config): + logger.addHandler(ch) cls.loggers[name] = logger return cls.loggers[name] + + @classmethod + def getLoggers(cls, config: InvokeAIAppConfig) -> list[logging.Handler]: + handler_strs = config.log_handlers + print(f'handler_strs={handler_strs}') + handlers = list() + for handler in handler_strs: + handler_name,*args = handler.split('=',2) + args = args[0] if len(args) > 0 else None + + # console is the only handler that gets a custom formatter + if handler_name=='console': + formatter = LOG_FORMATTERS[config.log_format] + ch = logging.StreamHandler() + ch.setFormatter(formatter()) + handlers.append(ch) + + elif handler_name=='syslog': + ch = cls._parse_syslog_args(args) + ch.setFormatter(InvokeAISyslogFormatter()) + handlers.append(ch) + + elif handler_name=='file': + handlers.append(cls._parse_file_args(args)) + + elif handler_name=='http': + handlers.append(cls._parse_http_args(args)) + return handlers + + @staticmethod + def _parse_syslog_args( + args: str=None + )-> logging.Handler: + if not args: + args='/dev/log' if Path('/dev/log').exists() else 'address:localhost:514' + syslog_args = dict() + try: + for a in args.split(','): + arg_name,*arg_value = a.split(':',2) + if arg_name=='address': + host,*port = arg_value + port = 514 if len(port)==0 else int(port[0]) + syslog_args['address'] = (host,port) + elif arg_name=='facility': + syslog_args['facility'] = _FACILITY_MAP[arg_value[0]] + elif arg_name=='socktype': + syslog_args['socktype'] = _SOCK_MAP[arg_value[0]] + else: + syslog_args['address'] = arg_name + except: + raise ValueError(f"{args} is not a value argument list for syslog logging") + return logging.handlers.SysLogHandler(**syslog_args) + + @staticmethod + def _parse_file_args(args: str=None)-> logging.Handler: + if not args: + raise ValueError("please provide filename for file logging using format 'file=/path/to/logfile.txt'") + return logging.FileHandler(args) + + @staticmethod + def _parse_http_args(args: str=None)-> logging.Handler: + if not args: + raise ValueError("please provide destination for http logging using format 'http=url'") + arg_list = args.split(',') + url = urllib.parse.urlparse(arg_list.pop(0)) + if url.scheme != 'http': + raise ValueError(f"the http logging module can only log to HTTP URLs, but {url.scheme} was specified") + host = url.hostname + path = url.path + port = url.port or 80 + + syslog_args = dict() + for a in arg_list: + arg_name, *arg_value = a.split(':',2) + if arg_name=='method': + arg_value = arg_value[0] if len(arg_value)>0 else 'GET' + syslog_args[arg_name] = arg_value + else: # TODO: Provide support for SSL context and credentials + pass + return logging.handlers.HTTPHandler(f'{host}:{port}',path,**syslog_args) diff --git a/tests/test_config.py b/tests/test_config.py index 6d0586213e..0bfb5d1980 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,13 +1,17 @@ import os import pytest +import sys from omegaconf import OmegaConf from pathlib import Path os.environ['INVOKEAI_ROOT']='/tmp' +sys.argv = [] # to prevent config from trying to parse pytest arguments + from invokeai.app.services.config import InvokeAIAppConfig, InvokeAISettings from invokeai.app.invocations.generate import TextToImageInvocation + init1 = OmegaConf.create( ''' InvokeAI: From 88776fb2de5b4e4620d0b66341743ea08be28b83 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Thu, 25 May 2023 09:39:45 -0400 Subject: [PATCH 02/47] get invokeai_configure working again --- invokeai/backend/config/invokeai_configure.py | 17 +++++++++-------- .../backend/config/model_install_backend.py | 2 +- invokeai/frontend/install/model_install.py | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/invokeai/backend/config/invokeai_configure.py b/invokeai/backend/config/invokeai_configure.py index 59f11d35bc..1ebb2471aa 100755 --- a/invokeai/backend/config/invokeai_configure.py +++ b/invokeai/backend/config/invokeai_configure.py @@ -35,15 +35,19 @@ from transformers import ( CLIPTextModel, CLIPTokenizer, ) - import invokeai.configs as configs +from invokeai.app.services.config import ( + get_invokeai_config, + InvokeAIAppConfig, +) from invokeai.frontend.install.model_install import addModelsForm, process_and_execute from invokeai.frontend.install.widgets import ( CenteredButtonPress, IntTitleSlider, set_min_terminal_size, ) + from invokeai.backend.config.legacy_arg_parsing import legacy_parser from invokeai.backend.config.model_install_backend import ( default_dataset, @@ -51,10 +55,6 @@ from invokeai.backend.config.model_install_backend import ( hf_download_with_resume, recommended_datasets, ) -from invokeai.app.services.config import ( - get_invokeai_config, - InvokeAIAppConfig, -) warnings.filterwarnings("ignore") @@ -62,7 +62,8 @@ transformers.logging.set_verbosity_error() # --------------------------globals----------------------- -config = get_invokeai_config() + +config = get_invokeai_config(argv=[]) Model_dir = "models" Weights_dir = "ldm/stable-diffusion-v1/" @@ -699,7 +700,7 @@ def write_opts(opts: Namespace, init_file: Path): """ # this will load current settings - config = InvokeAIAppConfig() + config = InvokeAIAppConfig(argv=[]) for key,value in opts.__dict__.items(): if hasattr(config,key): setattr(config,key,value) @@ -820,7 +821,7 @@ def main(): if old_init_file.exists() and not new_init_file.exists(): print('** Migrating invokeai.init to invokeai.yaml') migrate_init_file(old_init_file) - config = get_invokeai_config() # reread defaults + config = get_invokeai_config(argv=[]) # reread defaults if not config.model_conf_path.exists(): diff --git a/invokeai/backend/config/model_install_backend.py b/invokeai/backend/config/model_install_backend.py index cb76f955bc..538ab61321 100644 --- a/invokeai/backend/config/model_install_backend.py +++ b/invokeai/backend/config/model_install_backend.py @@ -27,7 +27,7 @@ from ..stable_diffusion import StableDiffusionGeneratorPipeline warnings.filterwarnings("ignore") # --------------------------globals----------------------- -config = get_invokeai_config() +config = get_invokeai_config(argv=[]) Model_dir = "models" Weights_dir = "ldm/stable-diffusion-v1/" diff --git a/invokeai/frontend/install/model_install.py b/invokeai/frontend/install/model_install.py index a283b4952d..45ce11c686 100644 --- a/invokeai/frontend/install/model_install.py +++ b/invokeai/frontend/install/model_install.py @@ -46,7 +46,7 @@ from invokeai.app.services.config import get_invokeai_config MIN_COLS = 120 MIN_LINES = 45 -config = get_invokeai_config() +config = get_invokeai_config(argv=[]) class addModelsForm(npyscreen.FormMultiPage): # for responsive resizing - disabled From ca7b267326ec346f5d8bebe7b158ed351c1e5250 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Thu, 25 May 2023 10:10:46 -0400 Subject: [PATCH 03/47] raise error if syslogging requested and syslog lib not available --- invokeai/backend/util/logging.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/invokeai/backend/util/logging.py b/invokeai/backend/util/logging.py index 445fef24df..16efd56c03 100644 --- a/invokeai/backend/util/logging.py +++ b/invokeai/backend/util/logging.py @@ -33,8 +33,6 @@ IAILogger.debug('this is a debugging message') import logging import logging.handlers import socket -import syslog -import sys import urllib.parse from abc import abstractmethod @@ -42,6 +40,12 @@ from pathlib import Path from invokeai.app.services.config import InvokeAIAppConfig, get_invokeai_config +try: + import syslog + SYSLOG_AVAILABLE = True +except: + SYSLOG_AVAILABLE = False + # module level functions def debug(msg, *args, **kwargs): InvokeAILogger.getLogger().debug(msg, *args, **kwargs) @@ -90,7 +94,7 @@ _FACILITY_MAP = dict( LOG_LOCAL5 = syslog.LOG_LOCAL5, LOG_LOCAL6 = syslog.LOG_LOCAL6, LOG_LOCAL7 = syslog.LOG_LOCAL7, -) +) if SYSLOG_AVAILABLE else dict() _SOCK_MAP = dict( SOCK_STREAM = socket.SOCK_STREAM, @@ -220,6 +224,8 @@ class InvokeAILogger(object): def _parse_syslog_args( args: str=None )-> logging.Handler: + if not SYSLOG_AVAILABLE: + raise ValueError("syslog is not available on this system") if not args: args='/dev/log' if Path('/dev/log').exists() else 'address:localhost:514' syslog_args = dict() From 383e3d77cbc8be8aa88d75d972e9581f0c482f50 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 4 Jun 2023 22:00:57 +1000 Subject: [PATCH 04/47] feat(nodes): add separate scripts to launch cli and web --- scripts/invoke.py | 5 ----- scripts/{invoke-new.py => invokeai-cli.py} | 10 +++------- scripts/invokeai-web.py | 20 ++++++++++++++++++++ 3 files changed, 23 insertions(+), 12 deletions(-) delete mode 100755 scripts/invoke.py rename scripts/{invoke-new.py => invokeai-cli.py} (64%) create mode 100755 scripts/invokeai-web.py diff --git a/scripts/invoke.py b/scripts/invoke.py deleted file mode 100755 index 9cd4b5a0a6..0000000000 --- a/scripts/invoke.py +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env python - -from invokeai.frontend.CLI import invokeai_command_line_interface as main -main() - diff --git a/scripts/invoke-new.py b/scripts/invokeai-cli.py similarity index 64% rename from scripts/invoke-new.py rename to scripts/invokeai-cli.py index faf83a9993..aefe08e956 100755 --- a/scripts/invoke-new.py +++ b/scripts/invokeai-cli.py @@ -12,13 +12,9 @@ def main(): # Change working directory to the repo root os.chdir(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) - if '--web' in sys.argv: - from invokeai.app.api_app import invoke_api - invoke_api() - else: - # TODO: Parse some top-level args here. - from invokeai.app.cli_app import invoke_cli - invoke_cli() + # TODO: Parse some top-level args here. + from invokeai.app.cli_app import invoke_cli + invoke_cli() if __name__ == '__main__': diff --git a/scripts/invokeai-web.py b/scripts/invokeai-web.py new file mode 100755 index 0000000000..9ac7ee5cb9 --- /dev/null +++ b/scripts/invokeai-web.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) + +import logging +logging.getLogger("xformers").addFilter(lambda record: 'A matching Triton is not available' not in record.getMessage()) + +import os +import sys + +def main(): + # Change working directory to the repo root + os.chdir(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + + from invokeai.app.api_app import invoke_api + invoke_api() + + +if __name__ == '__main__': + main() From d6a959b0008f2b7b6b1168f64c129b9cd575f681 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Jun 2023 12:54:57 +1000 Subject: [PATCH 05/47] feat(nodes): tidy controlnet processor nodes & improve descriptions --- .../controlnet_image_processors.py | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index 7d5160a491..be0381c58e 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -94,13 +94,13 @@ CONTROLNET_DEFAULT_MODELS = [ CONTROLNET_NAME_VALUES = Literal[tuple(CONTROLNET_DEFAULT_MODELS)] class ControlField(BaseModel): - image: ImageField = Field(default=None, description="processed image") - control_model: Optional[str] = Field(default=None, description="control model used") - control_weight: Optional[float] = Field(default=1, description="weight given to controlnet") + image: ImageField = Field(default=None, description="The control image") + control_model: Optional[str] = Field(default=None, description="The ControlNet model to use") + control_weight: Optional[float] = Field(default=1, description="The weight given to the ControlNet") begin_step_percent: float = Field(default=0, ge=0, le=1, - description="% of total steps at which controlnet is first applied") + description="When the ControlNet is first applied (% of total steps)") end_step_percent: float = Field(default=1, ge=0, le=1, - description="% of total steps at which controlnet is last applied") + description="When the ControlNet is last applied (% of total steps)") class Config: schema_extra = { @@ -112,7 +112,7 @@ class ControlOutput(BaseInvocationOutput): """node output for ControlNet info""" # fmt: off type: Literal["control_output"] = "control_output" - control: ControlField = Field(default=None, description="The control info dict") + control: ControlField = Field(default=None, description="The output control image") # fmt: on @@ -121,15 +121,15 @@ class ControlNetInvocation(BaseInvocation): # fmt: off type: Literal["controlnet"] = "controlnet" # Inputs - image: ImageField = Field(default=None, description="image to process") + image: ImageField = Field(default=None, description="The control image") control_model: CONTROLNET_NAME_VALUES = Field(default="lllyasviel/sd-controlnet-canny", - description="control model used") - control_weight: float = Field(default=1.0, ge=0, le=1, description="weight given to controlnet") + description="The ControlNet model to use") + control_weight: float = Field(default=1.0, ge=0, le=1, description="The weight given to the ControlNet") # TODO: add support in backend core for begin_step_percent, end_step_percent, guess_mode begin_step_percent: float = Field(default=0, ge=0, le=1, - description="% of total steps at which controlnet is first applied") + description="When the ControlNet is first applied (% of total steps)") end_step_percent: float = Field(default=1, ge=0, le=1, - description="% of total steps at which controlnet is last applied") + description="When the ControlNet is last applied (% of total steps)") # fmt: on @@ -152,7 +152,7 @@ class ImageProcessorInvocation(BaseInvocation, PILInvocationConfig): # fmt: off type: Literal["image_processor"] = "image_processor" # Inputs - image: ImageField = Field(default=None, description="image to process") + image: ImageField = Field(default=None, description="The image to process") # fmt: on @@ -204,8 +204,8 @@ class CannyImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfi # fmt: off type: Literal["canny_image_processor"] = "canny_image_processor" # Input - low_threshold: float = Field(default=100, ge=0, description="low threshold of Canny pixel gradient") - high_threshold: float = Field(default=200, ge=0, description="high threshold of Canny pixel gradient") + low_threshold: int = Field(default=100, ge=0, le=255, description="The low threshold of the Canny pixel gradient (0-255)") + high_threshold: int = Field(default=200, ge=0, le=255, description="The high threshold of the Canny pixel gradient (0-255)") # fmt: on def run_processor(self, image): @@ -219,11 +219,11 @@ class HedImageprocessorInvocation(ImageProcessorInvocation, PILInvocationConfig) # fmt: off type: Literal["hed_image_processor"] = "hed_image_processor" # Inputs - detect_resolution: int = Field(default=512, ge=0, description="pixel resolution for edge detection") - image_resolution: int = Field(default=512, ge=0, description="pixel resolution for output image") + detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for edge detection") + image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") # safe not supported in controlnet_aux v0.0.3 # safe: bool = Field(default=False, description="whether to use safe mode") - scribble: bool = Field(default=False, description="whether to use scribble mode") + scribble: bool = Field(default=False, description="Whether to use scribble mode") # fmt: on def run_processor(self, image): @@ -243,9 +243,9 @@ class LineartImageProcessorInvocation(ImageProcessorInvocation, PILInvocationCon # fmt: off type: Literal["lineart_image_processor"] = "lineart_image_processor" # Inputs - detect_resolution: int = Field(default=512, ge=0, description="pixel resolution for edge detection") - image_resolution: int = Field(default=512, ge=0, description="pixel resolution for output image") - coarse: bool = Field(default=False, description="whether to use coarse mode") + detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for edge detection") + image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") + coarse: bool = Field(default=False, description="Whether to use coarse mode") # fmt: on def run_processor(self, image): @@ -262,8 +262,8 @@ class LineartAnimeImageProcessorInvocation(ImageProcessorInvocation, PILInvocati # fmt: off type: Literal["lineart_anime_image_processor"] = "lineart_anime_image_processor" # Inputs - detect_resolution: int = Field(default=512, ge=0, description="pixel resolution for edge detection") - image_resolution: int = Field(default=512, ge=0, description="pixel resolution for output image") + detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for edge detection") + image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") # fmt: on def run_processor(self, image): @@ -280,9 +280,9 @@ class OpenposeImageProcessorInvocation(ImageProcessorInvocation, PILInvocationCo # fmt: off type: Literal["openpose_image_processor"] = "openpose_image_processor" # Inputs - hand_and_face: bool = Field(default=False, description="whether to use hands and face mode") - detect_resolution: int = Field(default=512, ge=0, description="pixel resolution for edge detection") - image_resolution: int = Field(default=512, ge=0, description="pixel resolution for output image") + hand_and_face: bool = Field(default=False, description="Whether to use hands and face mode") + detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for edge detection") + image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") # fmt: on def run_processor(self, image): @@ -300,8 +300,8 @@ class MidasDepthImageProcessorInvocation(ImageProcessorInvocation, PILInvocation # fmt: off type: Literal["midas_depth_image_processor"] = "midas_depth_image_processor" # Inputs - a_mult: float = Field(default=2.0, ge=0, description="Midas parameter a = amult * PI") - bg_th: float = Field(default=0.1, ge=0, description="Midas parameter bg_th") + a_mult: float = Field(default=2.0, ge=0, description="Midas parameter `a_mult` (a = a_mult * PI)") + bg_th: float = Field(default=0.1, ge=0, description="Midas parameter `bg_th`") # depth_and_normal not supported in controlnet_aux v0.0.3 # depth_and_normal: bool = Field(default=False, description="whether to use depth and normal mode") # fmt: on @@ -322,8 +322,8 @@ class NormalbaeImageProcessorInvocation(ImageProcessorInvocation, PILInvocationC # fmt: off type: Literal["normalbae_image_processor"] = "normalbae_image_processor" # Inputs - detect_resolution: int = Field(default=512, ge=0, description="pixel resolution for edge detection") - image_resolution: int = Field(default=512, ge=0, description="pixel resolution for output image") + detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for edge detection") + image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") # fmt: on def run_processor(self, image): @@ -339,10 +339,10 @@ class MlsdImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig # fmt: off type: Literal["mlsd_image_processor"] = "mlsd_image_processor" # Inputs - detect_resolution: int = Field(default=512, ge=0, description="pixel resolution for edge detection") - image_resolution: int = Field(default=512, ge=0, description="pixel resolution for output image") - thr_v: float = Field(default=0.1, ge=0, description="MLSD parameter thr_v") - thr_d: float = Field(default=0.1, ge=0, description="MLSD parameter thr_d") + detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for edge detection") + image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") + thr_v: float = Field(default=0.1, ge=0, description="MLSD parameter `thr_v`") + thr_d: float = Field(default=0.1, ge=0, description="MLSD parameter `thr_d`") # fmt: on def run_processor(self, image): @@ -360,10 +360,10 @@ class PidiImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig # fmt: off type: Literal["pidi_image_processor"] = "pidi_image_processor" # Inputs - detect_resolution: int = Field(default=512, ge=0, description="pixel resolution for edge detection") - image_resolution: int = Field(default=512, ge=0, description="pixel resolution for output image") - safe: bool = Field(default=False, description="whether to use safe mode") - scribble: bool = Field(default=False, description="whether to use scribble mode") + detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for edge detection") + image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") + safe: bool = Field(default=False, description="Whether to use safe mode") + scribble: bool = Field(default=False, description="Whether to use scribble mode") # fmt: on def run_processor(self, image): @@ -381,11 +381,11 @@ class ContentShuffleImageProcessorInvocation(ImageProcessorInvocation, PILInvoca # fmt: off type: Literal["content_shuffle_image_processor"] = "content_shuffle_image_processor" # Inputs - detect_resolution: int = Field(default=512, ge=0, description="pixel resolution for edge detection") - image_resolution: int = Field(default=512, ge=0, description="pixel resolution for output image") - h: Union[int | None] = Field(default=512, ge=0, description="content shuffle h parameter") - w: Union[int | None] = Field(default=512, ge=0, description="content shuffle w parameter") - f: Union[int | None] = Field(default=256, ge=0, description="cont") + detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for edge detection") + image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") + h: Union[int, None] = Field(default=512, ge=0, description="Content shuffle `h` parameter") + w: Union[int, None] = Field(default=512, ge=0, description="Content shuffle `w` parameter") + f: Union[int, None] = Field(default=256, ge=0, description="Content shuffle `f` parameter") # fmt: on def run_processor(self, image): @@ -418,8 +418,8 @@ class MediapipeFaceProcessorInvocation(ImageProcessorInvocation, PILInvocationCo # fmt: off type: Literal["mediapipe_face_processor"] = "mediapipe_face_processor" # Inputs - max_faces: int = Field(default=1, ge=1, description="maximum number of faces to detect") - min_confidence: float = Field(default=0.5, ge=0, le=1, description="minimum confidence for face detection") + max_faces: int = Field(default=1, ge=1, description="Maximum number of faces to detect") + min_confidence: float = Field(default=0.5, ge=0, le=1, description="Minimum confidence for face detection") # fmt: on def run_processor(self, image): From e2e07696fcb12aab70f903a7e8aa8af4b5591e4a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Jun 2023 14:17:32 +1000 Subject: [PATCH 06/47] feat(ui): wip controlnet ui --- .../middleware/listenerMiddleware/index.ts | 4 + .../listeners/controlNetImageProcessed.ts | 60 +++++++ .../web/src/common/components/IAICollapse.tsx | 2 +- .../components/ImageMetadataOverlay.tsx | 18 +- .../controlNet/components/ControlNet.tsx | 27 +++ .../components/processors/CannyProcessor.tsx | 64 +++++++ .../components/processors/HedProcessor.tsx | 42 +++++ .../processors/LineartAnimeProcessor.tsx | 31 ++++ .../processors/LineartProcessor.tsx | 42 +++++ .../common/ControlNetProcessButton.tsx | 13 ++ .../common/ControlNetProcessorImage.tsx | 33 ++++ .../src/features/controlNet/store/actions.ts | 7 + .../controlNet/store/controlNetSlice.ts | 159 ++++++++++++++++++ .../src/features/controlNet/store/types.ts | 28 +++ .../ControlNet/ParamControlNetCollapse.tsx | 62 +++++++ .../TextToImage/TextToImageTabParameters.tsx | 2 + 16 files changed, 579 insertions(+), 15 deletions(-) create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts create mode 100644 invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorImage.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/store/actions.ts create mode 100644 invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts create mode 100644 invokeai/frontend/web/src/features/controlNet/store/types.ts create mode 100644 invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx 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 ba16e56371..7089707217 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -70,6 +70,7 @@ import { import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSaved'; import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingAreaImageListener'; import { addImageCategoriesChangedListener } from './listeners/imageCategoriesChanged'; +import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed'; export const listenerMiddleware = createListenerMiddleware(); @@ -173,3 +174,6 @@ addReceivedPageOfImagesRejectedListener(); // Gallery addImageCategoriesChangedListener(); + +// ControlNet +addControlNetImageProcessedListener(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts new file mode 100644 index 0000000000..6b04485581 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts @@ -0,0 +1,60 @@ +import { startAppListening } from '..'; +import { imageMetadataReceived, imageUploaded } from 'services/thunks/image'; +import { addToast } from 'features/system/store/systemSlice'; +import { log } from 'app/logging/useLogger'; +import { controlNetImageProcessed } from 'features/controlNet/store/actions'; +import { Graph } from 'services/api'; +import { sessionCreated } from 'services/thunks/session'; +import { sessionReadyToInvoke } from 'features/system/store/actions'; +import { appSocketInvocationComplete } from 'services/events/actions'; +import { isImageOutput } from 'services/types/guards'; +import { controlNetProcessedImageChanged } from 'features/controlNet/store/controlNetSlice'; +import { selectImagesById } from 'features/gallery/store/imagesSlice'; + +const moduleLog = log.child({ namespace: 'controlNet' }); + +export const addControlNetImageProcessedListener = () => { + startAppListening({ + actionCreator: controlNetImageProcessed, + effect: async (action, { dispatch, getState, take }) => { + const { controlNetId, processorNode } = action.payload; + const { id } = processorNode; + const graph: Graph = { + nodes: { [id]: processorNode }, + }; + const sessionCreatedAction = dispatch(sessionCreated({ graph })); + const [sessionCreatedFulfilledAction] = await take( + (action): action is ReturnType => + sessionCreated.fulfilled.match(action) && + action.meta.requestId === sessionCreatedAction.requestId + ); + const sessionId = sessionCreatedFulfilledAction.payload.id; + dispatch(sessionReadyToInvoke()); + const [processorAction] = await take( + (action): action is ReturnType => + appSocketInvocationComplete.match(action) && + action.payload.data.graph_execution_state_id === sessionId + ); + + if (isImageOutput(processorAction.payload.data.result)) { + const { image_name } = processorAction.payload.data.result.image; + + const [imageMetadataReceivedAction] = await take( + ( + action + ): action is ReturnType => + imageMetadataReceived.fulfilled.match(action) && + action.payload.image_name === image_name + ); + + const processedControlImage = imageMetadataReceivedAction.payload; + dispatch( + controlNetProcessedImageChanged({ + controlNetId, + processedControlImage, + }) + ); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/common/components/IAICollapse.tsx b/invokeai/frontend/web/src/common/components/IAICollapse.tsx index 161caca24d..ec23793741 100644 --- a/invokeai/frontend/web/src/common/components/IAICollapse.tsx +++ b/invokeai/frontend/web/src/common/components/IAICollapse.tsx @@ -49,7 +49,7 @@ const IAICollapse = (props: IAIToggleCollapseProps) => { /> )} - + {children} diff --git a/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx b/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx index bed0a26831..95c888d658 100644 --- a/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx +++ b/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx @@ -1,5 +1,5 @@ import { Badge, Flex } from '@chakra-ui/react'; -import { isNumber, isString } from 'lodash-es'; +import { isString } from 'lodash-es'; import { useMemo } from 'react'; import { ImageDTO } from 'services/api'; @@ -8,14 +8,6 @@ type ImageMetadataOverlayProps = { }; const ImageMetadataOverlay = ({ image }: ImageMetadataOverlayProps) => { - const dimensions = useMemo(() => { - if (!isNumber(image.metadata?.width) || isNumber(!image.metadata?.height)) { - return; - } - - return `${image.metadata?.width} × ${image.metadata?.height}`; - }, [image.metadata]); - const model = useMemo(() => { if (!isString(image.metadata?.model)) { return; @@ -37,11 +29,9 @@ const ImageMetadataOverlay = ({ image }: ImageMetadataOverlayProps) => { gap: 2, }} > - {dimensions && ( - - {dimensions} - - )} + + {image.width} × {image.height} + {model && ( {model} diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx new file mode 100644 index 0000000000..51fa33353b --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx @@ -0,0 +1,27 @@ +import { memo } from 'react'; +import { ControlNetProcessorNode } from '../store/types'; +import { ImageDTO } from 'services/api'; +import CannyProcessor from './processors/CannyProcessor'; + +export type ControlNetProcessorProps = { + controlNetId: string; + image: ImageDTO; + type: ControlNetProcessorNode['type']; +}; + +const renderProcessorComponent = (props: ControlNetProcessorProps) => { + const { type } = props; + if (type === 'canny_image_processor') { + return ; + } +}; + +const ControlNet = () => { + return ( +
+

ControlNet

+
+ ); +}; + +export default memo(ControlNet); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx new file mode 100644 index 0000000000..012fb8532b --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx @@ -0,0 +1,64 @@ +import { Flex } from '@chakra-ui/react'; +import IAISlider from 'common/components/IAISlider'; +import { memo, useCallback, useState } from 'react'; +import ControlNetProcessButton from './common/ControlNetProcessButton'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { controlNetImageProcessed } from 'features/controlNet/store/actions'; +import { ImageDTO } from 'services/api'; +import ControlNetProcessorImage from './common/ControlNetProcessorImage'; +import { ControlNetProcessorProps } from '../ControlNet'; + +export const CANNY_PROCESSOR = 'canny_processor'; + +const CannyProcessor = (props: ControlNetProcessorProps) => { + const { controlNetId, image, type } = props; + const dispatch = useAppDispatch(); + const [lowThreshold, setLowThreshold] = useState(100); + const [highThreshold, setHighThreshold] = useState(200); + + const handleProcess = useCallback(() => { + if (!image) { + return; + } + + dispatch( + controlNetImageProcessed({ + controlNetId, + processorNode: { + id: CANNY_PROCESSOR, + type: 'canny_image_processor', + image: { + image_name: image.image_name, + image_origin: image.image_origin, + }, + low_threshold: lowThreshold, + high_threshold: highThreshold, + }, + }) + ); + }, [controlNetId, dispatch, highThreshold, image, lowThreshold]); + + return ( + + + + + + ); +}; + +export default memo(CannyProcessor); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx new file mode 100644 index 0000000000..891f6d0adc --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx @@ -0,0 +1,42 @@ +import { Flex } from '@chakra-ui/react'; +import IAISlider from 'common/components/IAISlider'; +import IAISwitch from 'common/components/IAISwitch'; +import { ChangeEvent, memo, useState } from 'react'; + +const HedPreprocessor = () => { + const [detectResolution, setDetectResolution] = useState(512); + const [imageResolution, setImageResolution] = useState(512); + const [isScribbleEnabled, setIsScribbleEnabled] = useState(false); + + const handleChangeScribble = (e: ChangeEvent) => { + setIsScribbleEnabled(e.target.checked); + }; + + return ( + + + + + + ); +}; + +export default memo(HedPreprocessor); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx new file mode 100644 index 0000000000..6d4f61d8af --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx @@ -0,0 +1,31 @@ +import { Flex } from '@chakra-ui/react'; +import IAISlider from 'common/components/IAISlider'; +import { memo, useState } from 'react'; + +const LineartPreprocessor = () => { + const [detectResolution, setDetectResolution] = useState(512); + const [imageResolution, setImageResolution] = useState(512); + + return ( + + + + + ); +}; + +export default memo(LineartPreprocessor); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx new file mode 100644 index 0000000000..763d6f2b37 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx @@ -0,0 +1,42 @@ +import { Flex } from '@chakra-ui/react'; +import IAISlider from 'common/components/IAISlider'; +import IAISwitch from 'common/components/IAISwitch'; +import { ChangeEvent, memo, useState } from 'react'; + +const LineartPreprocessor = () => { + const [detectResolution, setDetectResolution] = useState(512); + const [imageResolution, setImageResolution] = useState(512); + const [isCoarseEnabled, setIsCoarseEnabled] = useState(false); + + const handleChangeScribble = (e: ChangeEvent) => { + setIsCoarseEnabled(e.target.checked); + }; + + return ( + + + + + + ); +}; + +export default memo(LineartPreprocessor); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessButton.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessButton.tsx new file mode 100644 index 0000000000..2fb6d60e55 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessButton.tsx @@ -0,0 +1,13 @@ +import IAIButton from 'common/components/IAIButton'; +import { memo } from 'react'; + +type ControlNetProcessButtonProps = { + onClick: () => void; +}; + +const ControlNetProcessButton = (props: ControlNetProcessButtonProps) => { + const { onClick } = props; + return Process Control Image; +}; + +export default memo(ControlNetProcessButton); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorImage.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorImage.tsx new file mode 100644 index 0000000000..6c253291f7 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorImage.tsx @@ -0,0 +1,33 @@ +import { Flex, Image } from '@chakra-ui/react'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { selectImagesById } from 'features/gallery/store/imagesSlice'; +import { DragEvent, memo, useCallback } from 'react'; +import { ImageDTO } from 'services/api'; + +type ControlNetProcessorImageProps = { + image: ImageDTO | undefined; + setImage: (image: ImageDTO) => void; +}; + +const ControlNetProcessorImage = (props: ControlNetProcessorImageProps) => { + const { image, setImage } = props; + const state = useAppSelector((state) => state); + const handleDrop = useCallback( + (e: DragEvent) => { + const name = e.dataTransfer.getData('invokeai/imageName'); + const droppedImage = selectImagesById(state, name); + if (droppedImage) { + setImage(droppedImage); + } + }, + [setImage, state] + ); + + if (!image) { + return Upload Image; + } + + return ; +}; + +export default memo(ControlNetProcessorImage); diff --git a/invokeai/frontend/web/src/features/controlNet/store/actions.ts b/invokeai/frontend/web/src/features/controlNet/store/actions.ts new file mode 100644 index 0000000000..9b6c11f22d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/store/actions.ts @@ -0,0 +1,7 @@ +import { createAction } from '@reduxjs/toolkit'; +import { ControlNetProcessorNode } from './types'; + +export const controlNetImageProcessed = createAction<{ + controlNetId: string; + processorNode: ControlNetProcessorNode; +}>('controlNet/imageProcessed'); diff --git a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts new file mode 100644 index 0000000000..88188a0a7f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts @@ -0,0 +1,159 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import { ImageDTO } from 'services/api'; + +export const CONTROLNET_MODELS = [ + 'lllyasviel/sd-controlnet-canny', + 'lllyasviel/sd-controlnet-depth', + 'lllyasviel/sd-controlnet-hed', + 'lllyasviel/sd-controlnet-seg', + 'lllyasviel/sd-controlnet-openpose', + 'lllyasviel/sd-controlnet-scribble', + 'lllyasviel/sd-controlnet-normal', + 'lllyasviel/sd-controlnet-mlsd', +] as const; + +export const CONTROLNET_PROCESSORS = [ + 'canny', + 'contentShuffle', + 'hed', + 'lineart', + 'lineartAnime', + 'mediapipeFace', + 'midasDepth', + 'mlsd', + 'normalBae', + 'openpose', + 'pidi', + 'zoeDepth', +] as const; + +export type ControlNetModel = (typeof CONTROLNET_MODELS)[number]; + +export const initialControlNet: Omit = { + isEnabled: true, + model: CONTROLNET_MODELS[0], + weight: 1, + beginStepPct: 0, + endStepPct: 1, + controlImage: null, + processedControlImage: null, +}; + +export type ControlNet = { + controlNetId: string; + isEnabled: boolean; + model: string; + weight: number; + beginStepPct: number; + endStepPct: number; + controlImage: ImageDTO | null; + processedControlImage: ImageDTO | null; +}; + +export type ControlNetState = { + controlNets: Record; +}; + +export const initialControlNetState: ControlNetState = { + controlNets: {}, +}; + +export const controlNetSlice = createSlice({ + name: 'controlNet', + initialState: initialControlNetState, + reducers: { + controlNetAddedFromModel: ( + state, + action: PayloadAction<{ controlNetId: string; model: ControlNetModel }> + ) => { + const { controlNetId, model } = action.payload; + state.controlNets[controlNetId] = { + ...initialControlNet, + controlNetId, + model, + }; + }, + controlNetAddedFromImage: ( + state, + action: PayloadAction<{ controlNetId: string; controlImage: ImageDTO }> + ) => { + const { controlNetId, controlImage } = action.payload; + state.controlNets[controlNetId] = { + ...initialControlNet, + controlNetId, + controlImage, + }; + }, + controlNetRemoved: (state, action: PayloadAction) => { + const controlNetId = action.payload; + delete state.controlNets[controlNetId]; + }, + controlNetToggled: (state, action: PayloadAction) => { + const controlNetId = action.payload; + state.controlNets[controlNetId].isEnabled = + !state.controlNets[controlNetId].isEnabled; + }, + controlNetImageChanged: ( + state, + action: PayloadAction<{ controlNetId: string; controlImage: ImageDTO }> + ) => { + const { controlNetId, controlImage } = action.payload; + state.controlNets[controlNetId].controlImage = controlImage; + }, + controlNetProcessedImageChanged: ( + state, + action: PayloadAction<{ + controlNetId: string; + processedControlImage: ImageDTO | null; + }> + ) => { + const { controlNetId, processedControlImage } = action.payload; + state.controlNets[controlNetId].processedControlImage = + processedControlImage; + }, + controlNetModelChanged: ( + state, + action: PayloadAction<{ controlNetId: string; model: ControlNetModel }> + ) => { + const { controlNetId, model } = action.payload; + state.controlNets[controlNetId].model = model; + }, + controlNetWeightChanged: ( + state, + action: PayloadAction<{ controlNetId: string; weight: number }> + ) => { + const { controlNetId, weight } = action.payload; + state.controlNets[controlNetId].weight = weight; + }, + controlNetBeginStepPctChanged: ( + state, + action: PayloadAction<{ controlNetId: string; beginStepPct: number }> + ) => { + const { controlNetId, beginStepPct } = action.payload; + state.controlNets[controlNetId].beginStepPct = beginStepPct; + }, + controlNetEndStepPctChanged: ( + state, + action: PayloadAction<{ controlNetId: string; endStepPct: number }> + ) => { + const { controlNetId, endStepPct } = action.payload; + state.controlNets[controlNetId].endStepPct = endStepPct; + }, + }, +}); + +export const { + controlNetAddedFromModel, + controlNetAddedFromImage, + controlNetRemoved, + controlNetImageChanged, + controlNetProcessedImageChanged, + controlNetToggled, + controlNetModelChanged, + controlNetWeightChanged, + controlNetBeginStepPctChanged, + controlNetEndStepPctChanged, +} = controlNetSlice.actions; + +export default controlNetSlice.reducer; diff --git a/invokeai/frontend/web/src/features/controlNet/store/types.ts b/invokeai/frontend/web/src/features/controlNet/store/types.ts new file mode 100644 index 0000000000..ca3af7b406 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/store/types.ts @@ -0,0 +1,28 @@ +import { + CannyImageProcessorInvocation, + ContentShuffleImageProcessorInvocation, + HedImageprocessorInvocation, + LineartAnimeImageProcessorInvocation, + LineartImageProcessorInvocation, + MediapipeFaceProcessorInvocation, + MidasDepthImageProcessorInvocation, + MlsdImageProcessorInvocation, + NormalbaeImageProcessorInvocation, + OpenposeImageProcessorInvocation, + PidiImageProcessorInvocation, + ZoeDepthImageProcessorInvocation, +} from 'services/api'; + +export type ControlNetProcessorNode = + | CannyImageProcessorInvocation + | HedImageprocessorInvocation + | LineartImageProcessorInvocation + | LineartAnimeImageProcessorInvocation + | OpenposeImageProcessorInvocation + | MidasDepthImageProcessorInvocation + | NormalbaeImageProcessorInvocation + | MlsdImageProcessorInvocation + | PidiImageProcessorInvocation + | ContentShuffleImageProcessorInvocation + | ZoeDepthImageProcessorInvocation + | MediapipeFaceProcessorInvocation; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx new file mode 100644 index 0000000000..e62e343d66 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx @@ -0,0 +1,62 @@ +import { Flex, Text, useDisclosure } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import IAICollapse from 'common/components/IAICollapse'; +import { memo, useCallback, useState } from 'react'; +import IAICustomSelect from 'common/components/IAICustomSelect'; +import IAIIconButton from 'common/components/IAIIconButton'; +import { FaPlus } from 'react-icons/fa'; +import CannyProcessor from 'features/controlNet/components/processors/CannyProcessor'; +import ControlNet from 'features/controlNet/components/ControlNet'; + +const CONTROLNET_MODELS = [ + 'lllyasviel/sd-controlnet-canny', + 'lllyasviel/sd-controlnet-depth', + 'lllyasviel/sd-controlnet-hed', + 'lllyasviel/sd-controlnet-seg', + 'lllyasviel/sd-controlnet-openpose', + 'lllyasviel/sd-controlnet-scribble', + 'lllyasviel/sd-controlnet-normal', + 'lllyasviel/sd-controlnet-mlsd', +]; + +const ParamControlNetCollapse = () => { + const { t } = useTranslation(); + const { isOpen, onToggle } = useDisclosure(); + const [model, setModel] = useState(CONTROLNET_MODELS[0]); + + const handleSetControlNet = useCallback( + (model: string | null | undefined) => { + if (model) { + setModel(model); + } + }, + [] + ); + + return ( + + // + // + // + // } + // /> + // + // + // + ); +}; + +export default memo(ParamControlNetCollapse); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx index 0976e3eef2..a1084c4b8d 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx @@ -9,6 +9,7 @@ import ParamSymmetryCollapse from 'features/parameters/components/Parameters/Sym import ParamHiresCollapse from 'features/parameters/components/Parameters/Hires/ParamHiresCollapse'; import ParamSeamlessCollapse from 'features/parameters/components/Parameters/Seamless/ParamSeamlessCollapse'; import TextToImageTabCoreParameters from './TextToImageTabCoreParameters'; +import ParamControlNetCollapse from 'features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse'; const TextToImageTabParameters = () => { return ( @@ -18,6 +19,7 @@ const TextToImageTabParameters = () => { + From 3b9426eb7258b8de5c7dafe8d9d6622828da2eba Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Jun 2023 18:27:39 +1000 Subject: [PATCH 07/47] feat(ui): controlnet/image dnd wip Implement `dnd-kit` for image drag and drop - vastly simplifies logic bc we can drag and drop non-serializable data (like an `ImageDTO`) - also much prettier - also will fix conflicts with file upload via OS drag and drop, bc `dnd-kit` does not use native HTML drag and drop API - Implemented for Init image, controlnet, and node editor so far More progress on the ControlNet UI --- invokeai/frontend/web/package.json | 1 + .../components/ImageDnd/ImageDndContext.tsx | 45 ++++ .../components/ImageDnd/OverlayDragImage.tsx | 23 ++ .../web/src/app/components/InvokeAIUI.tsx | 13 +- .../listeners/initialImageSelected.ts | 6 +- invokeai/frontend/web/src/app/store/store.ts | 3 + .../web/src/common/components/IAICheckbox.tsx | 17 -- .../src/common/components/IAIFullCheckbox.tsx | 25 +++ .../common/components/IAISimpleCheckbox.tsx | 19 ++ .../components/ImageMetadataOverlay.tsx | 4 +- .../IAICanvasToolbar/IAICanvasMaskOptions.tsx | 6 +- .../IAICanvasSettingsButtonPopover.tsx | 20 +- .../controlNet/components/ControlNet.tsx | 107 +++++++++- .../parameters/IAISelectableImage.tsx | 200 ++++++++++++++++++ .../ParamControlNetBeginStepPct.tsx | 44 ++++ .../parameters/ParamControlNetEndStepPct.tsx | 42 ++++ .../parameters/ParamControlNetIsEnabled.tsx | 28 +++ .../parameters/ParamControlNetModel.tsx | 38 ++++ .../parameters/ParamControlNetWeight.tsx | 42 ++++ .../components/processors/CannyProcessor.tsx | 2 - .../common/ControlNetProcessorImage.tsx | 33 --- .../controlNet/store/controlNetSlice.ts | 66 ++++-- .../gallery/components/HoverableImage.tsx | 22 +- .../components/ImageGalleryContent.tsx | 8 +- .../fields/ImageInputFieldComponent.tsx | 40 ++-- .../ControlNet/ParamControlNetCollapse.tsx | 88 ++++---- .../ImageToImage/InitialImageDisplay.tsx | 1 - .../ImageToImage/InitialImagePreview.tsx | 29 ++- .../src/features/parameters/store/actions.ts | 19 -- .../ModelManager/AddCheckpointModel.tsx | 6 +- .../components/ModelManager/MergeModels.tsx | 4 +- .../components/ModelManager/SearchModels.tsx | 8 +- .../ImageToImageTabParameters.tsx | 2 + .../UnifiedCanvasDarkenOutsideSelection.tsx | 4 +- .../UnifiedCanvasEnableMask.tsx | 4 +- .../UnifiedCanvasLimitStrokesToBox.tsx | 4 +- .../UnifiedCanvasPreserveMask.tsx | 4 +- .../UnifiedCanvasSettings.tsx | 12 +- .../UnifiedCanvasShowGrid.tsx | 4 +- .../UnifiedCanvasSnapToGrid.tsx | 4 +- .../frontend/web/src/services/types/guards.ts | 17 ++ invokeai/frontend/web/yarn.lock | 23 ++ 42 files changed, 853 insertions(+), 234 deletions(-) create mode 100644 invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx create mode 100644 invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx delete mode 100644 invokeai/frontend/web/src/common/components/IAICheckbox.tsx create mode 100644 invokeai/frontend/web/src/common/components/IAIFullCheckbox.tsx create mode 100644 invokeai/frontend/web/src/common/components/IAISimpleCheckbox.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginStepPct.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetEndStepPct.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsEnabled.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetModel.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetWeight.tsx delete mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorImage.tsx diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index dd1c87effb..104fad3364 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -60,6 +60,7 @@ "@chakra-ui/styled-system": "^2.9.0", "@chakra-ui/theme-tools": "^2.0.16", "@dagrejs/graphlib": "^2.1.12", + "@dnd-kit/core": "^6.0.8", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@floating-ui/react-dom": "^2.0.0", diff --git a/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx b/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx new file mode 100644 index 0000000000..9e8495aa63 --- /dev/null +++ b/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx @@ -0,0 +1,45 @@ +import { + DndContext, + DragEndEvent, + DragOverlay, + DragStartEvent, +} from '@dnd-kit/core'; +import { PropsWithChildren, memo, useCallback, useState } from 'react'; +import OverlayDragImage from './OverlayDragImage'; +import { ImageDTO } from 'services/api'; +import { isImageDTO } from 'services/types/guards'; + +type ImageDndContextProps = PropsWithChildren; + +const ImageDndContext = (props: ImageDndContextProps) => { + const [draggedImage, setDraggedImage] = useState(null); + + const handleDragStart = useCallback((event: DragStartEvent) => { + const dragData = event.active.data.current; + if (dragData && 'image' in dragData && isImageDTO(dragData.image)) { + setDraggedImage(dragData.image); + } + }, []); + + const handleDragEnd = useCallback( + (event: DragEndEvent) => { + const handleDrop = event.over?.data.current?.handleDrop; + if (handleDrop && typeof handleDrop === 'function' && draggedImage) { + handleDrop(draggedImage); + } + setDraggedImage(null); + }, + [draggedImage] + ); + + return ( + + {props.children} + + {draggedImage && } + + + ); +}; + +export default memo(ImageDndContext); diff --git a/invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx b/invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx new file mode 100644 index 0000000000..59fe6a7971 --- /dev/null +++ b/invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx @@ -0,0 +1,23 @@ +import { Image } from '@chakra-ui/react'; +import { memo } from 'react'; +import { ImageDTO } from 'services/api'; + +type OverlayDragImageProps = { + image: ImageDTO; +}; + +const OverlayDragImage = (props: OverlayDragImageProps) => { + return ( + + ); +}; + +export default memo(OverlayDragImage); diff --git a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx index 69b2756f96..c94f7624b2 100644 --- a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx +++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx @@ -16,6 +16,7 @@ import { PartialAppConfig } from 'app/types/invokeai'; import '../../i18n'; import { socketMiddleware } from 'services/events/middleware'; import { Middleware } from '@reduxjs/toolkit'; +import ImageDndContext from './ImageDnd/ImageDndContext'; const App = lazy(() => import('./App')); const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider')); @@ -69,11 +70,13 @@ const InvokeAIUI = ({ }> - + + + diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts index 940cc84c1e..9069e477ac 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts @@ -2,12 +2,10 @@ import { initialImageChanged } from 'features/parameters/store/generationSlice'; import { t } from 'i18next'; import { addToast } from 'features/system/store/systemSlice'; import { startAppListening } from '..'; -import { - initialImageSelected, - isImageDTO, -} from 'features/parameters/store/actions'; +import { initialImageSelected } from 'features/parameters/store/actions'; import { makeToast } from 'app/components/Toaster'; import { selectImagesById } from 'features/gallery/store/imagesSlice'; +import { isImageDTO } from 'services/types/guards'; export const addInitialImageSelectedListener = () => { startAppListening({ diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 521610adcc..f577b73895 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -13,6 +13,7 @@ import galleryReducer from 'features/gallery/store/gallerySlice'; import imagesReducer from 'features/gallery/store/imagesSlice'; import lightboxReducer from 'features/lightbox/store/lightboxSlice'; import generationReducer from 'features/parameters/store/generationSlice'; +import controlNetReducer from 'features/controlNet/store/controlNetSlice'; import postprocessingReducer from 'features/parameters/store/postprocessingSlice'; import systemReducer from 'features/system/store/systemSlice'; // import sessionReducer from 'features/system/store/sessionSlice'; @@ -45,6 +46,7 @@ const allReducers = { ui: uiReducer, hotkeys: hotkeysReducer, images: imagesReducer, + controlNet: controlNetReducer, // session: sessionReducer, }; @@ -62,6 +64,7 @@ const rememberedKeys: (keyof typeof allReducers)[] = [ 'postprocessing', 'system', 'ui', + 'controlNet', // 'hotkeys', // 'config', ]; diff --git a/invokeai/frontend/web/src/common/components/IAICheckbox.tsx b/invokeai/frontend/web/src/common/components/IAICheckbox.tsx deleted file mode 100644 index eb423b2b27..0000000000 --- a/invokeai/frontend/web/src/common/components/IAICheckbox.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Checkbox, CheckboxProps } from '@chakra-ui/react'; -import { memo, ReactNode } from 'react'; - -type IAICheckboxProps = CheckboxProps & { - label: string | ReactNode; -}; - -const IAICheckbox = (props: IAICheckboxProps) => { - const { label, ...rest } = props; - return ( - - {label} - - ); -}; - -export default memo(IAICheckbox); diff --git a/invokeai/frontend/web/src/common/components/IAIFullCheckbox.tsx b/invokeai/frontend/web/src/common/components/IAIFullCheckbox.tsx new file mode 100644 index 0000000000..97ff24689c --- /dev/null +++ b/invokeai/frontend/web/src/common/components/IAIFullCheckbox.tsx @@ -0,0 +1,25 @@ +import { + Checkbox, + CheckboxProps, + FormControl, + FormControlProps, + FormLabel, +} from '@chakra-ui/react'; +import { memo, ReactNode } from 'react'; + +type IAIFullCheckboxProps = CheckboxProps & { + label: string | ReactNode; + formControlProps?: FormControlProps; +}; + +const IAIFullCheckbox = (props: IAIFullCheckboxProps) => { + const { label, formControlProps, ...rest } = props; + return ( + + {label} + + + ); +}; + +export default memo(IAIFullCheckbox); diff --git a/invokeai/frontend/web/src/common/components/IAISimpleCheckbox.tsx b/invokeai/frontend/web/src/common/components/IAISimpleCheckbox.tsx new file mode 100644 index 0000000000..4d21d3d3d0 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/IAISimpleCheckbox.tsx @@ -0,0 +1,19 @@ +import { Checkbox, CheckboxProps, Text } from '@chakra-ui/react'; +import { memo, ReactNode } from 'react'; + +type IAISimpleCheckboxProps = CheckboxProps & { + label: string | ReactNode; +}; + +const IAISimpleCheckbox = (props: IAISimpleCheckboxProps) => { + const { label, ...rest } = props; + return ( + + + {label} + + + ); +}; + +export default memo(IAISimpleCheckbox); diff --git a/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx b/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx index 95c888d658..e3bee9797b 100644 --- a/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx +++ b/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx @@ -23,9 +23,9 @@ const ImageMetadataOverlay = ({ image }: ImageMetadataOverlayProps) => { flexDirection: 'column', position: 'absolute', top: 0, - right: 0, + insetInlineStart: 0, p: 2, - alignItems: 'flex-end', + alignItems: 'flex-start', gap: 2, }} > 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 b345f2cda0..2f74e5542a 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx @@ -2,7 +2,7 @@ import { ButtonGroup, Flex } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIButton from 'common/components/IAIButton'; -import IAICheckbox from 'common/components/IAICheckbox'; +import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox'; import IAIColorPicker from 'common/components/IAIColorPicker'; import IAIIconButton from 'common/components/IAIIconButton'; import IAIPopover from 'common/components/IAIPopover'; @@ -117,12 +117,12 @@ const IAICanvasMaskOptions = () => { } > - - diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx index 94a990bb4c..638332809c 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx @@ -1,7 +1,7 @@ import { Flex } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAICheckbox from 'common/components/IAICheckbox'; +import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox'; import IAIIconButton from 'common/components/IAIIconButton'; import IAIPopover from 'common/components/IAIPopover'; import { canvasSelector } from 'features/canvas/store/canvasSelectors'; @@ -102,50 +102,50 @@ const IAICanvasSettingsButtonPopover = () => { } > - dispatch(setShouldShowIntermediates(e.target.checked)) } /> - dispatch(setShouldShowGrid(e.target.checked))} /> - - dispatch(setShouldDarkenOutsideBoundingBox(e.target.checked)) } /> - dispatch(setShouldAutoSave(e.target.checked))} /> - dispatch(setShouldCropToBoundingBoxOnSave(e.target.checked)) } /> - dispatch(setShouldRestrictStrokesToBox(e.target.checked)) } /> - @@ -153,7 +153,7 @@ const IAICanvasSettingsButtonPopover = () => { } /> - dispatch(setShouldAntialias(e.target.checked))} diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx index 51fa33353b..ad4c6e714b 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx @@ -1,7 +1,32 @@ -import { memo } from 'react'; +import { memo, useCallback } from 'react'; import { ControlNetProcessorNode } from '../store/types'; import { ImageDTO } from 'services/api'; import CannyProcessor from './processors/CannyProcessor'; +import { + ControlNet, + ControlNetModel, + controlNetBeginStepPctChanged, + controlNetEndStepPctChanged, + controlNetImageChanged, + controlNetModelChanged, + controlNetProcessedImageChanged, + controlNetRemoved, + controlNetToggled, + controlNetWeightChanged, + isControlNetImageProcessedToggled, +} from '../store/controlNetSlice'; +import { useAppDispatch } from 'app/store/storeHooks'; +import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox'; +import IAISlider from 'common/components/IAISlider'; +import ParamControlNetIsEnabled from './parameters/ParamControlNetIsEnabled'; +import ParamControlNetModel from './parameters/ParamControlNetModel'; +import ParamControlNetWeight from './parameters/ParamControlNetWeight'; +import ParamControlNetBeginStepPct from './parameters/ParamControlNetBeginStepPct'; +import ParamControlNetEndStepPct from './parameters/ParamControlNetEndStepPct'; +import { Flex, HStack, VStack } from '@chakra-ui/react'; +import IAISelectableImage from './parameters/IAISelectableImage'; +import IAIButton from 'common/components/IAIButton'; +import IAIIconButton from 'common/components/IAIIconButton'; export type ControlNetProcessorProps = { controlNetId: string; @@ -16,11 +41,83 @@ const renderProcessorComponent = (props: ControlNetProcessorProps) => { } }; -const ControlNet = () => { +type ControlNetProps = { + controlNet: ControlNet; +}; + +const ControlNet = (props: ControlNetProps) => { + const { + controlNetId, + isEnabled, + model, + weight, + beginStepPct, + endStepPct, + controlImage, + isControlImageProcessed, + processedControlImage, + } = props.controlNet; + const dispatch = useAppDispatch(); + + const handleControlImageChanged = useCallback( + (controlImage: ImageDTO) => { + dispatch(controlNetImageChanged({ controlNetId, controlImage })); + }, + [controlNetId, dispatch] + ); + + const handleControlImageReset = useCallback(() => { + dispatch(controlNetImageChanged({ controlNetId, controlImage: null })); + }, [controlNetId, dispatch]); + + const handleControlNetRemoved = useCallback(() => { + dispatch(controlNetRemoved(controlNetId)); + }, [controlNetId, dispatch]); + + const handleIsControlImageProcessedToggled = useCallback(() => { + dispatch( + isControlNetImageProcessedToggled({ + controlNetId, + }) + ); + }, [controlNetId, dispatch]); + + const handleProcessedControlImageChanged = useCallback( + (processedControlImage: ImageDTO | null) => { + dispatch( + controlNetProcessedImageChanged({ + controlNetId, + processedControlImage, + }) + ); + }, + [controlNetId, dispatch] + ); + return ( -
-

ControlNet

-
+ + Remove ControlNet + + + + + + + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx new file mode 100644 index 0000000000..62cd4603e5 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx @@ -0,0 +1,200 @@ +import { + Box, + Flex, + Icon, + IconButtonProps, + Image, + Text, +} from '@chakra-ui/react'; +import { useDroppable } from '@dnd-kit/core'; +import IAIIconButton from 'common/components/IAIIconButton'; +import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; +import { useGetUrl } from 'common/util/getUrl'; +import ImageFallbackSpinner from 'features/gallery/components/ImageFallbackSpinner'; +import { AnimatePresence, motion } from 'framer-motion'; +import { SyntheticEvent } from 'react'; +import { memo, useRef } from 'react'; +import { FaImage, FaUndo } from 'react-icons/fa'; +import { ImageDTO } from 'services/api'; +import { v4 as uuidv4 } from 'uuid'; + +type IAISelectableImageProps = { + image: ImageDTO | null | undefined; + onChange: (image: ImageDTO) => void; + onReset?: () => void; + onError?: (event: SyntheticEvent) => void; + resetIconSize?: IconButtonProps['size']; +}; + +const IAISelectableImage = (props: IAISelectableImageProps) => { + const { image, onChange, onReset, onError, resetIconSize = 'md' } = props; + const droppableId = useRef(uuidv4()); + const { getUrl } = useGetUrl(); + const { isOver, setNodeRef, active } = useDroppable({ + id: droppableId.current, + data: { + handleDrop: onChange, + }, + }); + + return ( + + {image && ( + + } + onError={onError} + sx={{ + borderRadius: 'base', + }} + /> + + + {active && } + + + )} + {!image && ( + <> + + + + + {active && } + + + )} + {image && onReset && ( + + } + onClick={onReset} + /> + + )} + + ); +}; + +export default memo(IAISelectableImage); + +type DropOverlayProps = { + isOver: boolean; +}; + +const DropOverlay = (props: DropOverlayProps) => { + const { isOver } = props; + return ( + + + + + + Drop Image + + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginStepPct.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginStepPct.tsx new file mode 100644 index 0000000000..914bfa2818 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginStepPct.tsx @@ -0,0 +1,44 @@ +import { useAppDispatch } from 'app/store/storeHooks'; +import IAISlider from 'common/components/IAISlider'; +import { controlNetBeginStepPctChanged } from 'features/controlNet/store/controlNetSlice'; +import { memo, useCallback } from 'react'; + +type ParamControlNetBeginStepPctProps = { + controlNetId: string; + beginStepPct: number; +}; + +const ParamControlNetBeginStepPct = ( + props: ParamControlNetBeginStepPctProps +) => { + const { controlNetId, beginStepPct } = props; + const dispatch = useAppDispatch(); + + const handleBeginStepPctChanged = useCallback( + (beginStepPct: number) => { + dispatch(controlNetBeginStepPctChanged({ controlNetId, beginStepPct })); + }, + [controlNetId, dispatch] + ); + + const handleBeginStepPctReset = () => { + dispatch(controlNetBeginStepPctChanged({ controlNetId, beginStepPct: 0 })); + }; + + return ( + + ); +}; + +export default memo(ParamControlNetBeginStepPct); diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetEndStepPct.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetEndStepPct.tsx new file mode 100644 index 0000000000..d3d831cf31 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetEndStepPct.tsx @@ -0,0 +1,42 @@ +import { useAppDispatch } from 'app/store/storeHooks'; +import IAISlider from 'common/components/IAISlider'; +import { controlNetEndStepPctChanged } from 'features/controlNet/store/controlNetSlice'; +import { memo, useCallback } from 'react'; + +type ParamControlNetEndStepPctProps = { + controlNetId: string; + endStepPct: number; +}; + +const ParamControlNetEndStepPct = (props: ParamControlNetEndStepPctProps) => { + const { controlNetId, endStepPct } = props; + const dispatch = useAppDispatch(); + + const handleEndStepPctChanged = useCallback( + (endStepPct: number) => { + dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct })); + }, + [controlNetId, dispatch] + ); + + const handleEndStepPctReset = () => { + dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct: 0 })); + }; + + return ( + + ); +}; + +export default memo(ParamControlNetEndStepPct); diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsEnabled.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsEnabled.tsx new file mode 100644 index 0000000000..f29b9396b4 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsEnabled.tsx @@ -0,0 +1,28 @@ +import { useAppDispatch } from 'app/store/storeHooks'; +import IAIFullCheckbox from 'common/components/IAIFullCheckbox'; +import { controlNetToggled } from 'features/controlNet/store/controlNetSlice'; +import { memo, useCallback } from 'react'; + +type ParamControlNetIsEnabledProps = { + controlNetId: string; + isEnabled: boolean; +}; + +const ParamControlNetIsEnabled = (props: ParamControlNetIsEnabledProps) => { + const { controlNetId, isEnabled } = props; + const dispatch = useAppDispatch(); + + const handleIsEnabledChanged = useCallback(() => { + dispatch(controlNetToggled(controlNetId)); + }, [dispatch, controlNetId]); + + return ( + + ); +}; + +export default memo(ParamControlNetIsEnabled); diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetModel.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetModel.tsx new file mode 100644 index 0000000000..d38fdc902c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetModel.tsx @@ -0,0 +1,38 @@ +import { useAppDispatch } from 'app/store/storeHooks'; +import IAICustomSelect from 'common/components/IAICustomSelect'; +import { + CONTROLNET_MODELS, + ControlNetModel, + controlNetModelChanged, +} from 'features/controlNet/store/controlNetSlice'; +import { memo, useCallback } from 'react'; + +type ParamIsControlNetModelProps = { + controlNetId: string; + model: ControlNetModel; +}; + +const ParamIsControlNetModel = (props: ParamIsControlNetModelProps) => { + const { controlNetId, model } = props; + const dispatch = useAppDispatch(); + + const handleModelChanged = useCallback( + (val: string | null | undefined) => { + // TODO: do not cast + const model = val as ControlNetModel; + dispatch(controlNetModelChanged({ controlNetId, model })); + }, + [controlNetId, dispatch] + ); + + return ( + + ); +}; + +export default memo(ParamIsControlNetModel); diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetWeight.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetWeight.tsx new file mode 100644 index 0000000000..11272582d0 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetWeight.tsx @@ -0,0 +1,42 @@ +import { useAppDispatch } from 'app/store/storeHooks'; +import IAISlider from 'common/components/IAISlider'; +import { controlNetWeightChanged } from 'features/controlNet/store/controlNetSlice'; +import { memo, useCallback } from 'react'; + +type ParamControlNetWeightProps = { + controlNetId: string; + weight: number; +}; + +const ParamControlNetWeight = (props: ParamControlNetWeightProps) => { + const { controlNetId, weight } = props; + const dispatch = useAppDispatch(); + + const handleWeightChanged = useCallback( + (weight: number) => { + dispatch(controlNetWeightChanged({ controlNetId, weight })); + }, + [controlNetId, dispatch] + ); + + const handleWeightReset = () => { + dispatch(controlNetWeightChanged({ controlNetId, weight: 1 })); + }; + + return ( + + ); +}; + +export default memo(ParamControlNetWeight); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx index 012fb8532b..dc735a1ee5 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx @@ -4,8 +4,6 @@ import { memo, useCallback, useState } from 'react'; import ControlNetProcessButton from './common/ControlNetProcessButton'; import { useAppDispatch } from 'app/store/storeHooks'; import { controlNetImageProcessed } from 'features/controlNet/store/actions'; -import { ImageDTO } from 'services/api'; -import ControlNetProcessorImage from './common/ControlNetProcessorImage'; import { ControlNetProcessorProps } from '../ControlNet'; export const CANNY_PROCESSOR = 'canny_processor'; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorImage.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorImage.tsx deleted file mode 100644 index 6c253291f7..0000000000 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorImage.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Flex, Image } from '@chakra-ui/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { selectImagesById } from 'features/gallery/store/imagesSlice'; -import { DragEvent, memo, useCallback } from 'react'; -import { ImageDTO } from 'services/api'; - -type ControlNetProcessorImageProps = { - image: ImageDTO | undefined; - setImage: (image: ImageDTO) => void; -}; - -const ControlNetProcessorImage = (props: ControlNetProcessorImageProps) => { - const { image, setImage } = props; - const state = useAppSelector((state) => state); - const handleDrop = useCallback( - (e: DragEvent) => { - const name = e.dataTransfer.getData('invokeai/imageName'); - const droppedImage = selectImagesById(state, name); - if (droppedImage) { - setImage(droppedImage); - } - }, - [setImage, state] - ); - - if (!image) { - return Upload Image; - } - - return ; -}; - -export default memo(ControlNetProcessorImage); diff --git a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts index 88188a0a7f..5909c85cac 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts @@ -1,5 +1,10 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; +import { + $CombinedState, + PayloadAction, + createSelector, +} from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; +import { RootState } from 'app/store/store'; import { ImageDTO } from 'services/api'; export const CONTROLNET_MODELS = [ @@ -11,22 +16,22 @@ export const CONTROLNET_MODELS = [ 'lllyasviel/sd-controlnet-scribble', 'lllyasviel/sd-controlnet-normal', 'lllyasviel/sd-controlnet-mlsd', -] as const; +]; -export const CONTROLNET_PROCESSORS = [ - 'canny', - 'contentShuffle', - 'hed', - 'lineart', - 'lineartAnime', - 'mediapipeFace', - 'midasDepth', - 'mlsd', - 'normalBae', - 'openpose', - 'pidi', - 'zoeDepth', -] as const; +// export const CONTROLNET_PROCESSORS = [ +// 'canny', +// 'contentShuffle', +// 'hed', +// 'lineart', +// 'lineartAnime', +// 'mediapipeFace', +// 'midasDepth', +// 'mlsd', +// 'normalBae', +// 'openpose', +// 'pidi', +// 'zoeDepth', +// ] as const; export type ControlNetModel = (typeof CONTROLNET_MODELS)[number]; @@ -37,6 +42,7 @@ export const initialControlNet: Omit = { beginStepPct: 0, endStepPct: 1, controlImage: null, + isControlImageProcessed: false, processedControlImage: null, }; @@ -48,6 +54,7 @@ export type ControlNet = { beginStepPct: number; endStepPct: number; controlImage: ImageDTO | null; + isControlImageProcessed: boolean; processedControlImage: ImageDTO | null; }; @@ -63,15 +70,14 @@ export const controlNetSlice = createSlice({ name: 'controlNet', initialState: initialControlNetState, reducers: { - controlNetAddedFromModel: ( + controlNetAdded: ( state, - action: PayloadAction<{ controlNetId: string; model: ControlNetModel }> + action: PayloadAction<{ controlNetId: string }> ) => { - const { controlNetId, model } = action.payload; + const { controlNetId } = action.payload; state.controlNets[controlNetId] = { ...initialControlNet, controlNetId, - model, }; }, controlNetAddedFromImage: ( @@ -96,11 +102,24 @@ export const controlNetSlice = createSlice({ }, controlNetImageChanged: ( state, - action: PayloadAction<{ controlNetId: string; controlImage: ImageDTO }> + action: PayloadAction<{ + controlNetId: string; + controlImage: ImageDTO | null; + }> ) => { const { controlNetId, controlImage } = action.payload; state.controlNets[controlNetId].controlImage = controlImage; }, + isControlNetImageProcessedToggled: ( + state, + action: PayloadAction<{ + controlNetId: string; + }> + ) => { + const { controlNetId } = action.payload; + state.controlNets[controlNetId].isControlImageProcessed = + !state.controlNets[controlNetId].isControlImageProcessed; + }, controlNetProcessedImageChanged: ( state, action: PayloadAction<{ @@ -144,10 +163,11 @@ export const controlNetSlice = createSlice({ }); export const { - controlNetAddedFromModel, + controlNetAdded, controlNetAddedFromImage, controlNetRemoved, controlNetImageChanged, + isControlNetImageProcessedToggled, controlNetProcessedImageChanged, controlNetToggled, controlNetModelChanged, @@ -157,3 +177,5 @@ export const { } = controlNetSlice.actions; export default controlNetSlice.reducer; + +export const controlNetSelector = (state: RootState) => state.controlNet; diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx index f652cebda2..c1fe2569e3 100644 --- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx @@ -39,6 +39,8 @@ import { } from '../store/actions'; import { useAppToaster } from 'app/components/Toaster'; import { ImageDTO } from 'services/api'; +import { useDraggable } from '@dnd-kit/core'; +import { CSS } from '@dnd-kit/utilities'; export const selector = createSelector( [gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector], @@ -117,6 +119,13 @@ const HoverableImage = memo((props: HoverableImageProps) => { const { recallBothPrompts, recallSeed, recallAllParameters } = useRecallParameters(); + const { attributes, listeners, setNodeRef } = useDraggable({ + id: image_name, + data: { + image, + }, + }); + const handleMouseOver = () => setIsHovered(true); const handleMouseOut = () => setIsHovered(false); @@ -212,7 +221,12 @@ const HoverableImage = memo((props: HoverableImageProps) => { }; return ( - <> + menuProps={{ size: 'sm', isLazy: true }} renderMenu={() => ( @@ -291,8 +305,8 @@ const HoverableImage = memo((props: HoverableImageProps) => { onMouseOver={handleMouseOver} onMouseOut={handleMouseOut} userSelect="none" - draggable={true} - onDragStart={handleDragStart} + // draggable={true} + // onDragStart={handleDragStart} onClick={handleSelectImage} ref={ref} sx={{ @@ -373,7 +387,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { onClose={onDeleteDialogClose} handleDelete={handleDelete} /> - + ); }, memoEqualityCheck); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx index 77f42a11a6..fe8690e379 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx @@ -10,7 +10,7 @@ import { } from '@chakra-ui/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIButton from 'common/components/IAIButton'; -import IAICheckbox from 'common/components/IAICheckbox'; +import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox'; import IAIIconButton from 'common/components/IAIIconButton'; import IAIPopover from 'common/components/IAIPopover'; import IAISlider from 'common/components/IAISlider'; @@ -233,7 +233,7 @@ const ImageGalleryContent = () => { withReset handleReset={() => dispatch(setGalleryImageMinimumWidth(64))} /> - @@ -244,14 +244,14 @@ const ImageGalleryContent = () => { ) } /> - ) => dispatch(setShouldAutoSwitchToNewImages(e.target.checked)) } /> - ) => diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx index 57cefb0a9c..1232ff28e1 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx @@ -1,39 +1,26 @@ -import { Box, Image } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; -import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder'; -import { useGetUrl } from 'common/util/getUrl'; -import useGetImageByName from 'features/gallery/hooks/useGetImageByName'; import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; import { ImageInputFieldTemplate, ImageInputFieldValue, } from 'features/nodes/types/types'; -import { DragEvent, memo, useCallback, useState } from 'react'; +import { memo, useCallback } from 'react'; import { FieldComponentProps } from './types'; +import IAISelectableImage from 'features/controlNet/components/parameters/IAISelectableImage'; +import { ImageDTO } from 'services/api'; +import { Flex } from '@chakra-ui/react'; const ImageInputFieldComponent = ( props: FieldComponentProps ) => { const { nodeId, field } = props; - const getImageByName = useGetImageByName(); const dispatch = useAppDispatch(); - const [url, setUrl] = useState(field.value?.image_url); - const { getUrl } = useGetUrl(); - - const handleDrop = useCallback( - (e: DragEvent) => { - const name = e.dataTransfer.getData('invokeai/imageName'); - const image = getImageByName(name); - - if (!image) { - return; - } - - setUrl(image.image_url); + const handleChange = useCallback( + (image: ImageDTO) => { dispatch( fieldValueChanged({ nodeId, @@ -42,13 +29,20 @@ const ImageInputFieldComponent = ( }) ); }, - [getImageByName, dispatch, field.name, nodeId] + [dispatch, field.name, nodeId] ); return ( - - } /> - + + + ); }; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx index e62e343d66..2c4088d376 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx @@ -1,61 +1,59 @@ -import { Flex, Text, useDisclosure } from '@chakra-ui/react'; +import { Flex, useDisclosure } from '@chakra-ui/react'; import { useTranslation } from 'react-i18next'; import IAICollapse from 'common/components/IAICollapse'; -import { memo, useCallback, useState } from 'react'; -import IAICustomSelect from 'common/components/IAICustomSelect'; +import { memo, useCallback } from 'react'; import IAIIconButton from 'common/components/IAIIconButton'; import { FaPlus } from 'react-icons/fa'; -import CannyProcessor from 'features/controlNet/components/processors/CannyProcessor'; import ControlNet from 'features/controlNet/components/ControlNet'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { createSelector } from '@reduxjs/toolkit'; +import { + controlNetAdded, + controlNetSelector, +} from 'features/controlNet/store/controlNetSlice'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { map } from 'lodash-es'; +import { v4 as uuidv4 } from 'uuid'; -const CONTROLNET_MODELS = [ - 'lllyasviel/sd-controlnet-canny', - 'lllyasviel/sd-controlnet-depth', - 'lllyasviel/sd-controlnet-hed', - 'lllyasviel/sd-controlnet-seg', - 'lllyasviel/sd-controlnet-openpose', - 'lllyasviel/sd-controlnet-scribble', - 'lllyasviel/sd-controlnet-normal', - 'lllyasviel/sd-controlnet-mlsd', -]; +const selector = createSelector( + controlNetSelector, + (controlNet) => { + const { controlNets } = controlNet; + + return { controlNets }; + }, + defaultSelectorOptions +); const ParamControlNetCollapse = () => { const { t } = useTranslation(); const { isOpen, onToggle } = useDisclosure(); - const [model, setModel] = useState(CONTROLNET_MODELS[0]); + const { controlNets } = useAppSelector(selector); + const dispatch = useAppDispatch(); - const handleSetControlNet = useCallback( - (model: string | null | undefined) => { - if (model) { - setModel(model); - } - }, - [] - ); + const handleClickedAddControlNet = useCallback(() => { + dispatch(controlNetAdded({ controlNetId: uuidv4() })); + }, [dispatch]); return ( - - // - // - // - // } - // /> - // - // - // + + + } + /> + + {map(controlNets, (c) => ( + + ))} + ); }; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx index f17ebcbdc0..1746c9b592 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx @@ -28,7 +28,6 @@ const InitialImageDisplay = () => { gap: 4, }} > -
diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx index cfe1513420..8cd7b99dc5 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx @@ -2,7 +2,10 @@ import { Flex, Icon, Image } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useGetUrl } from 'common/util/getUrl'; -import { clearInitialImage } from 'features/parameters/store/generationSlice'; +import { + clearInitialImage, + initialImageChanged, +} from 'features/parameters/store/generationSlice'; import { DragEvent, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; @@ -13,6 +16,8 @@ import ImageFallbackSpinner from 'features/gallery/components/ImageFallbackSpinn import { FaImage } from 'react-icons/fa'; import { configSelector } from '../../../../system/store/configSelectors'; import { useAppToaster } from 'app/components/Toaster'; +import IAISelectableImage from 'features/controlNet/components/parameters/IAISelectableImage'; +import { ImageDTO } from 'services/api'; const selector = createSelector( [generationSelector], @@ -51,14 +56,17 @@ const InitialImagePreview = () => { } }, [dispatch, t, toaster, shouldFetchImages]); - const handleDrop = useCallback( - (e: DragEvent) => { - const name = e.dataTransfer.getData('invokeai/imageName'); - dispatch(initialImageSelected(name)); + const handleChange = useCallback( + (image: ImageDTO) => { + dispatch(initialImageChanged(image)); }, [dispatch] ); + const handleReset = useCallback(() => { + dispatch(clearInitialImage()); + }, [dispatch]); + return ( { alignItems: 'center', justifyContent: 'center', }} - onDrop={handleDrop} + // onDrop={handleDrop} > - {initialImage?.image_url && ( + + {/* {initialImage?.image_url && ( <> { color: 'base.500', }} /> - )} + )} */} ); }; diff --git a/invokeai/frontend/web/src/features/parameters/store/actions.ts b/invokeai/frontend/web/src/features/parameters/store/actions.ts index e9b90134e1..eba01248d1 100644 --- a/invokeai/frontend/web/src/features/parameters/store/actions.ts +++ b/invokeai/frontend/web/src/features/parameters/store/actions.ts @@ -7,25 +7,6 @@ export type ImageNameAndOrigin = { image_origin: ResourceOrigin; }; -export const isImageDTO = (image: any): image is ImageDTO => { - return ( - image && - isObject(image) && - 'image_name' in image && - image?.image_name !== undefined && - 'image_origin' in image && - image?.image_origin !== undefined && - 'image_url' in image && - image?.image_url !== undefined && - 'thumbnail_url' in image && - image?.thumbnail_url !== undefined && - 'image_category' in image && - image?.image_category !== undefined && - 'created_at' in image && - image?.created_at !== undefined - ); -}; - export const initialImageSelected = createAction( 'generation/initialImageSelected' ); diff --git a/invokeai/frontend/web/src/features/system/components/ModelManager/AddCheckpointModel.tsx b/invokeai/frontend/web/src/features/system/components/ModelManager/AddCheckpointModel.tsx index bb5db0302d..e6bd0b6ffb 100644 --- a/invokeai/frontend/web/src/features/system/components/ModelManager/AddCheckpointModel.tsx +++ b/invokeai/frontend/web/src/features/system/components/ModelManager/AddCheckpointModel.tsx @@ -10,7 +10,7 @@ import { } from '@chakra-ui/react'; import IAIButton from 'common/components/IAIButton'; -import IAICheckbox from 'common/components/IAICheckbox'; +import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox'; import IAIInput from 'common/components/IAIInput'; import IAINumberInput from 'common/components/IAINumberInput'; import React from 'react'; @@ -74,12 +74,12 @@ export default function AddCheckpointModel() { return ( - setAddmanually(!addManually)} /> - setAddmanually(!addManually)} diff --git a/invokeai/frontend/web/src/features/system/components/ModelManager/MergeModels.tsx b/invokeai/frontend/web/src/features/system/components/ModelManager/MergeModels.tsx index 6ba148cac4..219d49d4ee 100644 --- a/invokeai/frontend/web/src/features/system/components/ModelManager/MergeModels.tsx +++ b/invokeai/frontend/web/src/features/system/components/ModelManager/MergeModels.tsx @@ -24,7 +24,7 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import * as InvokeAI from 'app/types/invokeai'; import IAISlider from 'common/components/IAISlider'; -import IAICheckbox from 'common/components/IAICheckbox'; +import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox'; export default function MergeModels() { const dispatch = useAppDispatch(); @@ -286,7 +286,7 @@ export default function MergeModels() { )} - setModelMergeForce(e.target.checked)} diff --git a/invokeai/frontend/web/src/features/system/components/ModelManager/SearchModels.tsx b/invokeai/frontend/web/src/features/system/components/ModelManager/SearchModels.tsx index 3a99997ac8..3381cb85d3 100644 --- a/invokeai/frontend/web/src/features/system/components/ModelManager/SearchModels.tsx +++ b/invokeai/frontend/web/src/features/system/components/ModelManager/SearchModels.tsx @@ -1,5 +1,5 @@ import IAIButton from 'common/components/IAIButton'; -import IAICheckbox from 'common/components/IAICheckbox'; +import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox'; import IAIIconButton from 'common/components/IAIIconButton'; import React from 'react'; @@ -81,13 +81,13 @@ function SearchModelEntry({ borderRadius={4} > - {model.name}} isChecked={modelsToAdd.includes(model.name)} isDisabled={existingModels.includes(model.location)} onChange={foundModelsChangeHandler} - > + > {existingModels.includes(model.location) && ( {t('modelManager.modelExists')} )} @@ -324,7 +324,7 @@ export default function SearchModels() { > {t('modelManager.deselectAll')} - diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx index 3b3daeaa4c..dd2fd00d22 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx @@ -8,6 +8,7 @@ import ParamNoiseCollapse from 'features/parameters/components/Parameters/Noise/ import ParamSymmetryCollapse from 'features/parameters/components/Parameters/Symmetry/ParamSymmetryCollapse'; import ParamSeamlessCollapse from 'features/parameters/components/Parameters/Seamless/ParamSeamlessCollapse'; import ImageToImageTabCoreParameters from './ImageToImageTabCoreParameters'; +import ParamControlNetCollapse from 'features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse'; const ImageToImageTabParameters = () => { return ( @@ -17,6 +18,7 @@ const ImageToImageTabParameters = () => { + diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasDarkenOutsideSelection.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasDarkenOutsideSelection.tsx index 042749e792..53e36f62b6 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasDarkenOutsideSelection.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasDarkenOutsideSelection.tsx @@ -1,6 +1,6 @@ import { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAICheckbox from 'common/components/IAICheckbox'; +import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox'; import { setShouldDarkenOutsideBoundingBox } from 'features/canvas/store/canvasSlice'; import { useTranslation } from 'react-i18next'; @@ -14,7 +14,7 @@ export default function UnifiedCanvasDarkenOutsideSelection() { const { t } = useTranslation(); return ( - diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasEnableMask.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasEnableMask.tsx index 24f3f45a25..ceb58cb5ca 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasEnableMask.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasEnableMask.tsx @@ -1,6 +1,6 @@ import { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAICheckbox from 'common/components/IAICheckbox'; +import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox'; import { setIsMaskEnabled } from 'features/canvas/store/canvasSlice'; import { useTranslation } from 'react-i18next'; @@ -16,7 +16,7 @@ export default function UnifiedCanvasEnableMask() { dispatch(setIsMaskEnabled(!isMaskEnabled)); return ( - diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasPreserveMask.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasPreserveMask.tsx index 9b4b20e936..fd3396533c 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasPreserveMask.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasPreserveMask.tsx @@ -1,6 +1,6 @@ import { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAICheckbox from 'common/components/IAICheckbox'; +import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox'; import { setShouldPreserveMaskedArea } from 'features/canvas/store/canvasSlice'; import { useTranslation } from 'react-i18next'; @@ -13,7 +13,7 @@ export default function UnifiedCanvasPreserveMask() { ); return ( - dispatch(setShouldPreserveMaskedArea(e.target.checked))} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasSettings.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasSettings.tsx index bfaa7cdae8..a173211258 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasSettings.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasSettings.tsx @@ -1,7 +1,7 @@ import { Flex } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAICheckbox from 'common/components/IAICheckbox'; +import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox'; import IAIIconButton from 'common/components/IAIIconButton'; import IAIPopover from 'common/components/IAIPopover'; import { canvasSelector } from 'features/canvas/store/canvasSelectors'; @@ -73,33 +73,33 @@ const UnifiedCanvasSettings = () => { } > - dispatch(setShouldShowIntermediates(e.target.checked)) } /> - dispatch(setShouldAutoSave(e.target.checked))} /> - dispatch(setShouldCropToBoundingBoxOnSave(e.target.checked)) } /> - dispatch(setShouldShowCanvasDebugInfo(e.target.checked)) } /> - dispatch(setShouldAntialias(e.target.checked))} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasShowGrid.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasShowGrid.tsx index e3d8a518ef..e17f74ce41 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasShowGrid.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasShowGrid.tsx @@ -1,6 +1,6 @@ import { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAICheckbox from 'common/components/IAICheckbox'; +import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox'; import { setShouldShowGrid } from 'features/canvas/store/canvasSlice'; import { useTranslation } from 'react-i18next'; @@ -13,7 +13,7 @@ export default function UnifiedCanvasShowGrid() { const { t } = useTranslation(); return ( - dispatch(setShouldShowGrid(e.target.checked))} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasSnapToGrid.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasSnapToGrid.tsx index c334bd213b..69e9a4e78b 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasSnapToGrid.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasSnapToGrid.tsx @@ -1,6 +1,6 @@ import { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAICheckbox from 'common/components/IAICheckbox'; +import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox'; import { setShouldSnapToGrid } from 'features/canvas/store/canvasSlice'; import { ChangeEvent } from 'react'; import { useTranslation } from 'react-i18next'; @@ -17,7 +17,7 @@ export default function UnifiedCanvasSnapToGrid() { dispatch(setShouldSnapToGrid(e.target.checked)); return ( - { + return ( + isObject(obj) && + 'image_name' in obj && + isString(obj?.image_name) && + 'thumbnail_url' in obj && + isString(obj?.thumbnail_url) && + 'image_url' in obj && + isString(obj?.image_url) && + 'image_origin' in obj && + isString(obj?.image_origin) && + 'created_at' in obj && + isString(obj?.created_at) + ); +}; + export const isImageOutput = ( output: GraphExecutionState['results'][string] ): output is ImageOutput => output.type === 'image_output'; diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock index 356f7466fe..e3b2978457 100644 --- a/invokeai/frontend/web/yarn.lock +++ b/invokeai/frontend/web/yarn.lock @@ -937,6 +937,29 @@ gonzales-pe "^4.3.0" node-source-walk "^5.0.1" +"@dnd-kit/accessibility@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz#3ccbefdfca595b0a23a5dc57d3de96bc6935641c" + integrity sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg== + dependencies: + tslib "^2.0.0" + +"@dnd-kit/core@^6.0.8": + version "6.0.8" + resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.0.8.tgz#040ae13fea9787ee078e5f0361f3b49b07f3f005" + integrity sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA== + dependencies: + "@dnd-kit/accessibility" "^3.0.0" + "@dnd-kit/utilities" "^3.2.1" + tslib "^2.0.0" + +"@dnd-kit/utilities@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.1.tgz#53f9e2016fd2506ec49e404c289392cfff30332a" + integrity sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA== + dependencies: + tslib "^2.0.0" + "@emotion/babel-plugin@^11.10.8": version "11.10.8" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.8.tgz#bae325c902937665d00684038fd5294223ef9e1d" From b1e1e3efc77a1397221142d6a988c244b92b39a1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Jun 2023 18:37:07 +1000 Subject: [PATCH 08/47] fix(ui): fix IAISelectableImage fallback --- .../parameters/IAISelectableImage.tsx | 69 +++++++++++++------ .../components/ImageFallbackSpinner.tsx | 1 + 2 files changed, 49 insertions(+), 21 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx index 62cd4603e5..25277269ed 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx @@ -4,13 +4,13 @@ import { Icon, IconButtonProps, Image, + Spinner, Text, } from '@chakra-ui/react'; import { useDroppable } from '@dnd-kit/core'; import IAIIconButton from 'common/components/IAIIconButton'; import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; import { useGetUrl } from 'common/util/getUrl'; -import ImageFallbackSpinner from 'features/gallery/components/ImageFallbackSpinner'; import { AnimatePresence, motion } from 'framer-motion'; import { SyntheticEvent } from 'react'; import { memo, useRef } from 'react'; @@ -18,6 +18,8 @@ import { FaImage, FaUndo } from 'react-icons/fa'; import { ImageDTO } from 'services/api'; import { v4 as uuidv4 } from 'uuid'; +const PLACEHOLDER_MIN_HEIGHT = 48; + type IAISelectableImageProps = { image: ImageDTO | null | undefined; onChange: (image: ImageDTO) => void; @@ -49,17 +51,42 @@ const IAISelectableImage = (props: IAISelectableImageProps) => { ref={setNodeRef} > {image && ( - + } + fallback={} onError={onError} sx={{ borderRadius: 'base', }} /> + {onReset && ( + + } + onClick={onReset} + /> + + )} {active && } @@ -69,7 +96,7 @@ const IAISelectableImage = (props: IAISelectableImageProps) => { <> { )} - {image && onReset && ( - - } - onClick={onReset} - /> - - )} ); }; @@ -198,3 +208,20 @@ const DropOverlay = (props: DropOverlayProps) => { ); }; + +const ImageFallback = () => ( + + + +); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageFallbackSpinner.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageFallbackSpinner.tsx index 394ff9db15..3d4a0d6911 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageFallbackSpinner.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageFallbackSpinner.tsx @@ -14,6 +14,7 @@ const ImageFallbackSpinner = (props: ImageFallbackSpinnerProps) => { justifyContent: 'center', position: 'absolute', color: 'base.400', + minH: 40, }} > From fa4d88e16375f579299f6f15a0e61c183bb4c2b9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Jun 2023 19:24:26 +1000 Subject: [PATCH 09/47] feat(ui): improve drag and drop ux --- .../components/ImageDnd/ImageDndContext.tsx | 22 +++++++++- .../components/ImageDnd/OverlayDragImage.tsx | 4 +- .../common/components/InitialImageButtons.tsx | 42 ------------------- .../parameters/IAISelectableImage.tsx | 9 ++-- .../components/CurrentImageDisplay.tsx | 12 +++--- .../components/CurrentImagePreview.tsx | 32 +++++++------- .../gallery/components/HoverableImage.tsx | 13 +----- .../fields/ImageInputFieldComponent.tsx | 17 +++++++- .../ImageToImage/InitialImageDisplay.tsx | 1 - .../ImageToImage/InitialImagePreview.tsx | 37 +--------------- 10 files changed, 72 insertions(+), 117 deletions(-) delete mode 100644 invokeai/frontend/web/src/common/components/InitialImageButtons.tsx diff --git a/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx b/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx index 9e8495aa63..6c76731d4c 100644 --- a/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx +++ b/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx @@ -3,6 +3,11 @@ import { DragEndEvent, DragOverlay, DragStartEvent, + KeyboardSensor, + MouseSensor, + TouchSensor, + useSensor, + useSensors, } from '@dnd-kit/core'; import { PropsWithChildren, memo, useCallback, useState } from 'react'; import OverlayDragImage from './OverlayDragImage'; @@ -32,8 +37,23 @@ const ImageDndContext = (props: ImageDndContextProps) => { [draggedImage] ); + const mouseSensor = useSensor(MouseSensor, { + activationConstraint: { distance: 15 }, + }); + + const touchSensor = useSensor(TouchSensor, { + activationConstraint: { distance: 15 }, + }); + const keyboardSensor = useSensor(KeyboardSensor); + + const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor); + return ( - + {props.children} {draggedImage && } diff --git a/invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx b/invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx index 59fe6a7971..25a5fe2449 100644 --- a/invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx +++ b/invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx @@ -10,8 +10,8 @@ const OverlayDragImage = (props: OverlayDragImageProps) => { return ( { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const { openUploader } = useImageUploader(); - - const handleResetInitialImage = useCallback(() => { - dispatch(clearInitialImage()); - }, [dispatch]); - - return ( - - - {t('parameters.initialImage')} - - - - } - aria-label={t('accessibility.reset')} - onClick={handleResetInitialImage} - /> - } - onClick={openUploader} - aria-label={t('common.upload')} - /> - - - ); -}; - -export default InitialImageButtons; diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx index 25277269ed..635c192db1 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx @@ -14,7 +14,7 @@ import { useGetUrl } from 'common/util/getUrl'; import { AnimatePresence, motion } from 'framer-motion'; import { SyntheticEvent } from 'react'; import { memo, useRef } from 'react'; -import { FaImage, FaUndo } from 'react-icons/fa'; +import { FaImage, FaTimes } from 'react-icons/fa'; import { ImageDTO } from 'services/api'; import { v4 as uuidv4 } from 'uuid'; @@ -53,9 +53,8 @@ const IAISelectableImage = (props: IAISelectableImageProps) => { {image && ( { } + icon={} onClick={onReset} /> @@ -184,7 +183,7 @@ const DropOverlay = (props: DropOverlayProps) => { transitionDuration: '0.15s', }} > - + Drop Image diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx index 5810c599c1..621ec8864b 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx @@ -1,4 +1,4 @@ -import { Flex, Icon } from '@chakra-ui/react'; +import { Box, Flex, Icon } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { systemSelector } from 'features/system/store/systemSelectors'; @@ -55,10 +55,7 @@ const CurrentImageDisplay = () => { }} > {hasAnImageToDisplay ? ( - <> - - - + ) : ( { /> )} + {hasAnImageToDisplay && ( + + + + )} ); }; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx index 280d859b87..f8194f5ad4 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx @@ -15,6 +15,7 @@ import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; import { configSelector } from '../../system/store/configSelectors'; import { useAppToaster } from 'app/components/Toaster'; import { imageSelected } from '../store/gallerySlice'; +import { useDraggable } from '@dnd-kit/core'; export const imagesSelector = createSelector( [uiSelector, gallerySelector, systemSelector], @@ -46,7 +47,6 @@ const CurrentImagePreview = () => { const { shouldShowImageDetails, image, - shouldHidePreview, progressImage, shouldShowProgressInViewer, shouldAntialiasProgressImage, @@ -56,16 +56,12 @@ const CurrentImagePreview = () => { const toaster = useAppToaster(); const dispatch = useAppDispatch(); - const handleDragStart = useCallback( - (e: DragEvent) => { - if (!image) { - return; - } - e.dataTransfer.setData('invokeai/imageName', image.image_name); - e.dataTransfer.effectAllowed = 'move'; + const { attributes, listeners, setNodeRef } = useDraggable({ + id: `currentImage_${image?.image_name}`, + data: { + image, }, - [image] - ); + }); const handleError = useCallback(() => { dispatch(imageSelected()); @@ -105,24 +101,32 @@ const CurrentImagePreview = () => { /> ) : ( image && ( - <> + } - onDragStart={handleDragStart} sx={{ objectFit: 'contain', maxWidth: '100%', maxHeight: '100%', height: 'auto', - position: 'absolute', borderRadius: 'base', + touchAction: 'none', }} onError={handleError} /> - + ) )} {shouldShowImageDetails && image && 'metadata' in image && ( diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx index c1fe2569e3..4dad27d4e8 100644 --- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx @@ -40,7 +40,6 @@ import { import { useAppToaster } from 'app/components/Toaster'; import { ImageDTO } from 'services/api'; import { useDraggable } from '@dnd-kit/core'; -import { CSS } from '@dnd-kit/utilities'; export const selector = createSelector( [gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector], @@ -120,7 +119,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { useRecallParameters(); const { attributes, listeners, setNodeRef } = useDraggable({ - id: image_name, + id: `galleryImage_${image_name}`, data: { image, }, @@ -153,14 +152,6 @@ const HoverableImage = memo((props: HoverableImageProps) => { dispatch(imageSelected(image)); }, [image, dispatch]); - const handleDragStart = useCallback( - (e: DragEvent) => { - e.dataTransfer.setData('invokeai/imageName', image.image_name); - e.dataTransfer.effectAllowed = 'move'; - }, - [image] - ); - // Recall parameters handlers const handleRecallPrompt = useCallback(() => { recallBothPrompts( @@ -225,7 +216,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { ref={setNodeRef} {...listeners} {...attributes} - sx={{ w: 'full', h: 'full' }} + sx={{ w: 'full', h: 'full', touchAction: 'none' }} > menuProps={{ size: 'sm', isLazy: true }} diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx index 1232ff28e1..9889ade2f3 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx @@ -32,6 +32,16 @@ const ImageInputFieldComponent = ( [dispatch, field.name, nodeId] ); + const handleReset = useCallback(() => { + dispatch( + fieldValueChanged({ + nodeId, + fieldName: field.name, + value: undefined, + }) + ); + }, [dispatch, field.name, nodeId]); + return ( - + ); }; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx index 1746c9b592..64974f0d35 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx @@ -1,6 +1,5 @@ import { Flex } from '@chakra-ui/react'; import InitialImagePreview from './InitialImagePreview'; -import InitialImageButtons from 'common/components/InitialImageButtons'; const InitialImageDisplay = () => { return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx index 8cd7b99dc5..2a0ed4ab5d 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx @@ -1,4 +1,4 @@ -import { Flex, Icon, Image } from '@chakra-ui/react'; +import { Flex } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useGetUrl } from 'common/util/getUrl'; @@ -6,14 +6,10 @@ import { clearInitialImage, initialImageChanged, } from 'features/parameters/store/generationSlice'; -import { DragEvent, useCallback } from 'react'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; import { generationSelector } from 'features/parameters/store/generationSelectors'; -import { initialImageSelected } from 'features/parameters/store/actions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import ImageFallbackSpinner from 'features/gallery/components/ImageFallbackSpinner'; -import { FaImage } from 'react-icons/fa'; import { configSelector } from '../../../../system/store/configSelectors'; import { useAppToaster } from 'app/components/Toaster'; import IAISelectableImage from 'features/controlNet/components/parameters/IAISelectableImage'; @@ -76,41 +72,12 @@ const InitialImagePreview = () => { alignItems: 'center', justifyContent: 'center', }} - // onDrop={handleDrop} > - {/* {initialImage?.image_url && ( - <> - } - onError={handleError} - sx={{ - objectFit: 'contain', - maxWidth: '100%', - maxHeight: '100%', - height: 'auto', - position: 'absolute', - borderRadius: 'base', - }} - /> - - - )} - {!initialImage?.image_url && ( - - )} */} ); }; From 94c953deab5cc4905727248160c41bec461f0cb1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Jun 2023 20:47:22 +1000 Subject: [PATCH 10/47] feat(ui): get processed images back into controlnet ui --- .../listeners/controlNetImageProcessed.ts | 32 +++++---- .../controlNet/components/ControlNet.tsx | 37 ++++------ .../ControlNetProcessorCollapse.tsx | 67 +++++++++++++++++++ .../ParamControlNetIsPreprocessed.tsx | 36 ++++++++++ .../components/processors/CannyProcessor.tsx | 20 +++++- .../ControlNetResetProcessedImageButton.tsx | 20 ++++++ .../controlNet/store/controlNetSlice.ts | 31 +++++---- 7 files changed, 191 insertions(+), 52 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorCollapse.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsPreprocessed.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetResetProcessedImageButton.tsx diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts index 6b04485581..901cb99bef 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts @@ -1,15 +1,13 @@ import { startAppListening } from '..'; -import { imageMetadataReceived, imageUploaded } from 'services/thunks/image'; -import { addToast } from 'features/system/store/systemSlice'; +import { imageMetadataReceived } from 'services/thunks/image'; import { log } from 'app/logging/useLogger'; import { controlNetImageProcessed } from 'features/controlNet/store/actions'; import { Graph } from 'services/api'; import { sessionCreated } from 'services/thunks/session'; import { sessionReadyToInvoke } from 'features/system/store/actions'; -import { appSocketInvocationComplete } from 'services/events/actions'; +import { socketInvocationComplete } from 'services/events/actions'; import { isImageOutput } from 'services/types/guards'; import { controlNetProcessedImageChanged } from 'features/controlNet/store/controlNetSlice'; -import { selectImagesById } from 'features/gallery/store/imagesSlice'; const moduleLog = log.child({ namespace: 'controlNet' }); @@ -18,27 +16,36 @@ export const addControlNetImageProcessedListener = () => { actionCreator: controlNetImageProcessed, effect: async (action, { dispatch, getState, take }) => { const { controlNetId, processorNode } = action.payload; - const { id } = processorNode; + + // ControlNet one-off procressing graph is just he processor node, no edges const graph: Graph = { - nodes: { [id]: processorNode }, + nodes: { [processorNode.id]: processorNode }, }; + + // Create a session to run the graph & wait til it's ready to invoke const sessionCreatedAction = dispatch(sessionCreated({ graph })); const [sessionCreatedFulfilledAction] = await take( (action): action is ReturnType => sessionCreated.fulfilled.match(action) && action.meta.requestId === sessionCreatedAction.requestId ); + const sessionId = sessionCreatedFulfilledAction.payload.id; + + // Invoke the session & wait til it's complete dispatch(sessionReadyToInvoke()); - const [processorAction] = await take( - (action): action is ReturnType => - appSocketInvocationComplete.match(action) && + const [invocationCompleteAction] = await take( + (action): action is ReturnType => + socketInvocationComplete.match(action) && action.payload.data.graph_execution_state_id === sessionId ); - if (isImageOutput(processorAction.payload.data.result)) { - const { image_name } = processorAction.payload.data.result.image; + // We still have to check the output type + if (isImageOutput(invocationCompleteAction.payload.data.result)) { + const { image_name } = + invocationCompleteAction.payload.data.result.image; + // Wait for the ImageDTO to be received const [imageMetadataReceivedAction] = await take( ( action @@ -46,8 +53,9 @@ export const addControlNetImageProcessedListener = () => { imageMetadataReceived.fulfilled.match(action) && action.payload.image_name === image_name ); - const processedControlImage = imageMetadataReceivedAction.payload; + + // Update the processed image in the store dispatch( controlNetProcessedImageChanged({ controlNetId, diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx index ad4c6e714b..e2cf3d17bd 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback } from 'react'; +import { memo, useCallback, useState } from 'react'; import { ControlNetProcessorNode } from '../store/types'; import { ImageDTO } from 'services/api'; import CannyProcessor from './processors/CannyProcessor'; @@ -27,19 +27,10 @@ import { Flex, HStack, VStack } from '@chakra-ui/react'; import IAISelectableImage from './parameters/IAISelectableImage'; import IAIButton from 'common/components/IAIButton'; import IAIIconButton from 'common/components/IAIIconButton'; - -export type ControlNetProcessorProps = { - controlNetId: string; - image: ImageDTO; - type: ControlNetProcessorNode['type']; -}; - -const renderProcessorComponent = (props: ControlNetProcessorProps) => { - const { type } = props; - if (type === 'canny_image_processor') { - return ; - } -}; +import IAISwitch from 'common/components/IAISwitch'; +import ParamControlNetIsPreprocessed from './parameters/ParamControlNetIsPreprocessed'; +import IAICollapse from 'common/components/IAICollapse'; +import ControlNetProcessorCollapse from './ControlNetProcessorCollapse'; type ControlNetProps = { controlNet: ControlNet; @@ -59,6 +50,10 @@ const ControlNet = (props: ControlNetProps) => { } = props.controlNet; const dispatch = useAppDispatch(); + const [processorType, setProcessorType] = useState< + ControlNetProcessorNode['type'] + >('canny_image_processor'); + const handleControlImageChanged = useCallback( (controlImage: ImageDTO) => { dispatch(controlNetImageChanged({ controlNetId, controlImage })); @@ -74,14 +69,6 @@ const ControlNet = (props: ControlNetProps) => { dispatch(controlNetRemoved(controlNetId)); }, [controlNetId, dispatch]); - const handleIsControlImageProcessedToggled = useCallback(() => { - dispatch( - isControlNetImageProcessedToggled({ - controlNetId, - }) - ); - }, [controlNetId, dispatch]); - const handleProcessedControlImageChanged = useCallback( (processedControlImage: ImageDTO | null) => { dispatch( @@ -98,11 +85,15 @@ const ControlNet = (props: ControlNetProps) => { Remove ControlNet + { + const { type } = props; + if (type === 'canny') { + return ; + } + return null; +}; + +type ControlNetProcessorCollapseProps = { + controlNetId: string; + image: ImageDTO | null; +}; + +const ControlNetProcessorCollapse = ( + props: ControlNetProcessorCollapseProps +) => { + const { image, controlNetId } = props; + const { isOpen, onToggle } = useDisclosure(); + + const [processorType, setProcessorType] = + useState('canny'); + + const handleProcessorTypeChanged = (type: string | null | undefined) => { + setProcessorType(type as ControlNetProcessor); + }; + + return ( + + + {image && ( + + )} + + ); +}; + +export default memo(ControlNetProcessorCollapse); diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsPreprocessed.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsPreprocessed.tsx new file mode 100644 index 0000000000..9e2658964d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsPreprocessed.tsx @@ -0,0 +1,36 @@ +import { useAppDispatch } from 'app/store/storeHooks'; +import IAIFullCheckbox from 'common/components/IAIFullCheckbox'; +import IAISwitch from 'common/components/IAISwitch'; +import { + controlNetToggled, + isControlNetImageProcessedToggled, +} from 'features/controlNet/store/controlNetSlice'; +import { memo, useCallback } from 'react'; + +type ParamControlNetIsEnabledProps = { + controlNetId: string; + isControlImageProcessed: boolean; +}; + +const ParamControlNetIsEnabled = (props: ParamControlNetIsEnabledProps) => { + const { controlNetId, isControlImageProcessed } = props; + const dispatch = useAppDispatch(); + + const handleIsControlImageProcessedToggled = useCallback(() => { + dispatch( + isControlNetImageProcessedToggled({ + controlNetId, + }) + ); + }, [controlNetId, dispatch]); + + return ( + + ); +}; + +export default memo(ParamControlNetIsEnabled); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx index dc735a1ee5..ecbb3912da 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx @@ -4,9 +4,11 @@ import { memo, useCallback, useState } from 'react'; import ControlNetProcessButton from './common/ControlNetProcessButton'; import { useAppDispatch } from 'app/store/storeHooks'; import { controlNetImageProcessed } from 'features/controlNet/store/actions'; -import { ControlNetProcessorProps } from '../ControlNet'; +import ControlNetResetProcessedImageButton from './common/ControlNetResetProcessedImageButton'; +import { ControlNetProcessorProps } from '../ControlNetProcessorCollapse'; +import { controlNetProcessedImageChanged } from 'features/controlNet/store/controlNetSlice'; -export const CANNY_PROCESSOR = 'canny_processor'; +export const CANNY_PROCESSOR = 'canny_image_processor'; const CannyProcessor = (props: ControlNetProcessorProps) => { const { controlNetId, image, type } = props; @@ -36,6 +38,15 @@ const CannyProcessor = (props: ControlNetProcessorProps) => { ); }, [controlNetId, dispatch, highThreshold, image, lowThreshold]); + const handleReset = useCallback(() => { + dispatch( + controlNetProcessedImageChanged({ + controlNetId, + processedControlImage: null, + }) + ); + }, [controlNetId, dispatch]); + return ( { max={255} withInput /> - + + + + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetResetProcessedImageButton.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetResetProcessedImageButton.tsx new file mode 100644 index 0000000000..11a4f66ac1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetResetProcessedImageButton.tsx @@ -0,0 +1,20 @@ +import IAIButton from 'common/components/IAIButton'; +import { memo } from 'react'; +import { FaUnderline, FaUndo } from 'react-icons/fa'; + +type ControlNetResetProcessedImageButtonProps = { + onClick: () => void; +}; + +const ControlNetResetProcessedImageButton = ( + props: ControlNetResetProcessedImageButtonProps +) => { + const { onClick } = props; + return ( + } onClick={onClick}> + Reset Processing + + ); +}; + +export default memo(ControlNetResetProcessedImageButton); diff --git a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts index 5909c85cac..cb4f86ddb2 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts @@ -18,20 +18,22 @@ export const CONTROLNET_MODELS = [ 'lllyasviel/sd-controlnet-mlsd', ]; -// export const CONTROLNET_PROCESSORS = [ -// 'canny', -// 'contentShuffle', -// 'hed', -// 'lineart', -// 'lineartAnime', -// 'mediapipeFace', -// 'midasDepth', -// 'mlsd', -// 'normalBae', -// 'openpose', -// 'pidi', -// 'zoeDepth', -// ] as const; +export const CONTROLNET_PROCESSORS = [ + 'canny', + 'contentShuffle', + 'hed', + 'lineart', + 'lineartAnime', + 'mediapipeFace', + 'midasDepth', + 'mlsd', + 'normalBae', + 'openpose', + 'pidi', + 'zoeDepth', +]; + +export type ControlNetProcessor = (typeof CONTROLNET_PROCESSORS)[number]; export type ControlNetModel = (typeof CONTROLNET_MODELS)[number]; @@ -109,6 +111,7 @@ export const controlNetSlice = createSlice({ ) => { const { controlNetId, controlImage } = action.payload; state.controlNets[controlNetId].controlImage = controlImage; + state.controlNets[controlNetId].processedControlImage = null; }, isControlNetImageProcessedToggled: ( state, From 98493ed9e2ec18881f553baedfa3e5f3912783d1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Jun 2023 21:26:45 +1000 Subject: [PATCH 11/47] feat(ui): reorg parameter panel to make room for controlnet --- .../web/src/common/components/IAISlider.tsx | 2 + .../controlNet/store/controlNetSlice.ts | 6 ++ .../ControlNet/ParamControlNetCollapse.tsx | 18 +++-- .../Parameters/Seed/ParamSeedFull.tsx | 17 +++++ .../Parameters/Seed/ParamSeedRandomize.tsx | 32 +++------ .../Parameters/Seed/ParamSeedShuffle.tsx | 13 ++++ .../ImageToImageTabCoreParameters.tsx | 72 ++++++++++--------- .../ImageToImageTabParameters.tsx | 2 - .../TextToImageTabCoreParameters.tsx | 66 +++++++++-------- .../TextToImage/TextToImageTabParameters.tsx | 2 - .../UnifiedCanvasCoreParameters.tsx | 71 +++++++++--------- .../UnifiedCanvas/UnifiedCanvasParameters.tsx | 1 - 12 files changed, 173 insertions(+), 129 deletions(-) create mode 100644 invokeai/frontend/web/src/features/parameters/components/Parameters/Seed/ParamSeedFull.tsx diff --git a/invokeai/frontend/web/src/common/components/IAISlider.tsx b/invokeai/frontend/web/src/common/components/IAISlider.tsx index 48080e8970..a2a3251f02 100644 --- a/invokeai/frontend/web/src/common/components/IAISlider.tsx +++ b/invokeai/frontend/web/src/common/components/IAISlider.tsx @@ -203,6 +203,7 @@ const IAISlider = (props: IAIFullSliderProps) => { sx={{ insetInlineStart: '0 !important', insetInlineEnd: 'unset !important', + mt: 1.5, }} {...sliderMarkProps} > @@ -213,6 +214,7 @@ const IAISlider = (props: IAIFullSliderProps) => { sx={{ insetInlineStart: 'unset !important', insetInlineEnd: '0 !important', + mt: 1.5, }} {...sliderMarkProps} > diff --git a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts index cb4f86ddb2..dbb45c25f1 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts @@ -62,16 +62,21 @@ export type ControlNet = { export type ControlNetState = { controlNets: Record; + isEnabled: boolean; }; export const initialControlNetState: ControlNetState = { controlNets: {}, + isEnabled: false, }; export const controlNetSlice = createSlice({ name: 'controlNet', initialState: initialControlNetState, reducers: { + isControlNetEnabledToggled: (state) => { + state.isEnabled = !state.isEnabled; + }, controlNetAdded: ( state, action: PayloadAction<{ controlNetId: string }> @@ -166,6 +171,7 @@ export const controlNetSlice = createSlice({ }); export const { + isControlNetEnabledToggled, controlNetAdded, controlNetAddedFromImage, controlNetRemoved, diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx index 2c4088d376..52ea702c4c 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx @@ -10,6 +10,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { controlNetAdded, controlNetSelector, + isControlNetEnabledToggled, } from 'features/controlNet/store/controlNetSlice'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { map } from 'lodash-es'; @@ -18,19 +19,22 @@ import { v4 as uuidv4 } from 'uuid'; const selector = createSelector( controlNetSelector, (controlNet) => { - const { controlNets } = controlNet; + const { controlNets, isEnabled } = controlNet; - return { controlNets }; + return { controlNets, isEnabled }; }, defaultSelectorOptions ); const ParamControlNetCollapse = () => { const { t } = useTranslation(); - const { isOpen, onToggle } = useDisclosure(); - const { controlNets } = useAppSelector(selector); + const { controlNets, isEnabled } = useAppSelector(selector); const dispatch = useAppDispatch(); + const handleClickControlNetToggle = useCallback(() => { + dispatch(isControlNetEnabledToggled()); + }, [dispatch]); + const handleClickedAddControlNet = useCallback(() => { dispatch(controlNetAdded({ controlNetId: uuidv4() })); }, [dispatch]); @@ -38,9 +42,9 @@ const ParamControlNetCollapse = () => { return ( { + return ( + + + + + + ); +}; + +export default memo(ParamSeedFull); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Seed/ParamSeedRandomize.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Seed/ParamSeedRandomize.tsx index 13380f3660..6b1dd46780 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Seed/ParamSeedRandomize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Seed/ParamSeedRandomize.tsx @@ -2,30 +2,10 @@ import { ChangeEvent, memo } from 'react'; import { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAISwitch from 'common/components/IAISwitch'; import { setShouldRandomizeSeed } from 'features/parameters/store/generationSlice'; import { useTranslation } from 'react-i18next'; -import { FormControl, FormLabel, Switch } from '@chakra-ui/react'; - -// export default function RandomizeSeed() { -// const dispatch = useAppDispatch(); -// const { t } = useTranslation(); - -// const shouldRandomizeSeed = useAppSelector( -// (state: RootState) => state.generation.shouldRandomizeSeed -// ); - -// const handleChangeShouldRandomizeSeed = (e: ChangeEvent) => -// dispatch(setShouldRandomizeSeed(e.target.checked)); - -// return ( -// -// ); -// } +import { FormControl, FormLabel, Switch, Tooltip } from '@chakra-ui/react'; +import IAISwitch from 'common/components/IAISwitch'; const ParamSeedRandomize = () => { const dispatch = useAppDispatch(); @@ -38,6 +18,14 @@ const ParamSeedRandomize = () => { const handleChangeShouldRandomizeSeed = (e: ChangeEvent) => dispatch(setShouldRandomizeSeed(e.target.checked)); + return ( + + ); + return ( dispatch(setSeed(randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX))); + return ( + } + /> + ); + return ( { const { shouldUseSliders, shouldFitToWidthHeight } = useAppSelector(selector); + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); return ( - - {shouldUseSliders ? ( - - - - - - - - - - - ) : ( - - + + + {shouldUseSliders ? ( + <> + + + + - - - - - - - - )} - + + + + ) : ( + <> + + + + + + + + + + + + + )} + + + + ); }; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx index dd2fd00d22..55d2c1def2 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx @@ -2,7 +2,6 @@ import { memo } from 'react'; import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons'; import ParamPositiveConditioning from 'features/parameters/components/Parameters/Core/ParamPositiveConditioning'; import ParamNegativeConditioning from 'features/parameters/components/Parameters/Core/ParamNegativeConditioning'; -import ParamSeedCollapse from 'features/parameters/components/Parameters/Seed/ParamSeedCollapse'; import ParamVariationCollapse from 'features/parameters/components/Parameters/Variations/ParamVariationCollapse'; import ParamNoiseCollapse from 'features/parameters/components/Parameters/Noise/ParamNoiseCollapse'; import ParamSymmetryCollapse from 'features/parameters/components/Parameters/Symmetry/ParamSymmetryCollapse'; @@ -17,7 +16,6 @@ const ImageToImageTabParameters = () => { - diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabCoreParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabCoreParameters.tsx index 59512775bc..07297bda31 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabCoreParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabCoreParameters.tsx @@ -3,13 +3,15 @@ import ParamSteps from 'features/parameters/components/Parameters/Core/ParamStep import ParamCFGScale from 'features/parameters/components/Parameters/Core/ParamCFGScale'; import ParamWidth from 'features/parameters/components/Parameters/Core/ParamWidth'; import ParamHeight from 'features/parameters/components/Parameters/Core/ParamHeight'; -import { Flex } from '@chakra-ui/react'; +import { Box, Flex, useDisclosure } from '@chakra-ui/react'; import { useAppSelector } from 'app/store/storeHooks'; import { createSelector } from '@reduxjs/toolkit'; import { uiSelector } from 'features/ui/store/uiSelectors'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { memo } from 'react'; import ParamSchedulerAndModel from 'features/parameters/components/Parameters/Core/ParamSchedulerAndModel'; +import IAICollapse from 'common/components/IAICollapse'; +import ParamSeedFull from 'features/parameters/components/Parameters/Seed/ParamSeedFull'; const selector = createSelector( uiSelector, @@ -23,39 +25,45 @@ const selector = createSelector( const TextToImageTabCoreParameters = () => { const { shouldUseSliders } = useAppSelector(selector); + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); return ( - - {shouldUseSliders ? ( - - - - - - - - - ) : ( - - + + + {shouldUseSliders ? ( + <> + + + + - - - - - - )} - + + + + ) : ( + <> + + + + + + + + + + + + + )} + + ); }; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx index a1084c4b8d..a28fa71407 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx @@ -2,7 +2,6 @@ import ProcessButtons from 'features/parameters/components/ProcessButtons/Proces import { memo } from 'react'; import ParamPositiveConditioning from 'features/parameters/components/Parameters/Core/ParamPositiveConditioning'; import ParamNegativeConditioning from 'features/parameters/components/Parameters/Core/ParamNegativeConditioning'; -import ParamSeedCollapse from 'features/parameters/components/Parameters/Seed/ParamSeedCollapse'; import ParamVariationCollapse from 'features/parameters/components/Parameters/Variations/ParamVariationCollapse'; import ParamNoiseCollapse from 'features/parameters/components/Parameters/Noise/ParamNoiseCollapse'; import ParamSymmetryCollapse from 'features/parameters/components/Parameters/Symmetry/ParamSymmetryCollapse'; @@ -18,7 +17,6 @@ const TextToImageTabParameters = () => { - diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx index 1b6b61f018..42e19eb096 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx @@ -1,5 +1,5 @@ import { memo } from 'react'; -import { Flex } from '@chakra-ui/react'; +import { Box, Flex, useDisclosure } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { uiSelector } from 'features/ui/store/uiSelectors'; import { useAppSelector } from 'app/store/storeHooks'; @@ -8,10 +8,11 @@ import ParamIterations from 'features/parameters/components/Parameters/Core/Para import ParamSteps from 'features/parameters/components/Parameters/Core/ParamSteps'; import ParamCFGScale from 'features/parameters/components/Parameters/Core/ParamCFGScale'; import ImageToImageStrength from 'features/parameters/components/Parameters/ImageToImage/ImageToImageStrength'; -import ImageToImageFit from 'features/parameters/components/Parameters/ImageToImage/ImageToImageFit'; import ParamSchedulerAndModel from 'features/parameters/components/Parameters/Core/ParamSchedulerAndModel'; import ParamBoundingBoxWidth from 'features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxWidth'; import ParamBoundingBoxHeight from 'features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxHeight'; +import ParamSeedFull from 'features/parameters/components/Parameters/Seed/ParamSeedFull'; +import IAICollapse from 'common/components/IAICollapse'; const selector = createSelector( uiSelector, @@ -25,42 +26,46 @@ const selector = createSelector( const UnifiedCanvasCoreParameters = () => { const { shouldUseSliders } = useAppSelector(selector); + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); return ( - - {shouldUseSliders ? ( - - - - - - - - - - - ) : ( - - + + + {shouldUseSliders ? ( + <> + + + + - - - - - - - )} - + + + + ) : ( + <> + + + + + + + + + + + + + )} + + + ); }; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx index c4501ffc44..19ef7fd6fa 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx @@ -16,7 +16,6 @@ const UnifiedCanvasParameters = () => { - From b17f4c165069be2dfee656f4fdd2307e3d77633b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Jun 2023 22:52:25 +1000 Subject: [PATCH 12/47] feat(ui): more tweaking controlnet ui --- .../src/common/components/IAICustomSelect.tsx | 4 +- .../controlNet/components/ControlNet.tsx | 113 ++++++++++++---- .../ControlNetProcessorCollapse.tsx | 127 ++++++++++-------- .../parameters/IAISelectableImage.tsx | 2 + .../parameters/ParamControlNetIsEnabled.tsx | 4 +- .../parameters/ParamControlNetModel.tsx | 1 - .../components/processors/CannyProcessor.tsx | 27 ++-- .../common/ControlNetProcessButton.tsx | 13 -- .../common/ControlNetProcessorButtons.tsx | 44 ++++++ .../ControlNetResetProcessedImageButton.tsx | 20 --- .../features/gallery/store/gallerySlice.ts | 5 +- .../graphBuilders/buildTextToImageGraph.ts | 1 + .../ControlNet/ParamControlNetCollapse.tsx | 57 +++++--- .../frontend/web/src/theme/components/tabs.ts | 1 + 14 files changed, 271 insertions(+), 148 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorButtons.tsx delete mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetResetProcessedImageButton.tsx diff --git a/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx b/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx index 6d6cdbadf5..5047a24c63 100644 --- a/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx +++ b/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx @@ -104,7 +104,8 @@ const IAICustomSelect = (props: IAICustomSelectProps) => { ref={refs.setFloating} sx={{ ...floatingStyles, - width: 'max-content', + width: 'full', + // width: 'max-content', top: 0, left: 0, flexDirection: 'column', @@ -118,6 +119,7 @@ const IAICustomSelect = (props: IAICustomSelectProps) => { px: 0, h: 'fit-content', maxH: 64, + minW: 48, }} > diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx index e2cf3d17bd..f46c1fea4b 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx @@ -3,8 +3,10 @@ import { ControlNetProcessorNode } from '../store/types'; import { ImageDTO } from 'services/api'; import CannyProcessor from './processors/CannyProcessor'; import { + CONTROLNET_PROCESSORS, ControlNet, ControlNetModel, + ControlNetProcessor, controlNetBeginStepPctChanged, controlNetEndStepPctChanged, controlNetImageChanged, @@ -23,7 +25,18 @@ import ParamControlNetModel from './parameters/ParamControlNetModel'; import ParamControlNetWeight from './parameters/ParamControlNetWeight'; import ParamControlNetBeginStepPct from './parameters/ParamControlNetBeginStepPct'; import ParamControlNetEndStepPct from './parameters/ParamControlNetEndStepPct'; -import { Flex, HStack, VStack } from '@chakra-ui/react'; +import { + Box, + Flex, + HStack, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, + VStack, + useDisclosure, +} from '@chakra-ui/react'; import IAISelectableImage from './parameters/IAISelectableImage'; import IAIButton from 'common/components/IAIButton'; import IAIIconButton from 'common/components/IAIIconButton'; @@ -31,6 +44,7 @@ import IAISwitch from 'common/components/IAISwitch'; import ParamControlNetIsPreprocessed from './parameters/ParamControlNetIsPreprocessed'; import IAICollapse from 'common/components/IAICollapse'; import ControlNetProcessorCollapse from './ControlNetProcessorCollapse'; +import IAICustomSelect from 'common/components/IAICustomSelect'; type ControlNetProps = { controlNet: ControlNet; @@ -50,9 +64,14 @@ const ControlNet = (props: ControlNetProps) => { } = props.controlNet; const dispatch = useAppDispatch(); - const [processorType, setProcessorType] = useState< - ControlNetProcessorNode['type'] - >('canny_image_processor'); + const [processorType, setProcessorType] = + useState('canny'); + + const handleProcessorTypeChanged = (type: string | null | undefined) => { + setProcessorType(type as ControlNetProcessor); + }; + + const { isOpen, onToggle } = useDisclosure(); const handleControlImageChanged = useCallback( (controlImage: ImageDTO) => { @@ -82,34 +101,82 @@ const ControlNet = (props: ControlNetProps) => { ); return ( - - Remove ControlNet + - - - - - + + + + Model Config + + + Preprocess + + + + + + + + + + + + + + + Remove ControlNet ); }; export default memo(ControlNet); + +export type ControlNetProcessorProps = { + controlNetId: string; + controlImage: ImageDTO | null; + processedControlImage: ImageDTO | null; + type: ControlNetProcessor; +}; + +const ProcessorComponent = (props: ControlNetProcessorProps) => { + const { type } = props; + if (type === 'canny') { + return ; + } + return null; +}; diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorCollapse.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorCollapse.tsx index 2023402af3..0ce675f4ed 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorCollapse.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorCollapse.tsx @@ -1,67 +1,76 @@ -import { useDisclosure } from '@chakra-ui/react'; -import IAICollapse from 'common/components/IAICollapse'; -import { memo, useState } from 'react'; -import CannyProcessor from './processors/CannyProcessor'; -import { ImageDTO } from 'services/api'; -import IAICustomSelect from 'common/components/IAICustomSelect'; -import { - CONTROLNET_PROCESSORS, - ControlNetProcessor, -} from '../store/controlNetSlice'; +// import { Collapse, Flex, useDisclosure } from '@chakra-ui/react'; +// import { memo, useState } from 'react'; +// import CannyProcessor from './processors/CannyProcessor'; +// import { ImageDTO } from 'services/api'; +// import IAICustomSelect from 'common/components/IAICustomSelect'; +// import { +// CONTROLNET_PROCESSORS, +// ControlNetProcessor, +// } from '../store/controlNetSlice'; +// import IAISwitch from 'common/components/IAISwitch'; -export type ControlNetProcessorProps = { - controlNetId: string; - image: ImageDTO; - type: ControlNetProcessor; -}; +// export type ControlNetProcessorProps = { +// controlNetId: string; +// controlImage: ImageDTO | null; +// processedControlImage: ImageDTO | null; +// type: ControlNetProcessor; +// }; -const ProcessorComponent = (props: ControlNetProcessorProps) => { - const { type } = props; - if (type === 'canny') { - return ; - } - return null; -}; +// const ProcessorComponent = (props: ControlNetProcessorProps) => { +// const { type } = props; +// if (type === 'canny') { +// return ; +// } +// return null; +// }; -type ControlNetProcessorCollapseProps = { - controlNetId: string; - image: ImageDTO | null; -}; +// type ControlNetProcessorCollapseProps = { +// isOpen: boolean; +// controlNetId: string; +// controlImage: ImageDTO | null; +// processedControlImage: ImageDTO | null; +// }; +// const ControlNetProcessorCollapse = ( +// props: ControlNetProcessorCollapseProps +// ) => { +// const { isOpen, controlImage, controlNetId, processedControlImage } = props; -const ControlNetProcessorCollapse = ( - props: ControlNetProcessorCollapseProps -) => { - const { image, controlNetId } = props; - const { isOpen, onToggle } = useDisclosure(); +// const [processorType, setProcessorType] = +// useState('canny'); - const [processorType, setProcessorType] = - useState('canny'); +// const handleProcessorTypeChanged = (type: string | null | undefined) => { +// setProcessorType(type as ControlNetProcessor); +// }; - const handleProcessorTypeChanged = (type: string | null | undefined) => { - setProcessorType(type as ControlNetProcessor); - }; +// return ( +// +// +// {controlImage && ( +// +// )} +// +// ); +// }; - return ( - - - {image && ( - - )} - - ); -}; +// export default memo(ControlNetProcessorCollapse); -export default memo(ControlNetProcessorCollapse); +export default {}; diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx index 635c192db1..1f8fc89c33 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx @@ -64,6 +64,7 @@ const IAISelectableImage = (props: IAISelectableImageProps) => { fallbackStrategy="beforeLoadOrError" fallback={} onError={onError} + draggable={false} sx={{ borderRadius: 'base', }} @@ -80,6 +81,7 @@ const IAISelectableImage = (props: IAISelectableImageProps) => { > } onClick={onReset} diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsEnabled.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsEnabled.tsx index f29b9396b4..f42265cb22 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsEnabled.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsEnabled.tsx @@ -1,5 +1,5 @@ import { useAppDispatch } from 'app/store/storeHooks'; -import IAIFullCheckbox from 'common/components/IAIFullCheckbox'; +import IAISwitch from 'common/components/IAISwitch'; import { controlNetToggled } from 'features/controlNet/store/controlNetSlice'; import { memo, useCallback } from 'react'; @@ -17,7 +17,7 @@ const ParamControlNetIsEnabled = (props: ParamControlNetIsEnabledProps) => { }, [dispatch, controlNetId]); return ( - { return ( { - const { controlNetId, image, type } = props; + const { controlNetId, controlImage, processedControlImage, type } = props; const dispatch = useAppDispatch(); const [lowThreshold, setLowThreshold] = useState(100); const [highThreshold, setHighThreshold] = useState(200); const handleProcess = useCallback(() => { - if (!image) { + if (!controlImage) { return; } @@ -28,15 +27,15 @@ const CannyProcessor = (props: ControlNetProcessorProps) => { id: CANNY_PROCESSOR, type: 'canny_image_processor', image: { - image_name: image.image_name, - image_origin: image.image_origin, + image_name: controlImage.image_name, + image_origin: controlImage.image_origin, }, low_threshold: lowThreshold, high_threshold: highThreshold, }, }) ); - }, [controlNetId, dispatch, highThreshold, image, lowThreshold]); + }, [controlNetId, dispatch, highThreshold, controlImage, lowThreshold]); const handleReset = useCallback(() => { dispatch( @@ -65,10 +64,12 @@ const CannyProcessor = (props: ControlNetProcessorProps) => { max={255} withInput /> - - - - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessButton.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessButton.tsx deleted file mode 100644 index 2fb6d60e55..0000000000 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessButton.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import IAIButton from 'common/components/IAIButton'; -import { memo } from 'react'; - -type ControlNetProcessButtonProps = { - onClick: () => void; -}; - -const ControlNetProcessButton = (props: ControlNetProcessButtonProps) => { - const { onClick } = props; - return Process Control Image; -}; - -export default memo(ControlNetProcessButton); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorButtons.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorButtons.tsx new file mode 100644 index 0000000000..afa94d6ada --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorButtons.tsx @@ -0,0 +1,44 @@ +import { Flex } from '@chakra-ui/react'; +import { memo } from 'react'; +import { FaUndo } from 'react-icons/fa'; +import IAIButton from 'common/components/IAIButton'; + +type ControlNetProcessorButtonsProps = { + handleProcess: () => void; + isProcessDisabled: boolean; + handleReset: () => void; + isResetDisabled: boolean; +}; + +const ControlNetProcessorButtons = (props: ControlNetProcessorButtonsProps) => { + const { handleProcess, isProcessDisabled, handleReset, isResetDisabled } = + props; + return ( + + + Preprocess + + } + onClick={handleReset} + isDisabled={isResetDisabled} + > + Reset Processing + + + ); +}; + +export default memo(ControlNetProcessorButtons); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetResetProcessedImageButton.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetResetProcessedImageButton.tsx deleted file mode 100644 index 11a4f66ac1..0000000000 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetResetProcessedImageButton.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import IAIButton from 'common/components/IAIButton'; -import { memo } from 'react'; -import { FaUnderline, FaUndo } from 'react-icons/fa'; - -type ControlNetResetProcessedImageButtonProps = { - onClick: () => void; -}; - -const ControlNetResetProcessedImageButton = ( - props: ControlNetResetProcessedImageButtonProps -) => { - const { onClick } = props; - return ( - } onClick={onClick}> - Reset Processing - - ); -}; - -export default memo(ControlNetResetProcessedImageButton); diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index ab62646c0f..8e5ecf64fa 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -50,7 +50,10 @@ export const gallerySlice = createSlice({ }, extraReducers: (builder) => { builder.addCase(imageUpserted, (state, action) => { - if (state.shouldAutoSwitchToNewImages) { + if ( + state.shouldAutoSwitchToNewImages && + action.payload.image_category === 'general' + ) { state.selectedImage = action.payload; } }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts index 753ccccff8..65c205f9a4 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts @@ -19,6 +19,7 @@ const NOISE = 'noise'; const RANDOM_INT = 'rand_int'; const RANGE_OF_SIZE = 'range_of_size'; const ITERATE = 'iterate'; +const CONTROL_NET = 'control_net'; /** * Builds the Text to Image tab graph. diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx index 52ea702c4c..a3f91fd432 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx @@ -1,4 +1,12 @@ -import { Flex, useDisclosure } from '@chakra-ui/react'; +import { + Flex, + Spacer, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, +} from '@chakra-ui/react'; import { useTranslation } from 'react-i18next'; import IAICollapse from 'common/components/IAICollapse'; import { memo, useCallback } from 'react'; @@ -13,22 +21,23 @@ import { isControlNetEnabledToggled, } from 'features/controlNet/store/controlNetSlice'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import { map } from 'lodash-es'; +import { map, startCase } from 'lodash-es'; import { v4 as uuidv4 } from 'uuid'; +import { CloseIcon } from '@chakra-ui/icons'; const selector = createSelector( controlNetSelector, (controlNet) => { const { controlNets, isEnabled } = controlNet; - return { controlNets, isEnabled }; + return { controlNetsArray: map(controlNets), isEnabled }; }, defaultSelectorOptions ); const ParamControlNetCollapse = () => { const { t } = useTranslation(); - const { controlNets, isEnabled } = useAppSelector(selector); + const { controlNetsArray, isEnabled } = useAppSelector(selector); const dispatch = useAppDispatch(); const handleClickControlNetToggle = useCallback(() => { @@ -46,17 +55,35 @@ const ParamControlNetCollapse = () => { onToggle={handleClickControlNetToggle} withSwitch > - - } - /> - - {map(controlNets, (c) => ( - - ))} + + + {controlNetsArray.map((c, i) => ( + + {i + 1} + + ))} + } + /> + + + {controlNetsArray.map((c) => ( + + + + ))} + + ); }; diff --git a/invokeai/frontend/web/src/theme/components/tabs.ts b/invokeai/frontend/web/src/theme/components/tabs.ts index 5eb1a36013..daf6e18cab 100644 --- a/invokeai/frontend/web/src/theme/components/tabs.ts +++ b/invokeai/frontend/web/src/theme/components/tabs.ts @@ -26,6 +26,7 @@ const invokeAITablist = defineStyle((_props) => ({ padding: 2, borderRadius: 'base', _selected: { + borderBottomColor: 'base.800', bg: 'accent.700', color: 'accent.100', _hover: { From 6896e69e95f21332b0f2bf81da49627e70cbe1f0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 2 Jun 2023 00:45:07 +1000 Subject: [PATCH 13/47] fix(ui): fix multiple controlnets --- .../controlNet/components/ControlNet.tsx | 37 ++++---- .../controlNet/store/controlNetSlice.ts | 15 +++- .../graphBuilders/buildTextToImageGraph.ts | 90 ++++++++++++++++++- 3 files changed, 118 insertions(+), 24 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx index f46c1fea4b..0626b08fd9 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx @@ -12,6 +12,7 @@ import { controlNetImageChanged, controlNetModelChanged, controlNetProcessedImageChanged, + controlNetProcessorChanged, controlNetRemoved, controlNetToggled, controlNetWeightChanged, @@ -61,17 +62,21 @@ const ControlNet = (props: ControlNetProps) => { controlImage, isControlImageProcessed, processedControlImage, + processor, } = props.controlNet; const dispatch = useAppDispatch(); - const [processorType, setProcessorType] = - useState('canny'); - - const handleProcessorTypeChanged = (type: string | null | undefined) => { - setProcessorType(type as ControlNetProcessor); - }; - - const { isOpen, onToggle } = useDisclosure(); + const handleProcessorTypeChanged = useCallback( + (processor: string | null | undefined) => { + dispatch( + controlNetProcessorChanged({ + controlNetId, + processor: processor as ControlNetProcessor, + }) + ); + }, + [controlNetId, dispatch] + ); const handleControlImageChanged = useCallback( (controlImage: ImageDTO) => { @@ -88,18 +93,6 @@ const ControlNet = (props: ControlNetProps) => { dispatch(controlNetRemoved(controlNetId)); }, [controlNetId, dispatch]); - const handleProcessedControlImageChanged = useCallback( - (processedControlImage: ImageDTO | null) => { - dispatch( - controlNetProcessedImageChanged({ - controlNetId, - processedControlImage, - }) - ); - }, - [controlNetId, dispatch] - ); - return ( { diff --git a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts index dbb45c25f1..a87b591bad 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts @@ -46,18 +46,20 @@ export const initialControlNet: Omit = { controlImage: null, isControlImageProcessed: false, processedControlImage: null, + processor: 'canny', }; export type ControlNet = { controlNetId: string; isEnabled: boolean; - model: string; + model: ControlNetModel; weight: number; beginStepPct: number; endStepPct: number; controlImage: ImageDTO | null; isControlImageProcessed: boolean; processedControlImage: ImageDTO | null; + processor: ControlNetProcessor; }; export type ControlNetState = { @@ -167,6 +169,16 @@ export const controlNetSlice = createSlice({ const { controlNetId, endStepPct } = action.payload; state.controlNets[controlNetId].endStepPct = endStepPct; }, + controlNetProcessorChanged: ( + state, + action: PayloadAction<{ + controlNetId: string; + processor: ControlNetProcessor; + }> + ) => { + const { controlNetId, processor } = action.payload; + state.controlNets[controlNetId].processor = processor; + }, }, }); @@ -183,6 +195,7 @@ export const { controlNetWeightChanged, controlNetBeginStepPctChanged, controlNetEndStepPctChanged, + controlNetProcessorChanged, } = controlNetSlice.actions; export default controlNetSlice.reducer; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts index 65c205f9a4..d52310abdd 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts @@ -1,6 +1,8 @@ import { RootState } from 'app/store/store'; import { + CollectInvocation, CompelInvocation, + ControlNetInvocation, Graph, IterateInvocation, LatentsToImageInvocation, @@ -10,6 +12,9 @@ import { TextToLatentsInvocation, } from 'services/api'; import { NonNullableGraph } from 'features/nodes/types/types'; +import { forEach, map, size } from 'lodash-es'; +import { ControlNetProcessorNode } from 'features/controlNet/store/types'; +import { ControlNetModel } from 'features/controlNet/store/controlNetSlice'; const POSITIVE_CONDITIONING = 'positive_conditioning'; const NEGATIVE_CONDITIONING = 'negative_conditioning'; @@ -19,7 +24,7 @@ const NOISE = 'noise'; const RANDOM_INT = 'rand_int'; const RANGE_OF_SIZE = 'range_of_size'; const ITERATE = 'iterate'; -const CONTROL_NET = 'control_net'; +const CONTROL_NET_COLLECT = 'control_net_collect'; /** * Builds the Text to Image tab graph. @@ -39,6 +44,8 @@ export const buildTextToImageGraph = (state: RootState): Graph => { shouldRandomizeSeed, } = state.generation; + const { isEnabled: isControlNetEnabled, controlNets } = state.controlNet; + const graph: NonNullableGraph = { nodes: {}, edges: [], @@ -309,5 +316,86 @@ export const buildTextToImageGraph = (state: RootState): Graph => { }, }); } + + // Add ControlNet + if (isControlNetEnabled) { + if (size(controlNets) > 1) { + const controlNetIterateNode: CollectInvocation = { + id: CONTROL_NET_COLLECT, + type: 'collect', + }; + graph.nodes[controlNetIterateNode.id] = controlNetIterateNode; + graph.edges.push({ + source: { node_id: controlNetIterateNode.id, field: 'collection' }, + destination: { + node_id: TEXT_TO_LATENTS, + field: 'control', + }, + }); + } + + forEach(controlNets, (controlNet, index) => { + const { + controlNetId, + isEnabled, + isControlImageProcessed, + controlImage, + processedControlImage, + beginStepPct, + endStepPct, + model, + processor, + weight, + } = controlNet; + + const controlNetNode: ControlNetInvocation = { + id: `control_net_${controlNetId}`, + type: 'controlnet', + begin_step_percent: beginStepPct, + end_step_percent: endStepPct, + control_model: model as ControlNetInvocation['control_model'], + control_weight: weight, + }; + + if (processedControlImage) { + // We've already processed the image in the app, so we can just use the processed image + const { image_name, image_origin } = processedControlImage; + controlNetNode.image = { + image_name, + image_origin, + }; + } else if (controlImage) { + // The control image is preprocessed + const { image_name, image_origin } = controlImage; + controlNetNode.image = { + image_name, + image_origin, + }; + } else { + // The control image is not processed, so we need to add a preprocess node + // TODO: Add preprocess node + } + graph.nodes[controlNetNode.id] = controlNetNode; + + if (size(controlNets) > 1) { + graph.edges.push({ + source: { node_id: controlNetNode.id, field: 'control' }, + destination: { + node_id: CONTROL_NET_COLLECT, + field: 'item', + }, + }); + } else { + graph.edges.push({ + source: { node_id: controlNetNode.id, field: 'control' }, + destination: { + node_id: TEXT_TO_LATENTS, + field: 'control', + }, + }); + } + }); + } + return graph; }; From 6bbb5f061ae3ffdf4b30e4725e9262131d12c247 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 2 Jun 2023 17:25:16 +1000 Subject: [PATCH 14/47] feat(nodes): update controlnet names/descriptions --- .../invocations/controlnet_image_processors.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index be0381c58e..15aecde851 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -214,12 +214,12 @@ class CannyImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfi return processed_image -class HedImageprocessorInvocation(ImageProcessorInvocation, PILInvocationConfig): +class HedImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig): """Applies HED edge detection to image""" # fmt: off type: Literal["hed_image_processor"] = "hed_image_processor" # Inputs - detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for edge detection") + detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for detection") image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") # safe not supported in controlnet_aux v0.0.3 # safe: bool = Field(default=False, description="whether to use safe mode") @@ -243,7 +243,7 @@ class LineartImageProcessorInvocation(ImageProcessorInvocation, PILInvocationCon # fmt: off type: Literal["lineart_image_processor"] = "lineart_image_processor" # Inputs - detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for edge detection") + detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for detection") image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") coarse: bool = Field(default=False, description="Whether to use coarse mode") # fmt: on @@ -262,7 +262,7 @@ class LineartAnimeImageProcessorInvocation(ImageProcessorInvocation, PILInvocati # fmt: off type: Literal["lineart_anime_image_processor"] = "lineart_anime_image_processor" # Inputs - detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for edge detection") + detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for detection") image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") # fmt: on @@ -281,7 +281,7 @@ class OpenposeImageProcessorInvocation(ImageProcessorInvocation, PILInvocationCo type: Literal["openpose_image_processor"] = "openpose_image_processor" # Inputs hand_and_face: bool = Field(default=False, description="Whether to use hands and face mode") - detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for edge detection") + detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for detection") image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") # fmt: on @@ -322,7 +322,7 @@ class NormalbaeImageProcessorInvocation(ImageProcessorInvocation, PILInvocationC # fmt: off type: Literal["normalbae_image_processor"] = "normalbae_image_processor" # Inputs - detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for edge detection") + detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for detection") image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") # fmt: on @@ -339,7 +339,7 @@ class MlsdImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig # fmt: off type: Literal["mlsd_image_processor"] = "mlsd_image_processor" # Inputs - detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for edge detection") + detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for detection") image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") thr_v: float = Field(default=0.1, ge=0, description="MLSD parameter `thr_v`") thr_d: float = Field(default=0.1, ge=0, description="MLSD parameter `thr_d`") @@ -360,7 +360,7 @@ class PidiImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig # fmt: off type: Literal["pidi_image_processor"] = "pidi_image_processor" # Inputs - detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for edge detection") + detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for detection") image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") safe: bool = Field(default=False, description="Whether to use safe mode") scribble: bool = Field(default=False, description="Whether to use scribble mode") @@ -381,7 +381,7 @@ class ContentShuffleImageProcessorInvocation(ImageProcessorInvocation, PILInvoca # fmt: off type: Literal["content_shuffle_image_processor"] = "content_shuffle_image_processor" # Inputs - detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for edge detection") + detect_resolution: int = Field(default=512, ge=0, description="The pixel resolution for detection") image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") h: Union[int, None] = Field(default=512, ge=0, description="Content shuffle `h` parameter") w: Union[int, None] = Field(default=512, ge=0, description="Content shuffle `w` parameter") From 707ed393001fe8e8450faa2fe99957f64b0bfdca Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 2 Jun 2023 17:25:47 +1000 Subject: [PATCH 15/47] chore(ui): regen api client --- invokeai/frontend/web/src/services/api/index.ts | 2 +- .../api/models/CannyImageProcessorInvocation.ts | 6 +++--- .../models/ContentShuffleImageProcessorInvocation.ts | 12 ++++++------ .../web/src/services/api/models/ControlField.ts | 10 +++++----- .../src/services/api/models/ControlNetInvocation.ts | 10 +++++----- .../web/src/services/api/models/ControlOutput.ts | 2 +- .../frontend/web/src/services/api/models/Graph.ts | 4 ++-- ...rInvocation.ts => HedImageProcessorInvocation.ts} | 10 +++++----- .../services/api/models/ImageProcessorInvocation.ts | 2 +- .../models/LineartAnimeImageProcessorInvocation.ts | 6 +++--- .../api/models/LineartImageProcessorInvocation.ts | 8 ++++---- .../api/models/MediapipeFaceProcessorInvocation.ts | 6 +++--- .../api/models/MidasDepthImageProcessorInvocation.ts | 6 +++--- .../api/models/MlsdImageProcessorInvocation.ts | 10 +++++----- .../api/models/NormalbaeImageProcessorInvocation.ts | 6 +++--- .../api/models/OpenposeImageProcessorInvocation.ts | 8 ++++---- .../api/models/PidiImageProcessorInvocation.ts | 10 +++++----- .../api/models/ZoeDepthImageProcessorInvocation.ts | 2 +- .../web/src/services/api/services/SessionsService.ts | 6 +++--- 19 files changed, 63 insertions(+), 63 deletions(-) rename invokeai/frontend/web/src/services/api/models/{HedImageprocessorInvocation.ts => HedImageProcessorInvocation.ts} (73%) diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index ff083079f9..187752627a 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -32,7 +32,7 @@ export type { Graph } from './models/Graph'; export type { GraphExecutionState } from './models/GraphExecutionState'; export type { GraphInvocation } from './models/GraphInvocation'; export type { GraphInvocationOutput } from './models/GraphInvocationOutput'; -export type { HedImageprocessorInvocation } from './models/HedImageprocessorInvocation'; +export type { HedImageProcessorInvocation } from './models/HedImageProcessorInvocation'; export type { HTTPValidationError } from './models/HTTPValidationError'; export type { ImageBlurInvocation } from './models/ImageBlurInvocation'; export type { ImageCategory } from './models/ImageCategory'; diff --git a/invokeai/frontend/web/src/services/api/models/CannyImageProcessorInvocation.ts b/invokeai/frontend/web/src/services/api/models/CannyImageProcessorInvocation.ts index 3a8b0b21e7..d5203867ac 100644 --- a/invokeai/frontend/web/src/services/api/models/CannyImageProcessorInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/CannyImageProcessorInvocation.ts @@ -18,15 +18,15 @@ export type CannyImageProcessorInvocation = { is_intermediate?: boolean; type?: 'canny_image_processor'; /** - * image to process + * The image to process */ image?: ImageField; /** - * low threshold of Canny pixel gradient + * The low threshold of the Canny pixel gradient (0-255) */ low_threshold?: number; /** - * high threshold of Canny pixel gradient + * The high threshold of the Canny pixel gradient (0-255) */ high_threshold?: number; }; diff --git a/invokeai/frontend/web/src/services/api/models/ContentShuffleImageProcessorInvocation.ts b/invokeai/frontend/web/src/services/api/models/ContentShuffleImageProcessorInvocation.ts index d8bc3fe58e..e3f67ec9be 100644 --- a/invokeai/frontend/web/src/services/api/models/ContentShuffleImageProcessorInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/ContentShuffleImageProcessorInvocation.ts @@ -18,27 +18,27 @@ export type ContentShuffleImageProcessorInvocation = { is_intermediate?: boolean; type?: 'content_shuffle_image_processor'; /** - * image to process + * The image to process */ image?: ImageField; /** - * pixel resolution for edge detection + * The pixel resolution for detection */ detect_resolution?: number; /** - * pixel resolution for output image + * The pixel resolution for the output image */ image_resolution?: number; /** - * content shuffle h parameter + * Content shuffle `h` parameter */ 'h'?: number; /** - * content shuffle w parameter + * Content shuffle `w` parameter */ 'w'?: number; /** - * cont + * Content shuffle `f` parameter */ 'f'?: number; }; diff --git a/invokeai/frontend/web/src/services/api/models/ControlField.ts b/invokeai/frontend/web/src/services/api/models/ControlField.ts index 4f493d4410..a67655c018 100644 --- a/invokeai/frontend/web/src/services/api/models/ControlField.ts +++ b/invokeai/frontend/web/src/services/api/models/ControlField.ts @@ -6,23 +6,23 @@ import type { ImageField } from './ImageField'; export type ControlField = { /** - * processed image + * The control image */ image: ImageField; /** - * control model used + * The ControlNet model to use */ control_model: string; /** - * weight given to controlnet + * The weight given to the ControlNet */ control_weight: number; /** - * % of total steps at which controlnet is first applied + * When the ControlNet is first applied (% of total steps) */ begin_step_percent: number; /** - * % of total steps at which controlnet is last applied + * When the ControlNet is last applied (% of total steps) */ end_step_percent: number; }; diff --git a/invokeai/frontend/web/src/services/api/models/ControlNetInvocation.ts b/invokeai/frontend/web/src/services/api/models/ControlNetInvocation.ts index fad3af911b..92688d6adc 100644 --- a/invokeai/frontend/web/src/services/api/models/ControlNetInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/ControlNetInvocation.ts @@ -18,23 +18,23 @@ export type ControlNetInvocation = { is_intermediate?: boolean; type?: 'controlnet'; /** - * image to process + * The control image */ image?: ImageField; /** - * control model used + * The ControlNet model to use */ control_model?: 'lllyasviel/sd-controlnet-canny' | 'lllyasviel/sd-controlnet-depth' | 'lllyasviel/sd-controlnet-hed' | 'lllyasviel/sd-controlnet-seg' | 'lllyasviel/sd-controlnet-openpose' | 'lllyasviel/sd-controlnet-scribble' | 'lllyasviel/sd-controlnet-normal' | 'lllyasviel/sd-controlnet-mlsd' | 'lllyasviel/control_v11p_sd15_canny' | 'lllyasviel/control_v11p_sd15_openpose' | 'lllyasviel/control_v11p_sd15_seg' | 'lllyasviel/control_v11f1p_sd15_depth' | 'lllyasviel/control_v11p_sd15_normalbae' | 'lllyasviel/control_v11p_sd15_scribble' | 'lllyasviel/control_v11p_sd15_mlsd' | 'lllyasviel/control_v11p_sd15_softedge' | 'lllyasviel/control_v11p_sd15s2_lineart_anime' | 'lllyasviel/control_v11p_sd15_lineart' | 'lllyasviel/control_v11p_sd15_inpaint' | 'lllyasviel/control_v11e_sd15_shuffle' | 'lllyasviel/control_v11e_sd15_ip2p' | 'lllyasviel/control_v11f1e_sd15_tile' | 'thibaud/controlnet-sd21-openpose-diffusers' | 'thibaud/controlnet-sd21-canny-diffusers' | 'thibaud/controlnet-sd21-depth-diffusers' | 'thibaud/controlnet-sd21-scribble-diffusers' | 'thibaud/controlnet-sd21-hed-diffusers' | 'thibaud/controlnet-sd21-zoedepth-diffusers' | 'thibaud/controlnet-sd21-color-diffusers' | 'thibaud/controlnet-sd21-openposev2-diffusers' | 'thibaud/controlnet-sd21-lineart-diffusers' | 'thibaud/controlnet-sd21-normalbae-diffusers' | 'thibaud/controlnet-sd21-ade20k-diffusers' | 'CrucibleAI/ControlNetMediaPipeFace,diffusion_sd15' | 'CrucibleAI/ControlNetMediaPipeFace'; /** - * weight given to controlnet + * The weight given to the ControlNet */ control_weight?: number; /** - * % of total steps at which controlnet is first applied + * When the ControlNet is first applied (% of total steps) */ begin_step_percent?: number; /** - * % of total steps at which controlnet is last applied + * When the ControlNet is last applied (% of total steps) */ end_step_percent?: number; }; diff --git a/invokeai/frontend/web/src/services/api/models/ControlOutput.ts b/invokeai/frontend/web/src/services/api/models/ControlOutput.ts index 43f1b3341c..8c8b76a32f 100644 --- a/invokeai/frontend/web/src/services/api/models/ControlOutput.ts +++ b/invokeai/frontend/web/src/services/api/models/ControlOutput.ts @@ -10,7 +10,7 @@ import type { ControlField } from './ControlField'; export type ControlOutput = { type?: 'control_output'; /** - * The control info dict + * The output control image */ control?: ControlField; }; diff --git a/invokeai/frontend/web/src/services/api/models/Graph.ts b/invokeai/frontend/web/src/services/api/models/Graph.ts index e89e815ab2..2c7efbb423 100644 --- a/invokeai/frontend/web/src/services/api/models/Graph.ts +++ b/invokeai/frontend/web/src/services/api/models/Graph.ts @@ -12,7 +12,7 @@ import type { CvInpaintInvocation } from './CvInpaintInvocation'; import type { DivideInvocation } from './DivideInvocation'; import type { Edge } from './Edge'; import type { GraphInvocation } from './GraphInvocation'; -import type { HedImageprocessorInvocation } from './HedImageprocessorInvocation'; +import type { HedImageProcessorInvocation } from './HedImageProcessorInvocation'; import type { ImageBlurInvocation } from './ImageBlurInvocation'; import type { ImageChannelInvocation } from './ImageChannelInvocation'; import type { ImageConvertInvocation } from './ImageConvertInvocation'; @@ -69,7 +69,7 @@ export type Graph = { /** * The nodes in this graph */ - nodes?: Record; + nodes?: Record; /** * The connections between nodes and their fields in this graph */ diff --git a/invokeai/frontend/web/src/services/api/models/HedImageprocessorInvocation.ts b/invokeai/frontend/web/src/services/api/models/HedImageProcessorInvocation.ts similarity index 73% rename from invokeai/frontend/web/src/services/api/models/HedImageprocessorInvocation.ts rename to invokeai/frontend/web/src/services/api/models/HedImageProcessorInvocation.ts index f975f18968..1132012c5a 100644 --- a/invokeai/frontend/web/src/services/api/models/HedImageprocessorInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/HedImageProcessorInvocation.ts @@ -7,7 +7,7 @@ import type { ImageField } from './ImageField'; /** * Applies HED edge detection to image */ -export type HedImageprocessorInvocation = { +export type HedImageProcessorInvocation = { /** * The id of this node. Must be unique among all nodes. */ @@ -18,19 +18,19 @@ export type HedImageprocessorInvocation = { is_intermediate?: boolean; type?: 'hed_image_processor'; /** - * image to process + * The image to process */ image?: ImageField; /** - * pixel resolution for edge detection + * The pixel resolution for detection */ detect_resolution?: number; /** - * pixel resolution for output image + * The pixel resolution for the output image */ image_resolution?: number; /** - * whether to use scribble mode + * Whether to use scribble mode */ scribble?: boolean; }; diff --git a/invokeai/frontend/web/src/services/api/models/ImageProcessorInvocation.ts b/invokeai/frontend/web/src/services/api/models/ImageProcessorInvocation.ts index f972582e2f..0d995c4e68 100644 --- a/invokeai/frontend/web/src/services/api/models/ImageProcessorInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/ImageProcessorInvocation.ts @@ -18,7 +18,7 @@ export type ImageProcessorInvocation = { is_intermediate?: boolean; type?: 'image_processor'; /** - * image to process + * The image to process */ image?: ImageField; }; diff --git a/invokeai/frontend/web/src/services/api/models/LineartAnimeImageProcessorInvocation.ts b/invokeai/frontend/web/src/services/api/models/LineartAnimeImageProcessorInvocation.ts index 4796d2a049..5d239536d5 100644 --- a/invokeai/frontend/web/src/services/api/models/LineartAnimeImageProcessorInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/LineartAnimeImageProcessorInvocation.ts @@ -18,15 +18,15 @@ export type LineartAnimeImageProcessorInvocation = { is_intermediate?: boolean; type?: 'lineart_anime_image_processor'; /** - * image to process + * The image to process */ image?: ImageField; /** - * pixel resolution for edge detection + * The pixel resolution for detection */ detect_resolution?: number; /** - * pixel resolution for output image + * The pixel resolution for the output image */ image_resolution?: number; }; diff --git a/invokeai/frontend/web/src/services/api/models/LineartImageProcessorInvocation.ts b/invokeai/frontend/web/src/services/api/models/LineartImageProcessorInvocation.ts index 8328849b50..17720e689b 100644 --- a/invokeai/frontend/web/src/services/api/models/LineartImageProcessorInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/LineartImageProcessorInvocation.ts @@ -18,19 +18,19 @@ export type LineartImageProcessorInvocation = { is_intermediate?: boolean; type?: 'lineart_image_processor'; /** - * image to process + * The image to process */ image?: ImageField; /** - * pixel resolution for edge detection + * The pixel resolution for detection */ detect_resolution?: number; /** - * pixel resolution for output image + * The pixel resolution for the output image */ image_resolution?: number; /** - * whether to use coarse mode + * Whether to use coarse mode */ coarse?: boolean; }; diff --git a/invokeai/frontend/web/src/services/api/models/MediapipeFaceProcessorInvocation.ts b/invokeai/frontend/web/src/services/api/models/MediapipeFaceProcessorInvocation.ts index bd223eed7d..aa7b966b4b 100644 --- a/invokeai/frontend/web/src/services/api/models/MediapipeFaceProcessorInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/MediapipeFaceProcessorInvocation.ts @@ -18,15 +18,15 @@ export type MediapipeFaceProcessorInvocation = { is_intermediate?: boolean; type?: 'mediapipe_face_processor'; /** - * image to process + * The image to process */ image?: ImageField; /** - * maximum number of faces to detect + * Maximum number of faces to detect */ max_faces?: number; /** - * minimum confidence for face detection + * Minimum confidence for face detection */ min_confidence?: number; }; diff --git a/invokeai/frontend/web/src/services/api/models/MidasDepthImageProcessorInvocation.ts b/invokeai/frontend/web/src/services/api/models/MidasDepthImageProcessorInvocation.ts index 11023086a2..bd274228db 100644 --- a/invokeai/frontend/web/src/services/api/models/MidasDepthImageProcessorInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/MidasDepthImageProcessorInvocation.ts @@ -18,15 +18,15 @@ export type MidasDepthImageProcessorInvocation = { is_intermediate?: boolean; type?: 'midas_depth_image_processor'; /** - * image to process + * The image to process */ image?: ImageField; /** - * Midas parameter a = amult * PI + * Midas parameter `a_mult` (a = a_mult * PI) */ a_mult?: number; /** - * Midas parameter bg_th + * Midas parameter `bg_th` */ bg_th?: number; }; diff --git a/invokeai/frontend/web/src/services/api/models/MlsdImageProcessorInvocation.ts b/invokeai/frontend/web/src/services/api/models/MlsdImageProcessorInvocation.ts index c2d4a61b9a..0e81c9a4b8 100644 --- a/invokeai/frontend/web/src/services/api/models/MlsdImageProcessorInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/MlsdImageProcessorInvocation.ts @@ -18,23 +18,23 @@ export type MlsdImageProcessorInvocation = { is_intermediate?: boolean; type?: 'mlsd_image_processor'; /** - * image to process + * The image to process */ image?: ImageField; /** - * pixel resolution for edge detection + * The pixel resolution for detection */ detect_resolution?: number; /** - * pixel resolution for output image + * The pixel resolution for the output image */ image_resolution?: number; /** - * MLSD parameter thr_v + * MLSD parameter `thr_v` */ thr_v?: number; /** - * MLSD parameter thr_d + * MLSD parameter `thr_d` */ thr_d?: number; }; diff --git a/invokeai/frontend/web/src/services/api/models/NormalbaeImageProcessorInvocation.ts b/invokeai/frontend/web/src/services/api/models/NormalbaeImageProcessorInvocation.ts index ecfb50a09f..400068171e 100644 --- a/invokeai/frontend/web/src/services/api/models/NormalbaeImageProcessorInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/NormalbaeImageProcessorInvocation.ts @@ -18,15 +18,15 @@ export type NormalbaeImageProcessorInvocation = { is_intermediate?: boolean; type?: 'normalbae_image_processor'; /** - * image to process + * The image to process */ image?: ImageField; /** - * pixel resolution for edge detection + * The pixel resolution for detection */ detect_resolution?: number; /** - * pixel resolution for output image + * The pixel resolution for the output image */ image_resolution?: number; }; diff --git a/invokeai/frontend/web/src/services/api/models/OpenposeImageProcessorInvocation.ts b/invokeai/frontend/web/src/services/api/models/OpenposeImageProcessorInvocation.ts index 5af21d542e..982ce8ade7 100644 --- a/invokeai/frontend/web/src/services/api/models/OpenposeImageProcessorInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/OpenposeImageProcessorInvocation.ts @@ -18,19 +18,19 @@ export type OpenposeImageProcessorInvocation = { is_intermediate?: boolean; type?: 'openpose_image_processor'; /** - * image to process + * The image to process */ image?: ImageField; /** - * whether to use hands and face mode + * Whether to use hands and face mode */ hand_and_face?: boolean; /** - * pixel resolution for edge detection + * The pixel resolution for detection */ detect_resolution?: number; /** - * pixel resolution for output image + * The pixel resolution for the output image */ image_resolution?: number; }; diff --git a/invokeai/frontend/web/src/services/api/models/PidiImageProcessorInvocation.ts b/invokeai/frontend/web/src/services/api/models/PidiImageProcessorInvocation.ts index a08bf6a920..91c9dc0ce5 100644 --- a/invokeai/frontend/web/src/services/api/models/PidiImageProcessorInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/PidiImageProcessorInvocation.ts @@ -18,23 +18,23 @@ export type PidiImageProcessorInvocation = { is_intermediate?: boolean; type?: 'pidi_image_processor'; /** - * image to process + * The image to process */ image?: ImageField; /** - * pixel resolution for edge detection + * The pixel resolution for detection */ detect_resolution?: number; /** - * pixel resolution for output image + * The pixel resolution for the output image */ image_resolution?: number; /** - * whether to use safe mode + * Whether to use safe mode */ safe?: boolean; /** - * whether to use scribble mode + * Whether to use scribble mode */ scribble?: boolean; }; diff --git a/invokeai/frontend/web/src/services/api/models/ZoeDepthImageProcessorInvocation.ts b/invokeai/frontend/web/src/services/api/models/ZoeDepthImageProcessorInvocation.ts index 55d05f3167..6caded8f04 100644 --- a/invokeai/frontend/web/src/services/api/models/ZoeDepthImageProcessorInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/ZoeDepthImageProcessorInvocation.ts @@ -18,7 +18,7 @@ export type ZoeDepthImageProcessorInvocation = { is_intermediate?: boolean; type?: 'zoe_depth_image_processor'; /** - * image to process + * The image to process */ image?: ImageField; }; diff --git a/invokeai/frontend/web/src/services/api/services/SessionsService.ts b/invokeai/frontend/web/src/services/api/services/SessionsService.ts index 6ae6783313..977c03e6fb 100644 --- a/invokeai/frontend/web/src/services/api/services/SessionsService.ts +++ b/invokeai/frontend/web/src/services/api/services/SessionsService.ts @@ -13,7 +13,7 @@ import type { Edge } from '../models/Edge'; import type { Graph } from '../models/Graph'; import type { GraphExecutionState } from '../models/GraphExecutionState'; import type { GraphInvocation } from '../models/GraphInvocation'; -import type { HedImageprocessorInvocation } from '../models/HedImageprocessorInvocation'; +import type { HedImageProcessorInvocation } from '../models/HedImageProcessorInvocation'; import type { ImageBlurInvocation } from '../models/ImageBlurInvocation'; import type { ImageChannelInvocation } from '../models/ImageChannelInvocation'; import type { ImageConvertInvocation } from '../models/ImageConvertInvocation'; @@ -171,7 +171,7 @@ export class SessionsService { * The id of the session */ sessionId: string, - requestBody: (LoadImageInvocation | ShowImageInvocation | ImageCropInvocation | ImagePasteInvocation | MaskFromAlphaInvocation | ImageMultiplyInvocation | ImageChannelInvocation | ImageConvertInvocation | ImageBlurInvocation | ImageResizeInvocation | ImageScaleInvocation | ImageLerpInvocation | ImageInverseLerpInvocation | ControlNetInvocation | ImageProcessorInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | ParamFloatInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | CannyImageProcessorInvocation | HedImageprocessorInvocation | LineartImageProcessorInvocation | LineartAnimeImageProcessorInvocation | OpenposeImageProcessorInvocation | MidasDepthImageProcessorInvocation | NormalbaeImageProcessorInvocation | MlsdImageProcessorInvocation | PidiImageProcessorInvocation | ContentShuffleImageProcessorInvocation | ZoeDepthImageProcessorInvocation | MediapipeFaceProcessorInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), + requestBody: (LoadImageInvocation | ShowImageInvocation | ImageCropInvocation | ImagePasteInvocation | MaskFromAlphaInvocation | ImageMultiplyInvocation | ImageChannelInvocation | ImageConvertInvocation | ImageBlurInvocation | ImageResizeInvocation | ImageScaleInvocation | ImageLerpInvocation | ImageInverseLerpInvocation | ControlNetInvocation | ImageProcessorInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | ParamFloatInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | CannyImageProcessorInvocation | HedImageProcessorInvocation | LineartImageProcessorInvocation | LineartAnimeImageProcessorInvocation | OpenposeImageProcessorInvocation | MidasDepthImageProcessorInvocation | NormalbaeImageProcessorInvocation | MlsdImageProcessorInvocation | PidiImageProcessorInvocation | ContentShuffleImageProcessorInvocation | ZoeDepthImageProcessorInvocation | MediapipeFaceProcessorInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), }): CancelablePromise { return __request(OpenAPI, { method: 'POST', @@ -208,7 +208,7 @@ export class SessionsService { * The path to the node in the graph */ nodePath: string, - requestBody: (LoadImageInvocation | ShowImageInvocation | ImageCropInvocation | ImagePasteInvocation | MaskFromAlphaInvocation | ImageMultiplyInvocation | ImageChannelInvocation | ImageConvertInvocation | ImageBlurInvocation | ImageResizeInvocation | ImageScaleInvocation | ImageLerpInvocation | ImageInverseLerpInvocation | ControlNetInvocation | ImageProcessorInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | ParamFloatInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | CannyImageProcessorInvocation | HedImageprocessorInvocation | LineartImageProcessorInvocation | LineartAnimeImageProcessorInvocation | OpenposeImageProcessorInvocation | MidasDepthImageProcessorInvocation | NormalbaeImageProcessorInvocation | MlsdImageProcessorInvocation | PidiImageProcessorInvocation | ContentShuffleImageProcessorInvocation | ZoeDepthImageProcessorInvocation | MediapipeFaceProcessorInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), + requestBody: (LoadImageInvocation | ShowImageInvocation | ImageCropInvocation | ImagePasteInvocation | MaskFromAlphaInvocation | ImageMultiplyInvocation | ImageChannelInvocation | ImageConvertInvocation | ImageBlurInvocation | ImageResizeInvocation | ImageScaleInvocation | ImageLerpInvocation | ImageInverseLerpInvocation | ControlNetInvocation | ImageProcessorInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | ParamFloatInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | CannyImageProcessorInvocation | HedImageProcessorInvocation | LineartImageProcessorInvocation | LineartAnimeImageProcessorInvocation | OpenposeImageProcessorInvocation | MidasDepthImageProcessorInvocation | NormalbaeImageProcessorInvocation | MlsdImageProcessorInvocation | PidiImageProcessorInvocation | ContentShuffleImageProcessorInvocation | ZoeDepthImageProcessorInvocation | MediapipeFaceProcessorInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), }): CancelablePromise { return __request(OpenAPI, { method: 'PUT', From 9cdad95f488a21011d532814082119403ce594f5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 2 Jun 2023 17:26:05 +1000 Subject: [PATCH 16/47] feat(ui): add rest of controlnet processors --- .../middleware/listenerMiddleware/index.ts | 2 + .../listeners/controlNetImageProcessed.ts | 23 +- .../controlNetProcessorParamsChanged.ts | 27 ++ .../controlNet/components/ControlNet.tsx | 105 +++--- .../ControlNetProcessorCollapse.tsx | 76 ----- .../components/ProcessorComponent.tsx | 131 +++++++ .../hooks/useProcessorNodeChanged.ts | 20 ++ .../ParamControlNetProcessorSelect.tsx | 46 +++ .../components/processors/CannyProcessor.tsx | 86 ++--- .../processors/ContentShuffleProcessor.tsx | 98 ++++++ .../components/processors/HedProcessor.tsx | 55 ++- .../processors/LineartAnimeProcessor.tsx | 40 ++- .../processors/LineartProcessor.tsx | 54 ++- .../processors/MediapipeFaceProcessor.tsx | 57 ++++ .../processors/MidasDepthProcessor.tsx | 55 +++ .../processors/MlsdImageProcessor.tsx | 85 +++++ .../processors/NormalBaeProcessor.tsx | 53 +++ .../processors/OpenposeProcessor.tsx | 66 ++++ .../components/processors/PidiProcessor.tsx | 74 ++++ .../processors/ZoeDepthProcessor.tsx | 14 + .../common/ControlNetProcessorButtons.tsx | 18 +- .../src/features/controlNet/store/actions.ts | 2 - .../features/controlNet/store/constants.ts | 166 +++++++++ .../controlNet/store/controlNetSlice.ts | 67 ++-- .../src/features/controlNet/store/types.ts | 323 +++++++++++++++++- .../graphBuilders/buildTextToImageGraph.ts | 6 +- 26 files changed, 1458 insertions(+), 291 deletions(-) create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetProcessorParamsChanged.ts delete mode 100644 invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorCollapse.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/ProcessorComponent.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/hooks/useProcessorNodeChanged.ts create mode 100644 invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetProcessorSelect.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/ContentShuffleProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/MediapipeFaceProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/MidasDepthProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/MlsdImageProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/NormalBaeProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/OpenposeProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/PidiProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/ZoeDepthProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/store/constants.ts 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 7089707217..9d938755f0 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -71,6 +71,7 @@ import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSa import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingAreaImageListener'; import { addImageCategoriesChangedListener } from './listeners/imageCategoriesChanged'; import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed'; +import { addControlNetProcessorParamsChangedListener } from './listeners/controlNetProcessorParamsChanged'; export const listenerMiddleware = createListenerMiddleware(); @@ -177,3 +178,4 @@ addImageCategoriesChangedListener(); // ControlNet addControlNetImageProcessedListener(); +addControlNetProcessorParamsChangedListener(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts index 901cb99bef..00cc2d2474 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts @@ -8,6 +8,7 @@ import { sessionReadyToInvoke } from 'features/system/store/actions'; import { socketInvocationComplete } from 'services/events/actions'; import { isImageOutput } from 'services/types/guards'; import { controlNetProcessedImageChanged } from 'features/controlNet/store/controlNetSlice'; +import { pick } from 'lodash-es'; const moduleLog = log.child({ namespace: 'controlNet' }); @@ -15,11 +16,27 @@ export const addControlNetImageProcessedListener = () => { startAppListening({ actionCreator: controlNetImageProcessed, effect: async (action, { dispatch, getState, take }) => { - const { controlNetId, processorNode } = action.payload; + const { controlNetId } = action.payload; + const controlNet = getState().controlNet.controlNets[controlNetId]; - // ControlNet one-off procressing graph is just he processor node, no edges + if (!controlNet.controlImage) { + moduleLog.error('Unable to process ControlNet image'); + return; + } + + // ControlNet one-off procressing graph is just the processor node, no edges. + // Also we need to grab the image. const graph: Graph = { - nodes: { [processorNode.id]: processorNode }, + nodes: { + [controlNet.processorNode.id]: { + ...controlNet.processorNode, + is_intermediate: true, + image: pick(controlNet.controlImage, [ + 'image_name', + 'image_origin', + ]), + }, + }, }; // Create a session to run the graph & wait til it's ready to invoke diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetProcessorParamsChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetProcessorParamsChanged.ts new file mode 100644 index 0000000000..315b793e53 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetProcessorParamsChanged.ts @@ -0,0 +1,27 @@ +import { startAppListening } from '..'; +import { log } from 'app/logging/useLogger'; +import { controlNetImageProcessed } from 'features/controlNet/store/actions'; +import { + controlNetProcessorParamsChanged, + controlNetProcessorTypeChanged, +} from 'features/controlNet/store/controlNetSlice'; + +const moduleLog = log.child({ namespace: 'controlNet' }); + +export const addControlNetProcessorParamsChangedListener = () => { + startAppListening({ + predicate: (action) => + controlNetProcessorParamsChanged.match(action) || + controlNetProcessorTypeChanged.match(action), + effect: async (action, { dispatch, cancelActiveListeners, delay }) => { + const { controlNetId } = action.payload; + // Cancel any in-progress instances of this listener + cancelActiveListeners(); + + // Delay before starting actual work + await delay(1000); + + dispatch(controlNetImageProcessed({ controlNetId })); + }, + }); +}; diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx index 0626b08fd9..b9b8e77fcc 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx @@ -1,51 +1,33 @@ -import { memo, useCallback, useState } from 'react'; -import { ControlNetProcessorNode } from '../store/types'; +import { memo, useCallback } from 'react'; +import { RequiredControlNetProcessorNode } from '../store/types'; import { ImageDTO } from 'services/api'; import CannyProcessor from './processors/CannyProcessor'; import { - CONTROLNET_PROCESSORS, ControlNet, - ControlNetModel, - ControlNetProcessor, - controlNetBeginStepPctChanged, - controlNetEndStepPctChanged, controlNetImageChanged, - controlNetModelChanged, controlNetProcessedImageChanged, - controlNetProcessorChanged, controlNetRemoved, - controlNetToggled, - controlNetWeightChanged, - isControlNetImageProcessedToggled, } from '../store/controlNetSlice'; import { useAppDispatch } from 'app/store/storeHooks'; -import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox'; -import IAISlider from 'common/components/IAISlider'; -import ParamControlNetIsEnabled from './parameters/ParamControlNetIsEnabled'; import ParamControlNetModel from './parameters/ParamControlNetModel'; import ParamControlNetWeight from './parameters/ParamControlNetWeight'; import ParamControlNetBeginStepPct from './parameters/ParamControlNetBeginStepPct'; import ParamControlNetEndStepPct from './parameters/ParamControlNetEndStepPct'; import { - Box, Flex, - HStack, Tab, TabList, TabPanel, TabPanels, Tabs, - VStack, - useDisclosure, } from '@chakra-ui/react'; import IAISelectableImage from './parameters/IAISelectableImage'; import IAIButton from 'common/components/IAIButton'; -import IAIIconButton from 'common/components/IAIIconButton'; -import IAISwitch from 'common/components/IAISwitch'; -import ParamControlNetIsPreprocessed from './parameters/ParamControlNetIsPreprocessed'; -import IAICollapse from 'common/components/IAICollapse'; -import ControlNetProcessorCollapse from './ControlNetProcessorCollapse'; -import IAICustomSelect from 'common/components/IAICustomSelect'; +import { controlNetImageProcessed } from '../store/actions'; +import { FaUndo } from 'react-icons/fa'; +import HedProcessor from './processors/HedProcessor'; +import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect'; +import ProcessorComponent from './ProcessorComponent'; type ControlNetProps = { controlNet: ControlNet; @@ -62,22 +44,10 @@ const ControlNet = (props: ControlNetProps) => { controlImage, isControlImageProcessed, processedControlImage, - processor, + processorNode, } = props.controlNet; const dispatch = useAppDispatch(); - const handleProcessorTypeChanged = useCallback( - (processor: string | null | undefined) => { - dispatch( - controlNetProcessorChanged({ - controlNetId, - processor: processor as ControlNetProcessor, - }) - ); - }, - [controlNetId, dispatch] - ); - const handleControlImageChanged = useCallback( (controlImage: ImageDTO) => { dispatch(controlNetImageChanged({ controlNetId, controlImage })); @@ -85,6 +55,23 @@ const ControlNet = (props: ControlNetProps) => { [controlNetId, dispatch] ); + const handleProcess = useCallback(() => { + dispatch( + controlNetImageProcessed({ + controlNetId, + }) + ); + }, [controlNetId, dispatch]); + + const handleReset = useCallback(() => { + dispatch( + controlNetProcessedImageChanged({ + controlNetId, + processedControlImage: null, + }) + ); + }, [controlNetId, dispatch]); + const handleControlImageReset = useCallback(() => { dispatch(controlNetImageChanged({ controlNetId, controlImage: null })); }, [controlNetId, dispatch]); @@ -137,18 +124,29 @@ const ControlNet = (props: ControlNetProps) => { /> - + + Preprocess + + } + onClick={handleReset} + isDisabled={Boolean(!processedControlImage)} + > + Reset Processing + @@ -158,18 +156,3 @@ const ControlNet = (props: ControlNetProps) => { }; export default memo(ControlNet); - -export type ControlNetProcessorProps = { - controlNetId: string; - controlImage: ImageDTO | null; - processedControlImage: ImageDTO | null; - type: ControlNetProcessor; -}; - -const ProcessorComponent = (props: ControlNetProcessorProps) => { - const { type } = props; - if (type === 'canny') { - return ; - } - return null; -}; diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorCollapse.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorCollapse.tsx deleted file mode 100644 index 0ce675f4ed..0000000000 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorCollapse.tsx +++ /dev/null @@ -1,76 +0,0 @@ -// import { Collapse, Flex, useDisclosure } from '@chakra-ui/react'; -// import { memo, useState } from 'react'; -// import CannyProcessor from './processors/CannyProcessor'; -// import { ImageDTO } from 'services/api'; -// import IAICustomSelect from 'common/components/IAICustomSelect'; -// import { -// CONTROLNET_PROCESSORS, -// ControlNetProcessor, -// } from '../store/controlNetSlice'; -// import IAISwitch from 'common/components/IAISwitch'; - -// export type ControlNetProcessorProps = { -// controlNetId: string; -// controlImage: ImageDTO | null; -// processedControlImage: ImageDTO | null; -// type: ControlNetProcessor; -// }; - -// const ProcessorComponent = (props: ControlNetProcessorProps) => { -// const { type } = props; -// if (type === 'canny') { -// return ; -// } -// return null; -// }; - -// type ControlNetProcessorCollapseProps = { -// isOpen: boolean; -// controlNetId: string; -// controlImage: ImageDTO | null; -// processedControlImage: ImageDTO | null; -// }; -// const ControlNetProcessorCollapse = ( -// props: ControlNetProcessorCollapseProps -// ) => { -// const { isOpen, controlImage, controlNetId, processedControlImage } = props; - -// const [processorType, setProcessorType] = -// useState('canny'); - -// const handleProcessorTypeChanged = (type: string | null | undefined) => { -// setProcessorType(type as ControlNetProcessor); -// }; - -// return ( -// -// -// {controlImage && ( -// -// )} -// -// ); -// }; - -// export default memo(ControlNetProcessorCollapse); - -export default {}; diff --git a/invokeai/frontend/web/src/features/controlNet/components/ProcessorComponent.tsx b/invokeai/frontend/web/src/features/controlNet/components/ProcessorComponent.tsx new file mode 100644 index 0000000000..246aea70c7 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/ProcessorComponent.tsx @@ -0,0 +1,131 @@ +import { memo } from 'react'; +import { RequiredControlNetProcessorNode } from '../store/types'; +import CannyProcessor from './processors/CannyProcessor'; +import HedProcessor from './processors/HedProcessor'; +import LineartProcessor from './processors/LineartProcessor'; +import LineartAnimeProcessor from './processors/LineartAnimeProcessor'; +import ContentShuffleProcessor from './processors/ContentShuffleProcessor'; +import MediapipeFaceProcessor from './processors/MediapipeFaceProcessor'; +import MidasDepthProcessor from './processors/MidasDepthProcessor'; +import MlsdImageProcessor from './processors/MlsdImageProcessor'; +import NormalBaeProcessor from './processors/NormalBaeProcessor'; +import OpenposeProcessor from './processors/OpenposeProcessor'; +import PidiProcessor from './processors/PidiProcessor'; +import ZoeDepthProcessor from './processors/ZoeDepthProcessor'; + +export type ControlNetProcessorProps = { + controlNetId: string; + processorNode: RequiredControlNetProcessorNode; +}; + +const ProcessorComponent = (props: ControlNetProcessorProps) => { + const { controlNetId, processorNode } = props; + if (processorNode.type === 'canny_image_processor') { + return ( + + ); + } + + if (processorNode.type === 'hed_image_processor') { + return ( + + ); + } + + if (processorNode.type === 'lineart_image_processor') { + return ( + + ); + } + + if (processorNode.type === 'content_shuffle_image_processor') { + return ( + + ); + } + + if (processorNode.type === 'lineart_anime_image_processor') { + return ( + + ); + } + + if (processorNode.type === 'mediapipe_face_processor') { + return ( + + ); + } + + if (processorNode.type === 'midas_depth_image_processor') { + return ( + + ); + } + + if (processorNode.type === 'mlsd_image_processor') { + return ( + + ); + } + + if (processorNode.type === 'normalbae_image_processor') { + return ( + + ); + } + + if (processorNode.type === 'openpose_image_processor') { + return ( + + ); + } + + if (processorNode.type === 'pidi_image_processor') { + return ( + + ); + } + + if (processorNode.type === 'zoe_depth_image_processor') { + return ( + + ); + } + + return null; +}; + +export default memo(ProcessorComponent); diff --git a/invokeai/frontend/web/src/features/controlNet/components/hooks/useProcessorNodeChanged.ts b/invokeai/frontend/web/src/features/controlNet/components/hooks/useProcessorNodeChanged.ts new file mode 100644 index 0000000000..79a502cb0e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/hooks/useProcessorNodeChanged.ts @@ -0,0 +1,20 @@ +import { useAppDispatch } from 'app/store/storeHooks'; +import { controlNetProcessorParamsChanged } from 'features/controlNet/store/controlNetSlice'; +import { ControlNetProcessorNode } from 'features/controlNet/store/types'; +import { useCallback } from 'react'; + +export const useProcessorNodeChanged = () => { + const dispatch = useAppDispatch(); + const handleProcessorNodeChanged = useCallback( + (controlNetId: string, changes: Partial) => { + dispatch( + controlNetProcessorParamsChanged({ + controlNetId, + changes, + }) + ); + }, + [dispatch] + ); + return handleProcessorNodeChanged; +}; diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetProcessorSelect.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetProcessorSelect.tsx new file mode 100644 index 0000000000..9d21727a3c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetProcessorSelect.tsx @@ -0,0 +1,46 @@ +import IAICustomSelect from 'common/components/IAICustomSelect'; +import { memo, useCallback } from 'react'; +import { + ControlNetProcessorNode, + ControlNetProcessorType, +} from '../../store/types'; +import { controlNetProcessorTypeChanged } from '../../store/controlNetSlice'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { CONTROLNET_PROCESSORS } from '../../store/constants'; + +type ParamControlNetProcessorSelectProps = { + controlNetId: string; + processorNode: ControlNetProcessorNode; +}; + +const CONTROLNET_PROCESSOR_TYPES = Object.keys( + CONTROLNET_PROCESSORS +) as ControlNetProcessorType[]; + +const ParamControlNetProcessorSelect = ( + props: ParamControlNetProcessorSelectProps +) => { + const { controlNetId, processorNode } = props; + const dispatch = useAppDispatch(); + const handleProcessorTypeChanged = useCallback( + (v: string | null | undefined) => { + dispatch( + controlNetProcessorTypeChanged({ + controlNetId, + processorType: v as ControlNetProcessorType, + }) + ); + }, + [controlNetId, dispatch] + ); + return ( + + ); +}; + +export default memo(ParamControlNetProcessorSelect); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx index a30c003e86..872be06177 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx @@ -1,75 +1,61 @@ import { Flex } from '@chakra-ui/react'; import IAISlider from 'common/components/IAISlider'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { controlNetImageProcessed } from 'features/controlNet/store/actions'; -import { controlNetProcessedImageChanged } from 'features/controlNet/store/controlNetSlice'; -import ControlNetProcessorButtons from './common/ControlNetProcessorButtons'; -import { memo, useCallback, useState } from 'react'; -import { ControlNetProcessorProps } from '../ControlNet'; +import { memo, useCallback } from 'react'; +import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; +import { RequiredCannyImageProcessorInvocation } from 'features/controlNet/store/types'; -export const CANNY_PROCESSOR = 'canny_image_processor'; +type CannyProcessorProps = { + controlNetId: string; + processorNode: RequiredCannyImageProcessorInvocation; +}; -const CannyProcessor = (props: ControlNetProcessorProps) => { - const { controlNetId, controlImage, processedControlImage, type } = props; - const dispatch = useAppDispatch(); - const [lowThreshold, setLowThreshold] = useState(100); - const [highThreshold, setHighThreshold] = useState(200); +const CannyProcessor = (props: CannyProcessorProps) => { + const { controlNetId, processorNode } = props; + const { low_threshold, high_threshold } = processorNode; + const processorChanged = useProcessorNodeChanged(); - const handleProcess = useCallback(() => { - if (!controlImage) { - return; - } + const handleLowThresholdChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { low_threshold: v }); + }, + [controlNetId, processorChanged] + ); - dispatch( - controlNetImageProcessed({ - controlNetId, - processorNode: { - id: CANNY_PROCESSOR, - type: 'canny_image_processor', - image: { - image_name: controlImage.image_name, - image_origin: controlImage.image_origin, - }, - low_threshold: lowThreshold, - high_threshold: highThreshold, - }, - }) - ); - }, [controlNetId, dispatch, highThreshold, controlImage, lowThreshold]); + const handleLowThresholdReset = useCallback(() => { + processorChanged(controlNetId, { low_threshold: 100 }); + }, [controlNetId, processorChanged]); - const handleReset = useCallback(() => { - dispatch( - controlNetProcessedImageChanged({ - controlNetId, - processedControlImage: null, - }) - ); - }, [controlNetId, dispatch]); + const handleHighThresholdChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { high_threshold: v }); + }, + [controlNetId, processorChanged] + ); + + const handleHighThresholdReset = useCallback(() => { + processorChanged(controlNetId, { high_threshold: 200 }); + }, [controlNetId, processorChanged]); return ( - ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/ContentShuffleProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/ContentShuffleProcessor.tsx new file mode 100644 index 0000000000..480275cd1c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/ContentShuffleProcessor.tsx @@ -0,0 +1,98 @@ +import { Flex } from '@chakra-ui/react'; +import IAISlider from 'common/components/IAISlider'; +import { memo, useCallback } from 'react'; +import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; +import { RequiredContentShuffleImageProcessorInvocation } from 'features/controlNet/store/types'; + +type Props = { + controlNetId: string; + processorNode: RequiredContentShuffleImageProcessorInvocation; +}; + +const ContentShuffleProcessor = (props: Props) => { + const { controlNetId, processorNode } = props; + const { image_resolution, detect_resolution, w, h, f } = processorNode; + const processorChanged = useProcessorNodeChanged(); + + const handleDetectResolutionChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { detect_resolution: v }); + }, + [controlNetId, processorChanged] + ); + + const handleImageResolutionChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { image_resolution: v }); + }, + [controlNetId, processorChanged] + ); + + const handleWChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { w: v }); + }, + [controlNetId, processorChanged] + ); + + const handleHChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { h: v }); + }, + [controlNetId, processorChanged] + ); + + const handleFChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { f: v }); + }, + [controlNetId, processorChanged] + ); + + return ( + + + + + + + + ); +}; + +export default memo(ContentShuffleProcessor); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx index 891f6d0adc..22c5c487cd 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx @@ -1,39 +1,66 @@ import { Flex } from '@chakra-ui/react'; import IAISlider from 'common/components/IAISlider'; import IAISwitch from 'common/components/IAISwitch'; -import { ChangeEvent, memo, useState } from 'react'; +import { ChangeEvent, memo, useCallback } from 'react'; +import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; +import { RequiredHedImageProcessorInvocation } from 'features/controlNet/store/types'; -const HedPreprocessor = () => { - const [detectResolution, setDetectResolution] = useState(512); - const [imageResolution, setImageResolution] = useState(512); - const [isScribbleEnabled, setIsScribbleEnabled] = useState(false); +type HedProcessorProps = { + controlNetId: string; + processorNode: RequiredHedImageProcessorInvocation; +}; - const handleChangeScribble = (e: ChangeEvent) => { - setIsScribbleEnabled(e.target.checked); - }; +const HedPreprocessor = (props: HedProcessorProps) => { + const { + controlNetId, + processorNode: { detect_resolution, image_resolution, scribble }, + } = props; + + const processorChanged = useProcessorNodeChanged(); + + const handleDetectResolutionChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { detect_resolution: v }); + }, + [controlNetId, processorChanged] + ); + + const handleImageResolutionChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { image_resolution: v }); + }, + [controlNetId, processorChanged] + ); + + const handleScribbleChanged = useCallback( + (e: ChangeEvent) => { + processorChanged(controlNetId, { scribble: e.target.checked }); + }, + [controlNetId, processorChanged] + ); return ( ); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx index 6d4f61d8af..87a21f95f0 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx @@ -1,25 +1,47 @@ import { Flex } from '@chakra-ui/react'; import IAISlider from 'common/components/IAISlider'; -import { memo, useState } from 'react'; +import { memo, useCallback } from 'react'; +import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; +import { RequiredLineartAnimeImageProcessorInvocation } from 'features/controlNet/store/types'; -const LineartPreprocessor = () => { - const [detectResolution, setDetectResolution] = useState(512); - const [imageResolution, setImageResolution] = useState(512); +type Props = { + controlNetId: string; + processorNode: RequiredLineartAnimeImageProcessorInvocation; +}; + +const LineartAnimeProcessor = (props: Props) => { + const { controlNetId, processorNode } = props; + const { image_resolution, detect_resolution } = processorNode; + const processorChanged = useProcessorNodeChanged(); + + const handleDetectResolutionChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { detect_resolution: v }); + }, + [controlNetId, processorChanged] + ); + + const handleImageResolutionChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { image_resolution: v }); + }, + [controlNetId, processorChanged] + ); return ( { ); }; -export default memo(LineartPreprocessor); +export default memo(LineartAnimeProcessor); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx index 763d6f2b37..503fd73fa8 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx @@ -1,42 +1,66 @@ import { Flex } from '@chakra-ui/react'; import IAISlider from 'common/components/IAISlider'; +import { ChangeEvent, memo, useCallback } from 'react'; +import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; +import { RequiredLineartImageProcessorInvocation } from 'features/controlNet/store/types'; import IAISwitch from 'common/components/IAISwitch'; -import { ChangeEvent, memo, useState } from 'react'; -const LineartPreprocessor = () => { - const [detectResolution, setDetectResolution] = useState(512); - const [imageResolution, setImageResolution] = useState(512); - const [isCoarseEnabled, setIsCoarseEnabled] = useState(false); +type LineartProcessorProps = { + controlNetId: string; + processorNode: RequiredLineartImageProcessorInvocation; +}; - const handleChangeScribble = (e: ChangeEvent) => { - setIsCoarseEnabled(e.target.checked); - }; +const LineartProcessor = (props: LineartProcessorProps) => { + const { controlNetId, processorNode } = props; + const { image_resolution, detect_resolution, coarse } = processorNode; + const processorChanged = useProcessorNodeChanged(); + + const handleDetectResolutionChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { detect_resolution: v }); + }, + [controlNetId, processorChanged] + ); + + const handleImageResolutionChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { image_resolution: v }); + }, + [controlNetId, processorChanged] + ); + + const handleCoarseChanged = useCallback( + (e: ChangeEvent) => { + processorChanged(controlNetId, { coarse: e.target.checked }); + }, + [controlNetId, processorChanged] + ); return ( ); }; -export default memo(LineartPreprocessor); +export default memo(LineartProcessor); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/MediapipeFaceProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/MediapipeFaceProcessor.tsx new file mode 100644 index 0000000000..f76f00c3b7 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/MediapipeFaceProcessor.tsx @@ -0,0 +1,57 @@ +import { Flex } from '@chakra-ui/react'; +import IAISlider from 'common/components/IAISlider'; +import { memo, useCallback } from 'react'; +import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; +import { + RequiredContentShuffleImageProcessorInvocation, + RequiredMediapipeFaceProcessorInvocation, +} from 'features/controlNet/store/types'; + +type Props = { + controlNetId: string; + processorNode: RequiredMediapipeFaceProcessorInvocation; +}; + +const MediapipeFaceProcessor = (props: Props) => { + const { controlNetId, processorNode } = props; + const { max_faces, min_confidence } = processorNode; + const processorChanged = useProcessorNodeChanged(); + + const handleMaxFacesChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { max_faces: v }); + }, + [controlNetId, processorChanged] + ); + + const handleMinConfidenceChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { min_confidence: v }); + }, + [controlNetId, processorChanged] + ); + + return ( + + + + + ); +}; + +export default memo(MediapipeFaceProcessor); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/MidasDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/MidasDepthProcessor.tsx new file mode 100644 index 0000000000..9a205f28b5 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/MidasDepthProcessor.tsx @@ -0,0 +1,55 @@ +import { Flex } from '@chakra-ui/react'; +import IAISlider from 'common/components/IAISlider'; +import { memo, useCallback } from 'react'; +import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; +import { RequiredMidasDepthImageProcessorInvocation } from 'features/controlNet/store/types'; + +type Props = { + controlNetId: string; + processorNode: RequiredMidasDepthImageProcessorInvocation; +}; + +const MidasDepthProcessor = (props: Props) => { + const { controlNetId, processorNode } = props; + const { a_mult, bg_th } = processorNode; + const processorChanged = useProcessorNodeChanged(); + + const handleAMultChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { a_mult: v }); + }, + [controlNetId, processorChanged] + ); + + const handleBgThChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { bg_th: v }); + }, + [controlNetId, processorChanged] + ); + + return ( + + + + + ); +}; + +export default memo(MidasDepthProcessor); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/MlsdImageProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/MlsdImageProcessor.tsx new file mode 100644 index 0000000000..e33b2102d1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/MlsdImageProcessor.tsx @@ -0,0 +1,85 @@ +import { Flex } from '@chakra-ui/react'; +import IAISlider from 'common/components/IAISlider'; +import { memo, useCallback } from 'react'; +import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; +import { RequiredMlsdImageProcessorInvocation } from 'features/controlNet/store/types'; + +type Props = { + controlNetId: string; + processorNode: RequiredMlsdImageProcessorInvocation; +}; + +const MlsdImageProcessor = (props: Props) => { + const { controlNetId, processorNode } = props; + const { image_resolution, detect_resolution, thr_d, thr_v } = processorNode; + const processorChanged = useProcessorNodeChanged(); + + const handleDetectResolutionChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { detect_resolution: v }); + }, + [controlNetId, processorChanged] + ); + + const handleImageResolutionChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { image_resolution: v }); + }, + [controlNetId, processorChanged] + ); + + const handleThrDChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { thr_d: v }); + }, + [controlNetId, processorChanged] + ); + + const handleThrVChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { thr_v: v }); + }, + [controlNetId, processorChanged] + ); + + return ( + + + + + + + ); +}; + +export default memo(MlsdImageProcessor); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/NormalBaeProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/NormalBaeProcessor.tsx new file mode 100644 index 0000000000..61836e7668 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/NormalBaeProcessor.tsx @@ -0,0 +1,53 @@ +import { Flex } from '@chakra-ui/react'; +import IAISlider from 'common/components/IAISlider'; +import { memo, useCallback } from 'react'; +import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; +import { RequiredNormalbaeImageProcessorInvocation } from 'features/controlNet/store/types'; + +type Props = { + controlNetId: string; + processorNode: RequiredNormalbaeImageProcessorInvocation; +}; + +const NormalBaeProcessor = (props: Props) => { + const { controlNetId, processorNode } = props; + const { image_resolution, detect_resolution } = processorNode; + const processorChanged = useProcessorNodeChanged(); + + const handleDetectResolutionChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { detect_resolution: v }); + }, + [controlNetId, processorChanged] + ); + + const handleImageResolutionChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { image_resolution: v }); + }, + [controlNetId, processorChanged] + ); + + return ( + + + + + ); +}; + +export default memo(NormalBaeProcessor); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/OpenposeProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/OpenposeProcessor.tsx new file mode 100644 index 0000000000..63556d4da4 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/OpenposeProcessor.tsx @@ -0,0 +1,66 @@ +import { Flex } from '@chakra-ui/react'; +import IAISlider from 'common/components/IAISlider'; +import { ChangeEvent, memo, useCallback } from 'react'; +import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; +import { RequiredOpenposeImageProcessorInvocation } from 'features/controlNet/store/types'; +import IAISwitch from 'common/components/IAISwitch'; + +type Props = { + controlNetId: string; + processorNode: RequiredOpenposeImageProcessorInvocation; +}; + +const OpenposeProcessor = (props: Props) => { + const { controlNetId, processorNode } = props; + const { image_resolution, detect_resolution, hand_and_face } = processorNode; + const processorChanged = useProcessorNodeChanged(); + + const handleDetectResolutionChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { detect_resolution: v }); + }, + [controlNetId, processorChanged] + ); + + const handleImageResolutionChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { image_resolution: v }); + }, + [controlNetId, processorChanged] + ); + + const handleHandAndFaceChanged = useCallback( + (e: ChangeEvent) => { + processorChanged(controlNetId, { hand_and_face: e.target.checked }); + }, + [controlNetId, processorChanged] + ); + + return ( + + + + + + ); +}; + +export default memo(OpenposeProcessor); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/PidiProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/PidiProcessor.tsx new file mode 100644 index 0000000000..711d4930ab --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/PidiProcessor.tsx @@ -0,0 +1,74 @@ +import { Flex } from '@chakra-ui/react'; +import IAISlider from 'common/components/IAISlider'; +import { ChangeEvent, memo, useCallback } from 'react'; +import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; +import { RequiredPidiImageProcessorInvocation } from 'features/controlNet/store/types'; +import IAISwitch from 'common/components/IAISwitch'; + +type Props = { + controlNetId: string; + processorNode: RequiredPidiImageProcessorInvocation; +}; + +const PidiProcessor = (props: Props) => { + const { controlNetId, processorNode } = props; + const { image_resolution, detect_resolution, scribble, safe } = processorNode; + const processorChanged = useProcessorNodeChanged(); + + const handleDetectResolutionChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { detect_resolution: v }); + }, + [controlNetId, processorChanged] + ); + + const handleImageResolutionChanged = useCallback( + (v: number) => { + processorChanged(controlNetId, { image_resolution: v }); + }, + [controlNetId, processorChanged] + ); + + const handleScribbleChanged = useCallback( + (e: ChangeEvent) => { + processorChanged(controlNetId, { scribble: e.target.checked }); + }, + [controlNetId, processorChanged] + ); + + const handleSafeChanged = useCallback( + (e: ChangeEvent) => { + processorChanged(controlNetId, { safe: e.target.checked }); + }, + [controlNetId, processorChanged] + ); + + return ( + + + + + + + ); +}; + +export default memo(PidiProcessor); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/ZoeDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/ZoeDepthProcessor.tsx new file mode 100644 index 0000000000..20a1ec4493 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/ZoeDepthProcessor.tsx @@ -0,0 +1,14 @@ +import { memo } from 'react'; +import { RequiredZoeDepthImageProcessorInvocation } from 'features/controlNet/store/types'; + +type Props = { + controlNetId: string; + processorNode: RequiredZoeDepthImageProcessorInvocation; +}; + +const ZoeDepthProcessor = (props: Props) => { + // Has no parameters? + return null; +}; + +export default memo(ZoeDepthProcessor); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorButtons.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorButtons.tsx index afa94d6ada..a051990f67 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorButtons.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorButtons.tsx @@ -21,23 +21,7 @@ const ControlNetProcessorButtons = (props: ControlNetProcessorButtonsProps) => { alignItems: 'center', justifyContent: 'stretch', }} - > - - Preprocess - - } - onClick={handleReset} - isDisabled={isResetDisabled} - > - Reset Processing - - + > ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/store/actions.ts b/invokeai/frontend/web/src/features/controlNet/store/actions.ts index 9b6c11f22d..3d9f56a36b 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/actions.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/actions.ts @@ -1,7 +1,5 @@ import { createAction } from '@reduxjs/toolkit'; -import { ControlNetProcessorNode } from './types'; export const controlNetImageProcessed = createAction<{ controlNetId: string; - processorNode: ControlNetProcessorNode; }>('controlNet/imageProcessed'); diff --git a/invokeai/frontend/web/src/features/controlNet/store/constants.ts b/invokeai/frontend/web/src/features/controlNet/store/constants.ts new file mode 100644 index 0000000000..d7a76b6a5e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/store/constants.ts @@ -0,0 +1,166 @@ +import { + ControlNetProcessorType, + RequiredControlNetProcessorNode, +} from './types'; + +type ControlNetProcessorsDict = Record< + ControlNetProcessorType, + { + type: ControlNetProcessorType; + label: string; + description: string; + default: RequiredControlNetProcessorNode; + } +>; + +/** + * A dict of ControlNet processors, including: + * - type + * - label + * - description + * - default values + * + * TODO: Generate from the OpenAPI schema + */ +export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = { + canny_image_processor: { + type: 'canny_image_processor', + label: 'Canny', + description: '', + default: { + id: 'canny_image_processor', + type: 'canny_image_processor', + low_threshold: 100, + high_threshold: 200, + }, + }, + content_shuffle_image_processor: { + type: 'content_shuffle_image_processor', + label: 'Content Shuffle', + description: '', + default: { + id: 'content_shuffle_image_processor', + type: 'content_shuffle_image_processor', + detect_resolution: 512, + image_resolution: 512, + h: 512, + w: 512, + f: 256, + }, + }, + hed_image_processor: { + type: 'hed_image_processor', + label: 'HED', + description: '', + default: { + id: 'hed_image_processor', + type: 'hed_image_processor', + detect_resolution: 512, + image_resolution: 512, + scribble: false, + }, + }, + lineart_anime_image_processor: { + type: 'lineart_anime_image_processor', + label: 'Lineart Anime', + description: '', + default: { + id: 'lineart_anime_image_processor', + type: 'lineart_anime_image_processor', + detect_resolution: 512, + image_resolution: 512, + }, + }, + lineart_image_processor: { + type: 'lineart_image_processor', + label: 'Lineart', + description: '', + default: { + id: 'lineart_image_processor', + type: 'lineart_image_processor', + detect_resolution: 512, + image_resolution: 512, + coarse: false, + }, + }, + mediapipe_face_processor: { + type: 'mediapipe_face_processor', + label: 'Mediapipe Face', + description: '', + default: { + id: 'mediapipe_face_processor', + type: 'mediapipe_face_processor', + max_faces: 1, + min_confidence: 0.5, + }, + }, + midas_depth_image_processor: { + type: 'midas_depth_image_processor', + label: 'Depth (Midas)', + description: '', + default: { + id: 'midas_depth_image_processor', + type: 'midas_depth_image_processor', + a_mult: 2, + bg_th: 0.1, + }, + }, + mlsd_image_processor: { + type: 'mlsd_image_processor', + label: 'MLSD', + description: '', + default: { + id: 'mlsd_image_processor', + type: 'mlsd_image_processor', + detect_resolution: 512, + image_resolution: 512, + thr_d: 0.1, + thr_v: 0.1, + }, + }, + normalbae_image_processor: { + type: 'normalbae_image_processor', + label: 'NormalBae', + description: '', + default: { + id: 'normalbae_image_processor', + type: 'normalbae_image_processor', + detect_resolution: 512, + image_resolution: 512, + }, + }, + openpose_image_processor: { + type: 'openpose_image_processor', + label: 'Openpose', + description: '', + default: { + id: 'openpose_image_processor', + type: 'openpose_image_processor', + detect_resolution: 512, + image_resolution: 512, + hand_and_face: false, + }, + }, + pidi_image_processor: { + type: 'pidi_image_processor', + label: 'PIDI', + description: '', + default: { + id: 'pidi_image_processor', + type: 'pidi_image_processor', + detect_resolution: 512, + image_resolution: 512, + scribble: false, + safe: false, + }, + }, + zoe_depth_image_processor: { + type: 'zoe_depth_image_processor', + label: 'Depth (Zoe)', + description: '', + default: { + id: 'zoe_depth_image_processor', + type: 'zoe_depth_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 a87b591bad..674e130bdc 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts @@ -1,11 +1,12 @@ -import { - $CombinedState, - PayloadAction, - createSelector, -} from '@reduxjs/toolkit'; +import { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import { RootState } from 'app/store/store'; import { ImageDTO } from 'services/api'; +import { + ControlNetProcessorType, + RequiredControlNetProcessorNode, +} from './types'; +import { CONTROLNET_PROCESSORS } from './constants'; export const CONTROLNET_MODELS = [ 'lllyasviel/sd-controlnet-canny', @@ -18,23 +19,6 @@ export const CONTROLNET_MODELS = [ 'lllyasviel/sd-controlnet-mlsd', ]; -export const CONTROLNET_PROCESSORS = [ - 'canny', - 'contentShuffle', - 'hed', - 'lineart', - 'lineartAnime', - 'mediapipeFace', - 'midasDepth', - 'mlsd', - 'normalBae', - 'openpose', - 'pidi', - 'zoeDepth', -]; - -export type ControlNetProcessor = (typeof CONTROLNET_PROCESSORS)[number]; - export type ControlNetModel = (typeof CONTROLNET_MODELS)[number]; export const initialControlNet: Omit = { @@ -46,7 +30,7 @@ export const initialControlNet: Omit = { controlImage: null, isControlImageProcessed: false, processedControlImage: null, - processor: 'canny', + processorNode: CONTROLNET_PROCESSORS.canny_image_processor.default, }; export type ControlNet = { @@ -59,17 +43,19 @@ export type ControlNet = { controlImage: ImageDTO | null; isControlImageProcessed: boolean; processedControlImage: ImageDTO | null; - processor: ControlNetProcessor; + processorNode: RequiredControlNetProcessorNode; }; export type ControlNetState = { controlNets: Record; isEnabled: boolean; + shouldAutoProcess: boolean; }; export const initialControlNetState: ControlNetState = { controlNets: {}, isEnabled: false, + shouldAutoProcess: true, }; export const controlNetSlice = createSlice({ @@ -169,15 +155,36 @@ export const controlNetSlice = createSlice({ const { controlNetId, endStepPct } = action.payload; state.controlNets[controlNetId].endStepPct = endStepPct; }, - controlNetProcessorChanged: ( + controlNetProcessorParamsChanged: ( state, action: PayloadAction<{ controlNetId: string; - processor: ControlNetProcessor; + changes: Omit< + Partial, + 'id' | 'type' | 'is_intermediate' + >; }> ) => { - const { controlNetId, processor } = action.payload; - state.controlNets[controlNetId].processor = processor; + const { controlNetId, changes } = action.payload; + const processorNode = state.controlNets[controlNetId].processorNode; + state.controlNets[controlNetId].processorNode = { + ...processorNode, + ...changes, + }; + }, + controlNetProcessorTypeChanged: ( + state, + action: PayloadAction<{ + controlNetId: string; + processorType: ControlNetProcessorType; + }> + ) => { + const { controlNetId, processorType } = action.payload; + state.controlNets[controlNetId].processorNode = + CONTROLNET_PROCESSORS[processorType].default; + }, + shouldAutoProcessToggled: (state) => { + state.shouldAutoProcess = !state.shouldAutoProcess; }, }, }); @@ -195,7 +202,9 @@ export const { controlNetWeightChanged, controlNetBeginStepPctChanged, controlNetEndStepPctChanged, - controlNetProcessorChanged, + controlNetProcessorParamsChanged, + controlNetProcessorTypeChanged, + shouldAutoProcessToggled, } = controlNetSlice.actions; export default controlNetSlice.reducer; diff --git a/invokeai/frontend/web/src/features/controlNet/store/types.ts b/invokeai/frontend/web/src/features/controlNet/store/types.ts index ca3af7b406..808a50010b 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/types.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/types.ts @@ -1,7 +1,8 @@ +import { isObject } from 'lodash-es'; import { CannyImageProcessorInvocation, ContentShuffleImageProcessorInvocation, - HedImageprocessorInvocation, + HedImageProcessorInvocation, LineartAnimeImageProcessorInvocation, LineartImageProcessorInvocation, MediapipeFaceProcessorInvocation, @@ -12,17 +13,317 @@ import { PidiImageProcessorInvocation, ZoeDepthImageProcessorInvocation, } from 'services/api'; +import { O } from 'ts-toolbelt'; +/** + * Any ControlNet processor node + */ export type ControlNetProcessorNode = | CannyImageProcessorInvocation - | HedImageprocessorInvocation - | LineartImageProcessorInvocation - | LineartAnimeImageProcessorInvocation - | OpenposeImageProcessorInvocation - | MidasDepthImageProcessorInvocation - | NormalbaeImageProcessorInvocation - | MlsdImageProcessorInvocation - | PidiImageProcessorInvocation | ContentShuffleImageProcessorInvocation - | ZoeDepthImageProcessorInvocation - | MediapipeFaceProcessorInvocation; + | HedImageProcessorInvocation + | LineartAnimeImageProcessorInvocation + | LineartImageProcessorInvocation + | MediapipeFaceProcessorInvocation + | MidasDepthImageProcessorInvocation + | MlsdImageProcessorInvocation + | NormalbaeImageProcessorInvocation + | OpenposeImageProcessorInvocation + | PidiImageProcessorInvocation + | ZoeDepthImageProcessorInvocation; + +/** + * Any ControlNet processor type + */ +export type ControlNetProcessorType = NonNullable< + ControlNetProcessorNode['type'] +>; + +/** + * The Canny processor node, with parameters flagged as required + */ +export type RequiredCannyImageProcessorInvocation = O.Required< + CannyImageProcessorInvocation, + 'type' | 'low_threshold' | 'high_threshold' +>; + +/** + * The ContentShuffle processor node, with parameters flagged as required + */ +export type RequiredContentShuffleImageProcessorInvocation = O.Required< + ContentShuffleImageProcessorInvocation, + 'type' | 'detect_resolution' | 'image_resolution' | 'w' | 'h' | 'f' +>; + +/** + * The HED processor node, with parameters flagged as required + */ +export type RequiredHedImageProcessorInvocation = O.Required< + HedImageProcessorInvocation, + 'type' | 'detect_resolution' | 'image_resolution' | 'scribble' +>; + +/** + * The Lineart Anime processor node, with parameters flagged as required + */ +export type RequiredLineartAnimeImageProcessorInvocation = O.Required< + LineartAnimeImageProcessorInvocation, + 'type' | 'detect_resolution' | 'image_resolution' +>; + +/** + * The Lineart processor node, with parameters flagged as required + */ +export type RequiredLineartImageProcessorInvocation = O.Required< + LineartImageProcessorInvocation, + 'type' | 'detect_resolution' | 'image_resolution' | 'coarse' +>; + +/** + * The MediapipeFace processor node, with parameters flagged as required + */ +export type RequiredMediapipeFaceProcessorInvocation = O.Required< + MediapipeFaceProcessorInvocation, + 'type' | 'max_faces' | 'min_confidence' +>; + +/** + * The MidasDepth processor node, with parameters flagged as required + */ +export type RequiredMidasDepthImageProcessorInvocation = O.Required< + MidasDepthImageProcessorInvocation, + 'type' | 'a_mult' | 'bg_th' +>; + +/** + * The MLSD processor node, with parameters flagged as required + */ +export type RequiredMlsdImageProcessorInvocation = O.Required< + MlsdImageProcessorInvocation, + 'type' | 'detect_resolution' | 'image_resolution' | 'thr_v' | 'thr_d' +>; + +/** + * The NormalBae processor node, with parameters flagged as required + */ +export type RequiredNormalbaeImageProcessorInvocation = O.Required< + NormalbaeImageProcessorInvocation, + 'type' | 'detect_resolution' | 'image_resolution' +>; + +/** + * The Openpose processor node, with parameters flagged as required + */ +export type RequiredOpenposeImageProcessorInvocation = O.Required< + OpenposeImageProcessorInvocation, + 'type' | 'detect_resolution' | 'image_resolution' | 'hand_and_face' +>; + +/** + * The Pidi processor node, with parameters flagged as required + */ +export type RequiredPidiImageProcessorInvocation = O.Required< + PidiImageProcessorInvocation, + 'type' | 'detect_resolution' | 'image_resolution' | 'safe' | 'scribble' +>; + +/** + * The ZoeDepth processor node, with parameters flagged as required + */ +export type RequiredZoeDepthImageProcessorInvocation = O.Required< + ZoeDepthImageProcessorInvocation, + 'type' +>; + +/** + * Any ControlNet Processor node, with its parameters flagged as required + */ +export type RequiredControlNetProcessorNode = + | RequiredCannyImageProcessorInvocation + | RequiredContentShuffleImageProcessorInvocation + | RequiredHedImageProcessorInvocation + | RequiredLineartAnimeImageProcessorInvocation + | RequiredLineartImageProcessorInvocation + | RequiredMediapipeFaceProcessorInvocation + | RequiredMidasDepthImageProcessorInvocation + | RequiredMlsdImageProcessorInvocation + | RequiredNormalbaeImageProcessorInvocation + | RequiredOpenposeImageProcessorInvocation + | RequiredPidiImageProcessorInvocation + | RequiredZoeDepthImageProcessorInvocation; + +/** + * Type guard for CannyImageProcessorInvocation + */ +export const isCannyImageProcessorInvocation = ( + obj: unknown +): obj is CannyImageProcessorInvocation => { + if (isObject(obj) && 'type' in obj && obj.type === 'canny_image_processor') { + return true; + } + return false; +}; + +/** + * Type guard for ContentShuffleImageProcessorInvocation + */ +export const isContentShuffleImageProcessorInvocation = ( + obj: unknown +): obj is ContentShuffleImageProcessorInvocation => { + if ( + isObject(obj) && + 'type' in obj && + obj.type === 'content_shuffle_image_processor' + ) { + return true; + } + return false; +}; + +/** + * Type guard for HedImageprocessorInvocation + */ +export const isHedImageprocessorInvocation = ( + obj: unknown +): obj is HedImageProcessorInvocation => { + if (isObject(obj) && 'type' in obj && obj.type === 'hed_image_processor') { + return true; + } + return false; +}; + +/** + * Type guard for LineartAnimeImageProcessorInvocation + */ +export const isLineartAnimeImageProcessorInvocation = ( + obj: unknown +): obj is LineartAnimeImageProcessorInvocation => { + if ( + isObject(obj) && + 'type' in obj && + obj.type === 'lineart_anime_image_processor' + ) { + return true; + } + return false; +}; + +/** + * Type guard for LineartImageProcessorInvocation + */ +export const isLineartImageProcessorInvocation = ( + obj: unknown +): obj is LineartImageProcessorInvocation => { + if ( + isObject(obj) && + 'type' in obj && + obj.type === 'lineart_image_processor' + ) { + return true; + } + return false; +}; + +/** + * Type guard for MediapipeFaceProcessorInvocation + */ +export const isMediapipeFaceProcessorInvocation = ( + obj: unknown +): obj is MediapipeFaceProcessorInvocation => { + if ( + isObject(obj) && + 'type' in obj && + obj.type === 'mediapipe_face_processor' + ) { + return true; + } + return false; +}; + +/** + * Type guard for MidasDepthImageProcessorInvocation + */ +export const isMidasDepthImageProcessorInvocation = ( + obj: unknown +): obj is MidasDepthImageProcessorInvocation => { + if ( + isObject(obj) && + 'type' in obj && + obj.type === 'midas_depth_image_processor' + ) { + return true; + } + return false; +}; + +/** + * Type guard for MlsdImageProcessorInvocation + */ +export const isMlsdImageProcessorInvocation = ( + obj: unknown +): obj is MlsdImageProcessorInvocation => { + if (isObject(obj) && 'type' in obj && obj.type === 'mlsd_image_processor') { + return true; + } + return false; +}; + +/** + * Type guard for NormalbaeImageProcessorInvocation + */ +export const isNormalbaeImageProcessorInvocation = ( + obj: unknown +): obj is NormalbaeImageProcessorInvocation => { + if ( + isObject(obj) && + 'type' in obj && + obj.type === 'normalbae_image_processor' + ) { + return true; + } + return false; +}; + +/** + * Type guard for OpenposeImageProcessorInvocation + */ +export const isOpenposeImageProcessorInvocation = ( + obj: unknown +): obj is OpenposeImageProcessorInvocation => { + if ( + isObject(obj) && + 'type' in obj && + obj.type === 'openpose_image_processor' + ) { + return true; + } + return false; +}; + +/** + * Type guard for PidiImageProcessorInvocation + */ +export const isPidiImageProcessorInvocation = ( + obj: unknown +): obj is PidiImageProcessorInvocation => { + if (isObject(obj) && 'type' in obj && obj.type === 'pidi_image_processor') { + return true; + } + return false; +}; + +/** + * Type guard for ZoeDepthImageProcessorInvocation + */ +export const isZoeDepthImageProcessorInvocation = ( + obj: unknown +): obj is ZoeDepthImageProcessorInvocation => { + if ( + isObject(obj) && + 'type' in obj && + obj.type === 'zoe_depth_image_processor' + ) { + return true; + } + return false; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts index d52310abdd..9975d446a3 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts @@ -12,9 +12,7 @@ import { TextToLatentsInvocation, } from 'services/api'; import { NonNullableGraph } from 'features/nodes/types/types'; -import { forEach, map, size } from 'lodash-es'; -import { ControlNetProcessorNode } from 'features/controlNet/store/types'; -import { ControlNetModel } from 'features/controlNet/store/controlNetSlice'; +import { forEach, size } from 'lodash-es'; const POSITIVE_CONDITIONING = 'positive_conditioning'; const NEGATIVE_CONDITIONING = 'negative_conditioning'; @@ -344,7 +342,7 @@ export const buildTextToImageGraph = (state: RootState): Graph => { beginStepPct, endStepPct, model, - processor, + processorNode, weight, } = controlNet; From 2eb367969c40dc87bd8e81924ba83d11cc4b98d8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 2 Jun 2023 17:41:11 +1000 Subject: [PATCH 17/47] feat(ui): do not autoprocess control if invocation in progress --- .../enhancers/reduxRemember/unserialize.ts | 2 ++ .../listeners/controlNetImageProcessed.ts | 5 +++++ .../controlNetProcessorParamsChanged.ts | 22 ++++++++++++++++++- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/unserialize.ts b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/unserialize.ts index c6ae4946f2..c6af5f3612 100644 --- a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/unserialize.ts +++ b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/unserialize.ts @@ -1,4 +1,5 @@ import { initialCanvasState } from 'features/canvas/store/canvasSlice'; +import { initialControlNetState } from 'features/controlNet/store/controlNetSlice'; import { initialGalleryState } from 'features/gallery/store/gallerySlice'; import { initialImagesState } from 'features/gallery/store/imagesSlice'; import { initialLightboxState } from 'features/lightbox/store/lightboxSlice'; @@ -28,6 +29,7 @@ const initialStates: { ui: initialUIState, hotkeys: initialHotkeysState, images: initialImagesState, + controlNet: initialControlNetState, }; export const unserialize: UnserializeFunction = (data, key) => { diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts index 00cc2d2474..7d3def3a8f 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts @@ -72,6 +72,11 @@ export const addControlNetImageProcessedListener = () => { ); const processedControlImage = imageMetadataReceivedAction.payload; + moduleLog.debug( + { data: { arg: action.payload, processedControlImage } }, + 'ControlNet image processed' + ); + // Update the processed image in the store dispatch( controlNetProcessedImageChanged({ diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetProcessorParamsChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetProcessorParamsChanged.ts index 315b793e53..5f71c1ed56 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetProcessorParamsChanged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetProcessorParamsChanged.ts @@ -8,13 +8,33 @@ import { const moduleLog = log.child({ namespace: 'controlNet' }); +/** + * Listener that automatically processes a ControlNet image when its processor parameters are changed. + * + * The network request is debounced by 1 second. + */ export const addControlNetProcessorParamsChangedListener = () => { startAppListening({ predicate: (action) => controlNetProcessorParamsChanged.match(action) || controlNetProcessorTypeChanged.match(action), - effect: async (action, { dispatch, cancelActiveListeners, delay }) => { + effect: async ( + action, + { dispatch, getState, cancelActiveListeners, delay } + ) => { + const state = getState(); + if (!state.controlNet.shouldAutoProcess) { + // silently skip + return; + } + + if (state.system.isProcessing) { + moduleLog.trace('System busy, skipping ControlNet auto-processing'); + return; + } + const { controlNetId } = action.payload; + // Cancel any in-progress instances of this listener cancelActiveListeners(); From 3d99d7ae8be2a05d90f2d4c982b15c8901609217 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 2 Jun 2023 18:20:11 +1000 Subject: [PATCH 18/47] feat(ui): update handling of inProgess, do not allow cnet process when processing --- .../hooks/useIsReadyToInvoke.ts} | 9 +++-- .../controlNet/components/ControlNet.tsx | 27 ++++---------- .../components/ControlNetPreprocessButton.tsx | 36 +++++++++++++++++++ ...t.tsx => ControlNetProcessorComponent.tsx} | 4 +-- .../common/ControlNetProcessorButtons.tsx | 28 --------------- .../Core/ParamPositiveConditioning.tsx | 4 +-- .../ProcessButtons/InvokeButton.tsx | 4 +-- .../system/hooks/useIsApplicationReady.ts | 3 ++ 8 files changed, 58 insertions(+), 57 deletions(-) rename invokeai/frontend/web/src/{app/selectors/readinessSelector.ts => common/hooks/useIsReadyToInvoke.ts} (89%) create mode 100644 invokeai/frontend/web/src/features/controlNet/components/ControlNetPreprocessButton.tsx rename invokeai/frontend/web/src/features/controlNet/components/{ProcessorComponent.tsx => ControlNetProcessorComponent.tsx} (96%) delete mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorButtons.tsx diff --git a/invokeai/frontend/web/src/app/selectors/readinessSelector.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToInvoke.ts similarity index 89% rename from invokeai/frontend/web/src/app/selectors/readinessSelector.ts rename to invokeai/frontend/web/src/common/hooks/useIsReadyToInvoke.ts index 2b77fe9f47..7204205216 100644 --- a/invokeai/frontend/web/src/app/selectors/readinessSelector.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToInvoke.ts @@ -1,12 +1,12 @@ import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { validateSeedWeights } from 'common/util/seedWeightPairs'; import { generationSelector } from 'features/parameters/store/generationSelectors'; import { systemSelector } from 'features/system/store/systemSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { isEqual } from 'lodash-es'; -export const readinessSelector = createSelector( +const readinessSelector = createSelector( [generationSelector, systemSelector, activeTabNameSelector], (generation, system, activeTabName) => { const { @@ -60,3 +60,8 @@ export const readinessSelector = createSelector( }, defaultSelectorOptions ); + +export const useIsReadyToInvoke = () => { + const { isReady } = useAppSelector(readinessSelector); + return isReady; +}; diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx index b9b8e77fcc..6095e5e956 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx @@ -1,7 +1,5 @@ import { memo, useCallback } from 'react'; -import { RequiredControlNetProcessorNode } from '../store/types'; import { ImageDTO } from 'services/api'; -import CannyProcessor from './processors/CannyProcessor'; import { ControlNet, controlNetImageChanged, @@ -23,11 +21,11 @@ import { } from '@chakra-ui/react'; import IAISelectableImage from './parameters/IAISelectableImage'; import IAIButton from 'common/components/IAIButton'; -import { controlNetImageProcessed } from '../store/actions'; import { FaUndo } from 'react-icons/fa'; -import HedProcessor from './processors/HedProcessor'; import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect'; -import ProcessorComponent from './ProcessorComponent'; +import ControlNetProcessorComponent from './ControlNetProcessorComponent'; +import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady'; +import ControlNetPreprocessButton from './ControlNetPreprocessButton'; type ControlNetProps = { controlNet: ControlNet; @@ -47,6 +45,7 @@ const ControlNet = (props: ControlNetProps) => { processorNode, } = props.controlNet; const dispatch = useAppDispatch(); + const isReady = useIsApplicationReady(); const handleControlImageChanged = useCallback( (controlImage: ImageDTO) => { @@ -55,14 +54,6 @@ const ControlNet = (props: ControlNetProps) => { [controlNetId, dispatch] ); - const handleProcess = useCallback(() => { - dispatch( - controlNetImageProcessed({ - controlNetId, - }) - ); - }, [controlNetId, dispatch]); - const handleReset = useCallback(() => { dispatch( controlNetProcessedImageChanged({ @@ -128,17 +119,11 @@ const ControlNet = (props: ControlNetProps) => { controlNetId={controlNetId} processorNode={processorNode} /> - - - Preprocess - + } diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNetPreprocessButton.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNetPreprocessButton.tsx new file mode 100644 index 0000000000..94b1f86501 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNetPreprocessButton.tsx @@ -0,0 +1,36 @@ +import IAIButton from 'common/components/IAIButton'; +import { memo, useCallback } from 'react'; +import { ControlNet } from '../store/controlNetSlice'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { controlNetImageProcessed } from '../store/actions'; +import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke'; + +type Props = { + controlNet: ControlNet; +}; + +const ControlNetPreprocessButton = (props: Props) => { + const { controlNetId, controlImage } = props.controlNet; + const dispatch = useAppDispatch(); + const isReady = useIsReadyToInvoke(); + + const handleProcess = useCallback(() => { + dispatch( + controlNetImageProcessed({ + controlNetId, + }) + ); + }, [controlNetId, dispatch]); + + return ( + + Preprocess + + ); +}; + +export default memo(ControlNetPreprocessButton); diff --git a/invokeai/frontend/web/src/features/controlNet/components/ProcessorComponent.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorComponent.tsx similarity index 96% rename from invokeai/frontend/web/src/features/controlNet/components/ProcessorComponent.tsx rename to invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorComponent.tsx index 246aea70c7..4649f89b35 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ProcessorComponent.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorComponent.tsx @@ -18,7 +18,7 @@ export type ControlNetProcessorProps = { processorNode: RequiredControlNetProcessorNode; }; -const ProcessorComponent = (props: ControlNetProcessorProps) => { +const ControlNetProcessorComponent = (props: ControlNetProcessorProps) => { const { controlNetId, processorNode } = props; if (processorNode.type === 'canny_image_processor') { return ( @@ -128,4 +128,4 @@ const ProcessorComponent = (props: ControlNetProcessorProps) => { return null; }; -export default memo(ProcessorComponent); +export default memo(ControlNetProcessorComponent); diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorButtons.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorButtons.tsx deleted file mode 100644 index a051990f67..0000000000 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/common/ControlNetProcessorButtons.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Flex } from '@chakra-ui/react'; -import { memo } from 'react'; -import { FaUndo } from 'react-icons/fa'; -import IAIButton from 'common/components/IAIButton'; - -type ControlNetProcessorButtonsProps = { - handleProcess: () => void; - isProcessDisabled: boolean; - handleReset: () => void; - isResetDisabled: boolean; -}; - -const ControlNetProcessorButtons = (props: ControlNetProcessorButtonsProps) => { - const { handleProcess, isProcessDisabled, handleReset, isResetDisabled } = - props; - return ( - - ); -}; - -export default memo(ControlNetProcessorButtons); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx index 365bade0aa..0980b84ab3 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx @@ -4,7 +4,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { ChangeEvent, KeyboardEvent, useCallback, useRef } from 'react'; import { createSelector } from '@reduxjs/toolkit'; -import { readinessSelector } from 'app/selectors/readinessSelector'; import { GenerationState, clampSymmetrySteps, @@ -17,6 +16,7 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { userInvoked } from 'app/store/actions'; import IAITextarea from 'common/components/IAITextarea'; +import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke'; const promptInputSelector = createSelector( [(state: RootState) => state.generation, activeTabNameSelector], @@ -39,7 +39,7 @@ const promptInputSelector = createSelector( const ParamPositiveConditioning = () => { const dispatch = useAppDispatch(); const { prompt, activeTabName } = useAppSelector(promptInputSelector); - const { isReady } = useAppSelector(readinessSelector); + const isReady = useIsReadyToInvoke(); const promptRef = useRef(null); diff --git a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx index 4ada615628..449280aa03 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx @@ -1,11 +1,11 @@ import { Box } from '@chakra-ui/react'; -import { readinessSelector } from 'app/selectors/readinessSelector'; import { userInvoked } from 'app/store/actions'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIButton, { IAIButtonProps } from 'common/components/IAIButton'; import IAIIconButton, { IAIIconButtonProps, } from 'common/components/IAIIconButton'; +import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke'; import { clampSymmetrySteps } from 'features/parameters/store/generationSlice'; import ProgressBar from 'features/system/components/ProgressBar'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; @@ -22,7 +22,7 @@ interface InvokeButton export default function InvokeButton(props: InvokeButton) { const { iconButton = false, ...rest } = props; const dispatch = useAppDispatch(); - const { isReady } = useAppSelector(readinessSelector); + const isReady = useIsReadyToInvoke(); const activeTabName = useAppSelector(activeTabNameSelector); const handleInvoke = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/system/hooks/useIsApplicationReady.ts b/invokeai/frontend/web/src/features/system/hooks/useIsApplicationReady.ts index 6e62c3642b..193420e29c 100644 --- a/invokeai/frontend/web/src/features/system/hooks/useIsApplicationReady.ts +++ b/invokeai/frontend/web/src/features/system/hooks/useIsApplicationReady.ts @@ -19,6 +19,9 @@ const isApplicationReadySelector = createSelector( } ); +/** + * Checks if the application is ready to be used, i.e. if the initial startup process is finished. + */ export const useIsApplicationReady = () => { const { disabledTabs, wereModelsReceived, wasSchemaParsed } = useAppSelector( isApplicationReadySelector From fa290aff8d57fa6eb824727d490bb99f0ca073cc Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 2 Jun 2023 18:20:44 +1000 Subject: [PATCH 19/47] feat(ui): add defaults for all processors --- .../components/processors/CannyProcessor.tsx | 13 +++++- .../processors/ContentShuffleProcessor.tsx | 43 +++++++++++++++++++ .../components/processors/HedProcessor.tsx | 19 ++++++++ .../processors/LineartAnimeProcessor.tsx | 19 ++++++++ .../processors/LineartProcessor.tsx | 19 ++++++++ .../processors/MediapipeFaceProcessor.tsx | 20 +++++++-- .../processors/MidasDepthProcessor.tsx | 15 +++++++ .../processors/MlsdImageProcessor.tsx | 31 +++++++++++++ .../processors/NormalBaeProcessor.tsx | 19 ++++++++ .../processors/OpenposeProcessor.tsx | 19 ++++++++ .../components/processors/PidiProcessor.tsx | 19 ++++++++ .../features/controlNet/store/constants.ts | 2 +- .../controlNet/store/controlNetSlice.ts | 9 ++-- 13 files changed, 237 insertions(+), 10 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx index 872be06177..336d7d8bab 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx @@ -3,6 +3,9 @@ import IAISlider from 'common/components/IAISlider'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { RequiredCannyImageProcessorInvocation } from 'features/controlNet/store/types'; +import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; + +const DEFAULTS = CONTROLNET_PROCESSORS.canny_image_processor.default; type CannyProcessorProps = { controlNetId: string; @@ -22,7 +25,9 @@ const CannyProcessor = (props: CannyProcessorProps) => { ); const handleLowThresholdReset = useCallback(() => { - processorChanged(controlNetId, { low_threshold: 100 }); + processorChanged(controlNetId, { + low_threshold: DEFAULTS.low_threshold, + }); }, [controlNetId, processorChanged]); const handleHighThresholdChanged = useCallback( @@ -33,7 +38,9 @@ const CannyProcessor = (props: CannyProcessorProps) => { ); const handleHighThresholdReset = useCallback(() => { - processorChanged(controlNetId, { high_threshold: 200 }); + processorChanged(controlNetId, { + high_threshold: DEFAULTS.high_threshold, + }); }, [controlNetId, processorChanged]); return ( @@ -43,6 +50,7 @@ const CannyProcessor = (props: CannyProcessorProps) => { value={low_threshold} onChange={handleLowThresholdChanged} handleReset={handleLowThresholdReset} + withReset min={0} max={255} withInput @@ -52,6 +60,7 @@ const CannyProcessor = (props: CannyProcessorProps) => { value={high_threshold} onChange={handleHighThresholdChanged} handleReset={handleHighThresholdReset} + withReset min={0} max={255} withInput diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/ContentShuffleProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/ContentShuffleProcessor.tsx index 480275cd1c..0d8b85b89b 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/ContentShuffleProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/ContentShuffleProcessor.tsx @@ -3,6 +3,9 @@ import IAISlider from 'common/components/IAISlider'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { RequiredContentShuffleImageProcessorInvocation } from 'features/controlNet/store/types'; +import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; + +const DEFAULTS = CONTROLNET_PROCESSORS.content_shuffle_image_processor.default; type Props = { controlNetId: string; @@ -21,6 +24,12 @@ const ContentShuffleProcessor = (props: Props) => { [controlNetId, processorChanged] ); + const handleDetectResolutionReset = useCallback(() => { + processorChanged(controlNetId, { + detect_resolution: DEFAULTS.detect_resolution, + }); + }, [controlNetId, processorChanged]); + const handleImageResolutionChanged = useCallback( (v: number) => { processorChanged(controlNetId, { image_resolution: v }); @@ -28,6 +37,12 @@ const ContentShuffleProcessor = (props: Props) => { [controlNetId, processorChanged] ); + const handleImageResolutionReset = useCallback(() => { + processorChanged(controlNetId, { + image_resolution: DEFAULTS.image_resolution, + }); + }, [controlNetId, processorChanged]); + const handleWChanged = useCallback( (v: number) => { processorChanged(controlNetId, { w: v }); @@ -35,6 +50,12 @@ const ContentShuffleProcessor = (props: Props) => { [controlNetId, processorChanged] ); + const handleWReset = useCallback(() => { + processorChanged(controlNetId, { + w: DEFAULTS.w, + }); + }, [controlNetId, processorChanged]); + const handleHChanged = useCallback( (v: number) => { processorChanged(controlNetId, { h: v }); @@ -42,6 +63,12 @@ const ContentShuffleProcessor = (props: Props) => { [controlNetId, processorChanged] ); + const handleHReset = useCallback(() => { + processorChanged(controlNetId, { + h: DEFAULTS.h, + }); + }, [controlNetId, processorChanged]); + const handleFChanged = useCallback( (v: number) => { processorChanged(controlNetId, { f: v }); @@ -49,12 +76,20 @@ const ContentShuffleProcessor = (props: Props) => { [controlNetId, processorChanged] ); + const handleFReset = useCallback(() => { + processorChanged(controlNetId, { + f: DEFAULTS.f, + }); + }, [controlNetId, processorChanged]); + return ( { label="Image Resolution" value={image_resolution} onChange={handleImageResolutionChanged} + handleReset={handleImageResolutionReset} + withReset min={0} max={4096} withInput @@ -71,6 +108,8 @@ const ContentShuffleProcessor = (props: Props) => { label="W" value={w} onChange={handleWChanged} + handleReset={handleWReset} + withReset min={0} max={4096} withInput @@ -79,6 +118,8 @@ const ContentShuffleProcessor = (props: Props) => { label="H" value={h} onChange={handleHChanged} + handleReset={handleHReset} + withReset min={0} max={4096} withInput @@ -87,6 +128,8 @@ const ContentShuffleProcessor = (props: Props) => { label="F" value={f} onChange={handleFChanged} + handleReset={handleFReset} + withReset min={0} max={4096} withInput diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx index 22c5c487cd..23f79d69e2 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx @@ -4,6 +4,9 @@ import IAISwitch from 'common/components/IAISwitch'; import { ChangeEvent, memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { RequiredHedImageProcessorInvocation } from 'features/controlNet/store/types'; +import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; + +const DEFAULTS = CONTROLNET_PROCESSORS.hed_image_processor.default; type HedProcessorProps = { controlNetId: string; @@ -39,12 +42,26 @@ const HedPreprocessor = (props: HedProcessorProps) => { [controlNetId, processorChanged] ); + const handleDetectResolutionReset = useCallback(() => { + processorChanged(controlNetId, { + detect_resolution: DEFAULTS.detect_resolution, + }); + }, [controlNetId, processorChanged]); + + const handleImageResolutionReset = useCallback(() => { + processorChanged(controlNetId, { + image_resolution: DEFAULTS.image_resolution, + }); + }, [controlNetId, processorChanged]); + return ( { label="Image Resolution" value={image_resolution} onChange={handleImageResolutionChanged} + handleReset={handleImageResolutionReset} + withReset min={0} max={4096} withInput diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx index 87a21f95f0..1ccdcbd197 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx @@ -3,6 +3,9 @@ import IAISlider from 'common/components/IAISlider'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { RequiredLineartAnimeImageProcessorInvocation } from 'features/controlNet/store/types'; +import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; + +const DEFAULTS = CONTROLNET_PROCESSORS.lineart_anime_image_processor.default; type Props = { controlNetId: string; @@ -28,12 +31,26 @@ const LineartAnimeProcessor = (props: Props) => { [controlNetId, processorChanged] ); + const handleDetectResolutionReset = useCallback(() => { + processorChanged(controlNetId, { + detect_resolution: DEFAULTS.detect_resolution, + }); + }, [controlNetId, processorChanged]); + + const handleImageResolutionReset = useCallback(() => { + processorChanged(controlNetId, { + image_resolution: DEFAULTS.image_resolution, + }); + }, [controlNetId, processorChanged]); + return ( { label="Image Resolution" value={image_resolution} onChange={handleImageResolutionChanged} + handleReset={handleImageResolutionReset} + withReset min={0} max={4096} withInput diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx index 503fd73fa8..4376a0cbc3 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx @@ -4,6 +4,9 @@ import { ChangeEvent, memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { RequiredLineartImageProcessorInvocation } from 'features/controlNet/store/types'; import IAISwitch from 'common/components/IAISwitch'; +import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; + +const DEFAULTS = CONTROLNET_PROCESSORS.lineart_image_processor.default; type LineartProcessorProps = { controlNetId: string; @@ -29,6 +32,18 @@ const LineartProcessor = (props: LineartProcessorProps) => { [controlNetId, processorChanged] ); + const handleDetectResolutionReset = useCallback(() => { + processorChanged(controlNetId, { + detect_resolution: DEFAULTS.detect_resolution, + }); + }, [controlNetId, processorChanged]); + + const handleImageResolutionReset = useCallback(() => { + processorChanged(controlNetId, { + image_resolution: DEFAULTS.image_resolution, + }); + }, [controlNetId, processorChanged]); + const handleCoarseChanged = useCallback( (e: ChangeEvent) => { processorChanged(controlNetId, { coarse: e.target.checked }); @@ -42,6 +57,8 @@ const LineartProcessor = (props: LineartProcessorProps) => { label="Detect Resolution" value={detect_resolution} onChange={handleDetectResolutionChanged} + handleReset={handleDetectResolutionReset} + withReset min={0} max={4096} withInput @@ -50,6 +67,8 @@ const LineartProcessor = (props: LineartProcessorProps) => { label="Image Resolution" value={image_resolution} onChange={handleImageResolutionChanged} + handleReset={handleImageResolutionReset} + withReset min={0} max={4096} withInput diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/MediapipeFaceProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/MediapipeFaceProcessor.tsx index f76f00c3b7..9a044560cf 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/MediapipeFaceProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/MediapipeFaceProcessor.tsx @@ -2,10 +2,10 @@ import { Flex } from '@chakra-ui/react'; import IAISlider from 'common/components/IAISlider'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import { - RequiredContentShuffleImageProcessorInvocation, - RequiredMediapipeFaceProcessorInvocation, -} from 'features/controlNet/store/types'; +import { RequiredMediapipeFaceProcessorInvocation } from 'features/controlNet/store/types'; +import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; + +const DEFAULTS = CONTROLNET_PROCESSORS.mediapipe_face_processor.default; type Props = { controlNetId: string; @@ -31,12 +31,22 @@ const MediapipeFaceProcessor = (props: Props) => { [controlNetId, processorChanged] ); + const handleMaxFacesReset = useCallback(() => { + processorChanged(controlNetId, { max_faces: DEFAULTS.max_faces }); + }, [controlNetId, processorChanged]); + + const handleMinConfidenceReset = useCallback(() => { + processorChanged(controlNetId, { min_confidence: DEFAULTS.min_confidence }); + }, [controlNetId, processorChanged]); + return ( { label="Min Confidence" value={min_confidence} onChange={handleMinConfidenceChanged} + handleReset={handleMinConfidenceReset} + withReset min={0} max={1} step={0.01} diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/MidasDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/MidasDepthProcessor.tsx index 9a205f28b5..ece69e7f34 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/MidasDepthProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/MidasDepthProcessor.tsx @@ -3,6 +3,9 @@ import IAISlider from 'common/components/IAISlider'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { RequiredMidasDepthImageProcessorInvocation } from 'features/controlNet/store/types'; +import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; + +const DEFAULTS = CONTROLNET_PROCESSORS.midas_depth_image_processor.default; type Props = { controlNetId: string; @@ -28,12 +31,22 @@ const MidasDepthProcessor = (props: Props) => { [controlNetId, processorChanged] ); + const handleAMultReset = useCallback(() => { + processorChanged(controlNetId, { a_mult: DEFAULTS.a_mult }); + }, [controlNetId, processorChanged]); + + const handleBgThReset = useCallback(() => { + processorChanged(controlNetId, { bg_th: DEFAULTS.bg_th }); + }, [controlNetId, processorChanged]); + return ( { label="bg_th" value={bg_th} onChange={handleBgThChanged} + handleReset={handleBgThReset} + withReset min={0} max={20} step={0.01} diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/MlsdImageProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/MlsdImageProcessor.tsx index e33b2102d1..9b15935ea7 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/MlsdImageProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/MlsdImageProcessor.tsx @@ -3,6 +3,9 @@ import IAISlider from 'common/components/IAISlider'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { RequiredMlsdImageProcessorInvocation } from 'features/controlNet/store/types'; +import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; + +const DEFAULTS = CONTROLNET_PROCESSORS.mlsd_image_processor.default; type Props = { controlNetId: string; @@ -42,12 +45,34 @@ const MlsdImageProcessor = (props: Props) => { [controlNetId, processorChanged] ); + const handleDetectResolutionReset = useCallback(() => { + processorChanged(controlNetId, { + detect_resolution: DEFAULTS.detect_resolution, + }); + }, [controlNetId, processorChanged]); + + const handleImageResolutionReset = useCallback(() => { + processorChanged(controlNetId, { + image_resolution: DEFAULTS.image_resolution, + }); + }, [controlNetId, processorChanged]); + + const handleThrDReset = useCallback(() => { + processorChanged(controlNetId, { thr_d: DEFAULTS.thr_d }); + }, [controlNetId, processorChanged]); + + const handleThrVReset = useCallback(() => { + processorChanged(controlNetId, { thr_v: DEFAULTS.thr_v }); + }, [controlNetId, processorChanged]); + return ( { label="Image Resolution" value={image_resolution} onChange={handleImageResolutionChanged} + handleReset={handleImageResolutionReset} + withReset min={0} max={4096} withInput @@ -64,6 +91,8 @@ const MlsdImageProcessor = (props: Props) => { label="W" value={thr_d} onChange={handleThrDChanged} + handleReset={handleThrDReset} + withReset min={0} max={1} step={0.01} @@ -73,6 +102,8 @@ const MlsdImageProcessor = (props: Props) => { label="H" value={thr_v} onChange={handleThrVChanged} + handleReset={handleThrVReset} + withReset min={0} max={1} step={0.01} diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/NormalBaeProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/NormalBaeProcessor.tsx index 61836e7668..79b6885669 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/NormalBaeProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/NormalBaeProcessor.tsx @@ -3,6 +3,9 @@ import IAISlider from 'common/components/IAISlider'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { RequiredNormalbaeImageProcessorInvocation } from 'features/controlNet/store/types'; +import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; + +const DEFAULTS = CONTROLNET_PROCESSORS.normalbae_image_processor.default; type Props = { controlNetId: string; @@ -28,12 +31,26 @@ const NormalBaeProcessor = (props: Props) => { [controlNetId, processorChanged] ); + const handleDetectResolutionReset = useCallback(() => { + processorChanged(controlNetId, { + detect_resolution: DEFAULTS.detect_resolution, + }); + }, [controlNetId, processorChanged]); + + const handleImageResolutionReset = useCallback(() => { + processorChanged(controlNetId, { + image_resolution: DEFAULTS.image_resolution, + }); + }, [controlNetId, processorChanged]); + return ( { label="Image Resolution" value={image_resolution} onChange={handleImageResolutionChanged} + handleReset={handleImageResolutionReset} + withReset min={0} max={4096} withInput diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/OpenposeProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/OpenposeProcessor.tsx index 63556d4da4..40619a6d5f 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/OpenposeProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/OpenposeProcessor.tsx @@ -4,6 +4,9 @@ import { ChangeEvent, memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { RequiredOpenposeImageProcessorInvocation } from 'features/controlNet/store/types'; import IAISwitch from 'common/components/IAISwitch'; +import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; + +const DEFAULTS = CONTROLNET_PROCESSORS.openpose_image_processor.default; type Props = { controlNetId: string; @@ -29,6 +32,18 @@ const OpenposeProcessor = (props: Props) => { [controlNetId, processorChanged] ); + const handleDetectResolutionReset = useCallback(() => { + processorChanged(controlNetId, { + detect_resolution: DEFAULTS.detect_resolution, + }); + }, [controlNetId, processorChanged]); + + const handleImageResolutionReset = useCallback(() => { + processorChanged(controlNetId, { + image_resolution: DEFAULTS.image_resolution, + }); + }, [controlNetId, processorChanged]); + const handleHandAndFaceChanged = useCallback( (e: ChangeEvent) => { processorChanged(controlNetId, { hand_and_face: e.target.checked }); @@ -42,6 +57,8 @@ const OpenposeProcessor = (props: Props) => { label="Detect Resolution" value={detect_resolution} onChange={handleDetectResolutionChanged} + handleReset={handleDetectResolutionReset} + withReset min={0} max={4096} withInput @@ -50,6 +67,8 @@ const OpenposeProcessor = (props: Props) => { label="Image Resolution" value={image_resolution} onChange={handleImageResolutionChanged} + handleReset={handleImageResolutionReset} + withReset min={0} max={4096} withInput diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/PidiProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/PidiProcessor.tsx index 711d4930ab..a5e82ee8d0 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/PidiProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/PidiProcessor.tsx @@ -4,6 +4,9 @@ import { ChangeEvent, memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; import { RequiredPidiImageProcessorInvocation } from 'features/controlNet/store/types'; import IAISwitch from 'common/components/IAISwitch'; +import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; + +const DEFAULTS = CONTROLNET_PROCESSORS.pidi_image_processor.default; type Props = { controlNetId: string; @@ -29,6 +32,18 @@ const PidiProcessor = (props: Props) => { [controlNetId, processorChanged] ); + const handleDetectResolutionReset = useCallback(() => { + processorChanged(controlNetId, { + detect_resolution: DEFAULTS.detect_resolution, + }); + }, [controlNetId, processorChanged]); + + const handleImageResolutionReset = useCallback(() => { + processorChanged(controlNetId, { + image_resolution: DEFAULTS.image_resolution, + }); + }, [controlNetId, processorChanged]); + const handleScribbleChanged = useCallback( (e: ChangeEvent) => { processorChanged(controlNetId, { scribble: e.target.checked }); @@ -49,6 +64,8 @@ const PidiProcessor = (props: Props) => { label="Detect Resolution" value={detect_resolution} onChange={handleDetectResolutionChanged} + handleReset={handleDetectResolutionReset} + withReset min={0} max={4096} withInput @@ -57,6 +74,8 @@ const PidiProcessor = (props: Props) => { label="Image Resolution" value={image_resolution} onChange={handleImageResolutionChanged} + handleReset={handleImageResolutionReset} + withReset min={0} max={4096} withInput diff --git a/invokeai/frontend/web/src/features/controlNet/store/constants.ts b/invokeai/frontend/web/src/features/controlNet/store/constants.ts index d7a76b6a5e..a7e20a78d7 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/constants.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/constants.ts @@ -22,7 +22,7 @@ type ControlNetProcessorsDict = Record< * * TODO: Generate from the OpenAPI schema */ -export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = { +export const CONTROLNET_PROCESSORS = { canny_image_processor: { type: 'canny_image_processor', label: 'Canny', diff --git a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts index 674e130bdc..764bac2d88 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts @@ -4,6 +4,7 @@ import { RootState } from 'app/store/store'; import { ImageDTO } from 'services/api'; import { ControlNetProcessorType, + RequiredCannyImageProcessorInvocation, RequiredControlNetProcessorNode, } from './types'; import { CONTROLNET_PROCESSORS } from './constants'; @@ -30,7 +31,8 @@ export const initialControlNet: Omit = { controlImage: null, isControlImageProcessed: false, processedControlImage: null, - processorNode: CONTROLNET_PROCESSORS.canny_image_processor.default, + processorNode: CONTROLNET_PROCESSORS.canny_image_processor + .default as RequiredCannyImageProcessorInvocation, }; export type ControlNet = { @@ -180,8 +182,9 @@ export const controlNetSlice = createSlice({ }> ) => { const { controlNetId, processorType } = action.payload; - state.controlNets[controlNetId].processorNode = - CONTROLNET_PROCESSORS[processorType].default; + state.controlNets[controlNetId].processorNode = CONTROLNET_PROCESSORS[ + processorType + ].default as RequiredControlNetProcessorNode; }, shouldAutoProcessToggled: (state) => { state.shouldAutoProcess = !state.shouldAutoProcess; From 72b437180479cb96f086e4e98ddbe0fda34c7363 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 2 Jun 2023 21:30:21 +1000 Subject: [PATCH 20/47] feat(ui): control image auto-process --- .../middleware/listenerMiddleware/index.ts | 4 +- .../listeners/controlNetImageProcessed.ts | 5 +- .../controlNetProcessorParamsChanged.ts | 14 +- .../controlNet/components/ControlNet.tsx | 41 ++--- .../components/ControlNetImagePreview.tsx | 168 ++++++++++++++++++ .../parameters/IAISelectableImage.tsx | 26 ++- .../parameters/ParamControlNetBeginEnd.tsx | 123 +++++++++++++ .../ParamControlNetBeginStepPct.tsx | 20 ++- .../controlNet/store/controlNetSlice.ts | 16 ++ .../components/CurrentImagePreview.tsx | 20 ++- .../components/ImageFallbackSpinner.tsx | 3 +- 11 files changed, 395 insertions(+), 45 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginEnd.tsx 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 9d938755f0..8c6503521c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -71,7 +71,7 @@ import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSa import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingAreaImageListener'; import { addImageCategoriesChangedListener } from './listeners/imageCategoriesChanged'; import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed'; -import { addControlNetProcessorParamsChangedListener } from './listeners/controlNetProcessorParamsChanged'; +import { addControlNetAutoProcessListener } from './listeners/controlNetProcessorParamsChanged'; export const listenerMiddleware = createListenerMiddleware(); @@ -178,4 +178,4 @@ addImageCategoriesChangedListener(); // ControlNet addControlNetImageProcessedListener(); -addControlNetProcessorParamsChangedListener(); +addControlNetAutoProcessListener(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts index 7d3def3a8f..717417792c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts @@ -15,7 +15,10 @@ const moduleLog = log.child({ namespace: 'controlNet' }); export const addControlNetImageProcessedListener = () => { startAppListening({ actionCreator: controlNetImageProcessed, - effect: async (action, { dispatch, getState, take }) => { + effect: async ( + action, + { dispatch, getState, take, unsubscribe, subscribe } + ) => { const { controlNetId } = action.payload; const controlNet = getState().controlNet.controlNets[controlNetId]; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetProcessorParamsChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetProcessorParamsChanged.ts index 5f71c1ed56..11237c9d27 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetProcessorParamsChanged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetProcessorParamsChanged.ts @@ -2,6 +2,7 @@ import { startAppListening } from '..'; import { log } from 'app/logging/useLogger'; import { controlNetImageProcessed } from 'features/controlNet/store/actions'; import { + controlNetImageChanged, controlNetProcessorParamsChanged, controlNetProcessorTypeChanged, } from 'features/controlNet/store/controlNetSlice'; @@ -13,10 +14,11 @@ const moduleLog = log.child({ namespace: 'controlNet' }); * * The network request is debounced by 1 second. */ -export const addControlNetProcessorParamsChangedListener = () => { +export const addControlNetAutoProcessListener = () => { startAppListening({ predicate: (action) => controlNetProcessorParamsChanged.match(action) || + controlNetImageChanged.match(action) || controlNetProcessorTypeChanged.match(action), effect: async ( action, @@ -35,11 +37,19 @@ export const addControlNetProcessorParamsChangedListener = () => { const { controlNetId } = action.payload; + if (!state.controlNet.controlNets[controlNetId].controlImage) { + moduleLog.trace( + { data: { controlNetId } }, + 'No ControlNet image to auto-process' + ); + return; + } + // Cancel any in-progress instances of this listener cancelActiveListeners(); // Delay before starting actual work - await delay(1000); + await delay(300); dispatch(controlNetImageProcessed({ controlNetId })); }, diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx index 6095e5e956..47d8118036 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx @@ -1,18 +1,21 @@ -import { memo, useCallback } from 'react'; +import { memo, useCallback, useState } from 'react'; import { ImageDTO } from 'services/api'; import { ControlNet, controlNetImageChanged, controlNetProcessedImageChanged, controlNetRemoved, + controlNetSelector, } from '../store/controlNetSlice'; -import { useAppDispatch } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import ParamControlNetModel from './parameters/ParamControlNetModel'; import ParamControlNetWeight from './parameters/ParamControlNetWeight'; import ParamControlNetBeginStepPct from './parameters/ParamControlNetBeginStepPct'; import ParamControlNetEndStepPct from './parameters/ParamControlNetEndStepPct'; import { + Box, Flex, + Spinner, Tab, TabList, TabPanel, @@ -22,10 +25,15 @@ import { import IAISelectableImage from './parameters/IAISelectableImage'; import IAIButton from 'common/components/IAIButton'; import { FaUndo } from 'react-icons/fa'; +import { TbSquareToggle } from 'react-icons/tb'; import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect'; import ControlNetProcessorComponent from './ControlNetProcessorComponent'; -import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady'; import ControlNetPreprocessButton from './ControlNetPreprocessButton'; +import IAIIconButton from 'common/components/IAIIconButton'; +import ParamControlNetBeginEnd from './parameters/ParamControlNetBeginEnd'; +import ControlNetImagePreview from './ControlNetImagePreview'; +import { createSelector } from '@reduxjs/toolkit'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; type ControlNetProps = { controlNet: ControlNet; @@ -45,15 +53,6 @@ const ControlNet = (props: ControlNetProps) => { processorNode, } = props.controlNet; const dispatch = useAppDispatch(); - const isReady = useIsApplicationReady(); - - const handleControlImageChanged = useCallback( - (controlImage: ImageDTO) => { - dispatch(controlNetImageChanged({ controlNetId, controlImage })); - }, - [controlNetId, dispatch] - ); - const handleReset = useCallback(() => { dispatch( controlNetProcessedImageChanged({ @@ -63,21 +62,16 @@ const ControlNet = (props: ControlNetProps) => { ); }, [controlNetId, dispatch]); - const handleControlImageReset = useCallback(() => { - dispatch(controlNetImageChanged({ controlNetId, controlImage: null })); - }, [controlNetId, dispatch]); - const handleControlNetRemoved = useCallback(() => { dispatch(controlNetRemoved(controlNetId)); }, [controlNetId, dispatch]); return ( - { controlNetId={controlNetId} weight={weight} /> - - diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx new file mode 100644 index 0000000000..d9b0b9d63d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx @@ -0,0 +1,168 @@ +import { memo, useCallback, useState } from 'react'; +import { ImageDTO } from 'services/api'; +import { + controlNetImageChanged, + controlNetSelector, +} from '../store/controlNetSlice'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { Box, Flex, Spinner } from '@chakra-ui/react'; +import IAISelectableImage from './parameters/IAISelectableImage'; +import { TbSquareToggle } from 'react-icons/tb'; +import IAIIconButton from 'common/components/IAIIconButton'; +import { createSelector } from '@reduxjs/toolkit'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { AnimatePresence, motion } from 'framer-motion'; + +const selector = createSelector( + controlNetSelector, + (controlNet) => { + const { isProcessingControlImage } = controlNet; + return { isProcessingControlImage }; + }, + defaultSelectorOptions +); + +type Props = { + controlNetId: string; + controlImage: ImageDTO | null; + processedControlImage: ImageDTO | null; +}; + +const ControlNetImagePreview = (props: Props) => { + const { controlNetId, controlImage, processedControlImage } = props; + const dispatch = useAppDispatch(); + const { isProcessingControlImage } = useAppSelector(selector); + + const [shouldShowProcessedImage, setShouldShowProcessedImage] = + useState(true); + + const handleControlImageChanged = useCallback( + (controlImage: ImageDTO) => { + dispatch(controlNetImageChanged({ controlNetId, controlImage })); + }, + [controlNetId, dispatch] + ); + + const handleControlImageReset = useCallback(() => { + dispatch(controlNetImageChanged({ controlNetId, controlImage: null })); + }, [controlNetId, dispatch]); + + const shouldShowProcessedImageBackdrop = + Number(controlImage?.width) > Number(processedControlImage?.width) || + Number(controlImage?.height) > Number(processedControlImage?.height); + + return ( + + } + withResetIcon + resetIconSize="sm" + /> + + {controlImage && + processedControlImage && + shouldShowProcessedImage && + !isProcessingControlImage && ( + + + {shouldShowProcessedImageBackdrop && ( + + )} + + } + /> + + + + )} + + {isProcessingControlImage && ( + + + + )} + {processedControlImage && !isProcessingControlImage && ( + + } + size="sm" + onMouseOver={() => setShouldShowProcessedImage(false)} + onMouseOut={() => setShouldShowProcessedImage(true)} + /> + + )} + + ); +}; + +export default memo(ControlNetImagePreview); + +const ProcessedImageFallback = () => ( + + + +); diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx index 1f8fc89c33..fd5ffef28e 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx @@ -12,7 +12,7 @@ import IAIIconButton from 'common/components/IAIIconButton'; import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; import { useGetUrl } from 'common/util/getUrl'; import { AnimatePresence, motion } from 'framer-motion'; -import { SyntheticEvent } from 'react'; +import { ReactElement, SyntheticEvent } from 'react'; import { memo, useRef } from 'react'; import { FaImage, FaTimes } from 'react-icons/fa'; import { ImageDTO } from 'services/api'; @@ -26,14 +26,29 @@ type IAISelectableImageProps = { onReset?: () => void; onError?: (event: SyntheticEvent) => void; resetIconSize?: IconButtonProps['size']; + withResetIcon?: boolean; + withMetadataOverlay?: boolean; + isDropDisabled?: boolean; + fallback?: ReactElement; }; const IAISelectableImage = (props: IAISelectableImageProps) => { - const { image, onChange, onReset, onError, resetIconSize = 'md' } = props; + const { + image, + onChange, + onReset, + onError, + resetIconSize = 'md', + withResetIcon = false, + withMetadataOverlay = false, + isDropDisabled = false, + fallback = , + } = props; const droppableId = useRef(uuidv4()); const { getUrl } = useGetUrl(); const { isOver, setNodeRef, active } = useDroppable({ id: droppableId.current, + disabled: isDropDisabled, data: { handleDrop: onChange, }, @@ -54,6 +69,7 @@ const IAISelectableImage = (props: IAISelectableImageProps) => { { } + fallback={fallback} onError={onError} draggable={false} sx={{ borderRadius: 'base', }} /> - - {onReset && ( + {withMetadataOverlay && } + {onReset && withResetIcon && ( `${Math.round(v * 100)}%`; + +const ParamControlNetBeginEnd = (props: Props) => { + const { controlNetId, beginStepPct, endStepPct } = props; + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + + const handleStepPctChanged = useCallback( + (v: number[]) => { + dispatch( + controlNetBeginStepPctChanged({ controlNetId, beginStepPct: v[0] }) + ); + dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct: v[1] })); + }, + [controlNetId, dispatch] + ); + + const handleStepPctReset = useCallback(() => { + dispatch(controlNetBeginStepPctChanged({ controlNetId, beginStepPct: 0 })); + dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct: 1 })); + }, [controlNetId, dispatch]); + + return ( + + Begin & End Step % + + + + + + + + + + + + + 0% + + + 50% + + + 100% + + + + } + onClick={handleStepPctReset} + /> + + + ); +}; + +export default memo(ParamControlNetBeginEnd); diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginStepPct.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginStepPct.tsx index 914bfa2818..d94db5e272 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginStepPct.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginStepPct.tsx @@ -1,6 +1,9 @@ import { useAppDispatch } from 'app/store/storeHooks'; import IAISlider from 'common/components/IAISlider'; -import { controlNetBeginStepPctChanged } from 'features/controlNet/store/controlNetSlice'; +import { + controlNetBeginStepPctChanged, + controlNetEndStepPctChanged, +} from 'features/controlNet/store/controlNetSlice'; import { memo, useCallback } from 'react'; type ParamControlNetBeginStepPctProps = { @@ -21,9 +24,20 @@ const ParamControlNetBeginStepPct = ( [controlNetId, dispatch] ); - const handleBeginStepPctReset = () => { + const handleBeginStepPctReset = useCallback(() => { dispatch(controlNetBeginStepPctChanged({ controlNetId, beginStepPct: 0 })); - }; + }, [controlNetId, dispatch]); + + const handleEndStepPctChanged = useCallback( + (endStepPct: number) => { + dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct })); + }, + [controlNetId, dispatch] + ); + + const handleEndStepPctReset = useCallback(() => { + dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct: 0 })); + }, [controlNetId, dispatch]); return ( ; isEnabled: boolean; shouldAutoProcess: boolean; + isProcessingControlImage: boolean; }; export const initialControlNetState: ControlNetState = { controlNets: {}, isEnabled: false, shouldAutoProcess: true, + isProcessingControlImage: false, }; export const controlNetSlice = createSlice({ @@ -107,6 +110,9 @@ export const controlNetSlice = createSlice({ const { controlNetId, controlImage } = action.payload; state.controlNets[controlNetId].controlImage = controlImage; state.controlNets[controlNetId].processedControlImage = null; + if (state.shouldAutoProcess && controlImage !== null) { + state.isProcessingControlImage = true; + } }, isControlNetImageProcessedToggled: ( state, @@ -128,6 +134,7 @@ export const controlNetSlice = createSlice({ const { controlNetId, processedControlImage } = action.payload; state.controlNets[controlNetId].processedControlImage = processedControlImage; + state.isProcessingControlImage = false; }, controlNetModelChanged: ( state, @@ -190,6 +197,15 @@ export const controlNetSlice = createSlice({ state.shouldAutoProcess = !state.shouldAutoProcess; }, }, + extraReducers: (builder) => { + builder.addCase(controlNetImageProcessed, (state, action) => { + if ( + state.controlNets[action.payload.controlNetId].controlImage !== null + ) { + state.isProcessingControlImage = true; + } + }); + }, }); export const { diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx index f8194f5ad4..f97e151a94 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx @@ -8,7 +8,7 @@ import { isEqual } from 'lodash-es'; import { gallerySelector } from '../store/gallerySelectors'; import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer'; import NextPrevImageButtons from './NextPrevImageButtons'; -import { DragEvent, memo, useCallback } from 'react'; +import { DragEvent, memo, useCallback, useEffect, useState } from 'react'; import { systemSelector } from 'features/system/store/systemSelectors'; import ImageFallbackSpinner from './ImageFallbackSpinner'; import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; @@ -55,6 +55,7 @@ const CurrentImagePreview = () => { const { getUrl } = useGetUrl(); const toaster = useAppToaster(); const dispatch = useAppDispatch(); + const [isLoaded, setIsLoaded] = useState(false); const { attributes, listeners, setNodeRef } = useDraggable({ id: `currentImage_${image?.image_name}`, @@ -74,11 +75,15 @@ const CurrentImagePreview = () => { } }, [dispatch, toaster, shouldFetchImages]); + useEffect(() => { + setIsLoaded(false); + }, [image]); + return ( { height={progressImage.height} sx={{ objectFit: 'contain', - maxWidth: '100%', - maxHeight: '100%', + maxWidth: 'full', + maxHeight: 'full', height: 'auto', position: 'absolute', borderRadius: 'base', @@ -124,8 +129,11 @@ const CurrentImagePreview = () => { touchAction: 'none', }} onError={handleError} + onLoad={() => { + setIsLoaded(true); + }} /> - + {isLoaded && } ) )} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageFallbackSpinner.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageFallbackSpinner.tsx index 3d4a0d6911..fd603d3756 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageFallbackSpinner.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageFallbackSpinner.tsx @@ -14,7 +14,8 @@ const ImageFallbackSpinner = (props: ImageFallbackSpinnerProps) => { justifyContent: 'center', position: 'absolute', color: 'base.400', - minH: 40, + minH: 36, + minW: 36, }} > From 6b824eb112021eb210cc309eb8e214cb8a697a6d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 3 Jun 2023 11:13:33 +1000 Subject: [PATCH 21/47] feat(ui): initial mini controlnet UI, dnd improvements --- invokeai/frontend/web/package.json | 1 + .../components/ImageDnd/ImageDndContext.tsx | 5 +- .../components/ImageDnd/OverlayDragImage.tsx | 30 +++-- .../frontend/web/src/app/types/invokeai.ts | 1 + .../src/common/components/IAICustomSelect.tsx | 2 +- .../common/components/IAIImageFallback.tsx | 27 ++++ .../web/src/common/components/IAISlider.tsx | 10 +- .../controlNet/components/ControlNet.tsx | 16 +-- .../components/ControlNetImagePreview.tsx | 63 +++------- .../controlNet/components/ControlNetMini.tsx | 101 +++++++++++++++ .../parameters/IAISelectableImage.tsx | 118 ++++++++++-------- .../parameters/ParamControlNetBeginEnd.tsx | 98 ++++++++------- .../parameters/ParamControlNetWeight.tsx | 17 ++- .../components/CurrentImagePreview.tsx | 73 +++++------ .../fields/ImageInputFieldComponent.tsx | 6 +- .../ControlNet/ParamControlNetCollapse.tsx | 16 +++ .../ImageToImage/InitialImagePreview.tsx | 8 +- invokeai/frontend/web/yarn.lock | 8 ++ 18 files changed, 377 insertions(+), 223 deletions(-) create mode 100644 invokeai/frontend/web/src/common/components/IAIImageFallback.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/ControlNetMini.tsx diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 104fad3364..a9d0bfba7e 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -61,6 +61,7 @@ "@chakra-ui/theme-tools": "^2.0.16", "@dagrejs/graphlib": "^2.1.12", "@dnd-kit/core": "^6.0.8", + "@dnd-kit/modifiers": "^6.0.1", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@floating-ui/react-dom": "^2.0.0", diff --git a/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx b/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx index 6c76731d4c..72487f329c 100644 --- a/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx +++ b/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx @@ -6,6 +6,7 @@ import { KeyboardSensor, MouseSensor, TouchSensor, + pointerWithin, useSensor, useSensors, } from '@dnd-kit/core'; @@ -13,6 +14,7 @@ import { PropsWithChildren, memo, useCallback, useState } from 'react'; import OverlayDragImage from './OverlayDragImage'; import { ImageDTO } from 'services/api'; import { isImageDTO } from 'services/types/guards'; +import { snapCenterToCursor } from '@dnd-kit/modifiers'; type ImageDndContextProps = PropsWithChildren; @@ -53,9 +55,10 @@ const ImageDndContext = (props: ImageDndContextProps) => { onDragStart={handleDragStart} onDragEnd={handleDragEnd} sensors={sensors} + collisionDetection={pointerWithin} > {props.children} - + {draggedImage && } diff --git a/invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx b/invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx index 25a5fe2449..deec1e96d2 100644 --- a/invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx +++ b/invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx @@ -1,4 +1,4 @@ -import { Image } from '@chakra-ui/react'; +import { Box, Image } from '@chakra-ui/react'; import { memo } from 'react'; import { ImageDTO } from 'services/api'; @@ -8,15 +8,27 @@ type OverlayDragImageProps = { const OverlayDragImage = (props: OverlayDragImageProps) => { return ( - + > + + ); }; diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index 8081ffa491..304b094749 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -95,6 +95,7 @@ export type AppFeature = * A disable-able Stable Diffusion feature */ export type SDFeature = + | 'controlNet' | 'noise' | 'variation' | 'symmetry' diff --git a/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx b/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx index 5047a24c63..548b4d73e0 100644 --- a/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx +++ b/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx @@ -109,7 +109,7 @@ const IAICustomSelect = (props: IAICustomSelectProps) => { top: 0, left: 0, flexDirection: 'column', - zIndex: 1, + zIndex: 2, bg: 'base.800', borderRadius: 'base', border: '1px', diff --git a/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx new file mode 100644 index 0000000000..3d34fbca9e --- /dev/null +++ b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx @@ -0,0 +1,27 @@ +import { Flex, FlexProps, Spinner, SpinnerProps } from '@chakra-ui/react'; + +type Props = FlexProps & { + spinnerProps?: SpinnerProps; +}; + +export const IAIImageFallback = (props: Props) => { + const { spinnerProps, ...rest } = props; + const { sx, ...restFlexProps } = rest; + return ( + + + + ); +}; diff --git a/invokeai/frontend/web/src/common/components/IAISlider.tsx b/invokeai/frontend/web/src/common/components/IAISlider.tsx index a2a3251f02..2777e35967 100644 --- a/invokeai/frontend/web/src/common/components/IAISlider.tsx +++ b/invokeai/frontend/web/src/common/components/IAISlider.tsx @@ -40,7 +40,7 @@ import IAIIconButton, { IAIIconButtonProps } from './IAIIconButton'; import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; export type IAIFullSliderProps = { - label: string; + label?: string; value: number; min?: number; max?: number; @@ -178,9 +178,11 @@ const IAISlider = (props: IAIFullSliderProps) => { isDisabled={isDisabled} {...sliderFormControlProps} > - - {label} - + {label && ( + + {label} + + )} { [controlNetId, dispatch] ); - const handleControlImageReset = useCallback(() => { - dispatch(controlNetImageChanged({ controlNetId, controlImage: null })); - }, [controlNetId, dispatch]); - const shouldShowProcessedImageBackdrop = Number(controlImage?.width) > Number(processedControlImage?.width) || Number(controlImage?.height) > Number(processedControlImage?.height); return ( - - setShouldShowProcessedImage(false)} + onMouseOut={() => setShouldShowProcessedImage(true)} + > + } - withResetIcon - resetIconSize="sm" /> {controlImage && @@ -108,13 +103,10 @@ const ControlNetImagePreview = (props: Props) => { h: 'full', }} > - } + onDrop={handleControlImageChanged} + payloadImage={controlImage} /> @@ -131,18 +123,7 @@ const ControlNetImagePreview = (props: Props) => { h: 'full', }} > - - - )} - {processedControlImage && !isProcessingControlImage && ( - - } - size="sm" - onMouseOver={() => setShouldShowProcessedImage(false)} - onMouseOut={() => setShouldShowProcessedImage(true)} - /> + )} @@ -150,19 +131,3 @@ const ControlNetImagePreview = (props: Props) => { }; export default memo(ControlNetImagePreview); - -const ProcessedImageFallback = () => ( - - - -); diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNetMini.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNetMini.tsx new file mode 100644 index 0000000000..7d7b329b71 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNetMini.tsx @@ -0,0 +1,101 @@ +import { memo, useCallback } from 'react'; +import { + ControlNet, + controlNetProcessedImageChanged, + controlNetRemoved, +} from '../store/controlNetSlice'; +import { useAppDispatch } from 'app/store/storeHooks'; +import ParamControlNetModel from './parameters/ParamControlNetModel'; +import ParamControlNetWeight from './parameters/ParamControlNetWeight'; +import { + Box, + Flex, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, + Text, +} from '@chakra-ui/react'; +import IAIButton from 'common/components/IAIButton'; +import { FaUndo } from 'react-icons/fa'; +import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect'; +import ControlNetProcessorComponent from './ControlNetProcessorComponent'; +import ControlNetPreprocessButton from './ControlNetPreprocessButton'; +import ParamControlNetBeginEnd from './parameters/ParamControlNetBeginEnd'; +import ControlNetImagePreview from './ControlNetImagePreview'; + +type ControlNetProps = { + controlNet: ControlNet; +}; + +const ControlNet = (props: ControlNetProps) => { + const { + controlNetId, + isEnabled, + model, + weight, + beginStepPct, + endStepPct, + controlImage, + isControlImageProcessed, + processedControlImage, + processorNode, + } = props.controlNet; + const dispatch = useAppDispatch(); + const handleReset = useCallback(() => { + dispatch( + controlNetProcessedImageChanged({ + controlNetId, + processedControlImage: null, + }) + ); + }, [controlNetId, dispatch]); + + const handleControlNetRemoved = useCallback(() => { + dispatch(controlNetRemoved(controlNetId)); + }, [controlNetId, dispatch]); + + return ( + + + + + + + + + + + ); +}; + +export default memo(ControlNet); diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx index fd5ffef28e..26da39baf2 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx @@ -4,11 +4,12 @@ import { Icon, IconButtonProps, Image, - Spinner, Text, } from '@chakra-ui/react'; -import { useDroppable } from '@dnd-kit/core'; +import { useDraggable, useDroppable } from '@dnd-kit/core'; +import { useCombinedRefs } from '@dnd-kit/utilities'; import IAIIconButton from 'common/components/IAIIconButton'; +import { IAIImageFallback } from 'common/components/IAIImageFallback'; import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; import { useGetUrl } from 'common/util/getUrl'; import { AnimatePresence, motion } from 'framer-motion'; @@ -18,42 +19,65 @@ import { FaImage, FaTimes } from 'react-icons/fa'; import { ImageDTO } from 'services/api'; import { v4 as uuidv4 } from 'uuid'; -const PLACEHOLDER_MIN_HEIGHT = 48; +const PLACEHOLDER_MIN_HEIGHT = 36; type IAISelectableImageProps = { image: ImageDTO | null | undefined; - onChange: (image: ImageDTO) => void; + onDrop: (image: ImageDTO) => void; onReset?: () => void; onError?: (event: SyntheticEvent) => void; + onLoad?: (event: SyntheticEvent) => void; resetIconSize?: IconButtonProps['size']; withResetIcon?: boolean; withMetadataOverlay?: boolean; + isDragDisabled?: boolean; isDropDisabled?: boolean; fallback?: ReactElement; + payloadImage?: ImageDTO | null | undefined; }; -const IAISelectableImage = (props: IAISelectableImageProps) => { +const IAIDndImage = (props: IAISelectableImageProps) => { const { image, - onChange, + onDrop, onReset, onError, resetIconSize = 'md', withResetIcon = false, withMetadataOverlay = false, isDropDisabled = false, - fallback = , + isDragDisabled = false, + fallback = , + payloadImage, } = props; - const droppableId = useRef(uuidv4()); + const dndId = useRef(uuidv4()); const { getUrl } = useGetUrl(); - const { isOver, setNodeRef, active } = useDroppable({ - id: droppableId.current, + const { + isOver, + setNodeRef: setDroppableRef, + active, + } = useDroppable({ + id: dndId.current, disabled: isDropDisabled, data: { - handleDrop: onChange, + handleDrop: onDrop, }, }); + const { + attributes, + listeners, + setNodeRef: setDraggableRef, + } = useDraggable({ + id: dndId.current, + data: { + image: payloadImage ? payloadImage : image, + }, + disabled: isDragDisabled, + }); + + const setNodeRef = useCombinedRefs(setDroppableRef, setDraggableRef); + return ( { alignItems: 'center', justifyContent: 'center', position: 'relative', + minW: 36, + minH: 36, }} + {...attributes} + {...listeners} ref={setNodeRef} > {image && ( @@ -80,8 +108,11 @@ const IAISelectableImage = (props: IAISelectableImageProps) => { fallbackStrategy="beforeLoadOrError" fallback={fallback} onError={onError} + objectFit="contain" draggable={false} sx={{ + maxW: 'full', + maxH: 'full', borderRadius: 'base', }} /> @@ -139,7 +170,7 @@ const IAISelectableImage = (props: IAISelectableImageProps) => { ); }; -export default memo(IAISelectableImage); +export default memo(IAIDndImage); type DropOverlayProps = { isOver: boolean; @@ -179,14 +210,15 @@ const DropOverlay = (props: DropOverlayProps) => { w: 'full', h: 'full', bg: 'base.900', - opacity: isOver ? 0.9 : 0.7, + opacity: 0.7, borderRadius: 'base', alignItems: 'center', justifyContent: 'center', transitionProperty: 'common', - transitionDuration: '0.15s', + transitionDuration: '0.1s', }} /> + { left: 0, w: 'full', h: 'full', - opacity: isOver ? 1 : 0.9, - alignItems: 'center', - justifyContent: 'center', - transitionProperty: 'common', - transitionDuration: '0.15s', - }} - > - - Drop Image - - - + > + + Drop + + ); }; - -const ImageFallback = () => ( - - - -); 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 fa7047126d..e258d4cf29 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginEnd.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginEnd.tsx @@ -23,12 +23,13 @@ type Props = { controlNetId: string; beginStepPct: number; endStepPct: number; + mini?: boolean; }; const formatPct = (v: number) => `${Math.round(v * 100)}%`; const ParamControlNetBeginEnd = (props: Props) => { - const { controlNetId, beginStepPct, endStepPct } = props; + const { controlNetId, beginStepPct, endStepPct, mini = false } = props; const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -69,52 +70,59 @@ const ParamControlNetBeginEnd = (props: Props) => { - - 0% - - - 50% - - - 100% - + {!mini && ( + <> + {' '} + + 0% + + + 50% + + + 100% + + + )} - } - onClick={handleStepPctReset} - /> + {!mini && ( + } + onClick={handleStepPctReset} + /> + )} ); diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetWeight.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetWeight.tsx index 11272582d0..007ef355c3 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetWeight.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetWeight.tsx @@ -6,10 +6,11 @@ import { memo, useCallback } from 'react'; type ParamControlNetWeightProps = { controlNetId: string; weight: number; + mini?: boolean; }; const ParamControlNetWeight = (props: ParamControlNetWeightProps) => { - const { controlNetId, weight } = props; + const { controlNetId, weight, mini = false } = props; const dispatch = useAppDispatch(); const handleWeightChanged = useCallback( @@ -23,6 +24,20 @@ const ParamControlNetWeight = (props: ParamControlNetWeightProps) => { dispatch(controlNetWeightChanged({ controlNetId, weight: 1 })); }; + if (mini) { + return ( + + ); + } + return ( { shouldAntialiasProgressImage, } = useAppSelector(imagesSelector); const { shouldFetchImages } = useAppSelector(configSelector); - const { getUrl } = useGetUrl(); const toaster = useAppToaster(); const dispatch = useAppDispatch(); - const [isLoaded, setIsLoaded] = useState(false); - - const { attributes, listeners, setNodeRef } = useDraggable({ - id: `currentImage_${image?.image_name}`, - data: { - image, - }, - }); const handleError = useCallback(() => { dispatch(imageSelected()); @@ -75,9 +65,12 @@ const CurrentImagePreview = () => { } }, [dispatch, toaster, shouldFetchImages]); - useEffect(() => { - setIsLoaded(false); - }, [image]); + const handleDrop = useCallback( + (droppedImage: ImageDTO) => { + dispatch(imageSelected(droppedImage)); + }, + [dispatch] + ); return ( { image && ( - } - sx={{ - objectFit: 'contain', - maxWidth: '100%', - maxHeight: '100%', - height: 'auto', - borderRadius: 'base', - touchAction: 'none', - }} + { - setIsLoaded(true); - }} + fallback={} /> - {isLoaded && } ) )} - {shouldShowImageDetails && image && 'metadata' in image && ( + {shouldShowImageDetails && image && image.metadata && ( { )} - {!shouldShowImageDetails && } + {!shouldShowImageDetails && ( + + + + )} ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx index 9889ade2f3..c83a0b4a40 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx @@ -8,7 +8,7 @@ import { import { memo, useCallback } from 'react'; import { FieldComponentProps } from './types'; -import IAISelectableImage from 'features/controlNet/components/parameters/IAISelectableImage'; +import IAIDndImage from 'features/controlNet/components/parameters/IAISelectableImage'; import { ImageDTO } from 'services/api'; import { Flex } from '@chakra-ui/react'; @@ -51,9 +51,9 @@ const ImageInputFieldComponent = ( justifyContent: 'center', }} > - diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx index a3f91fd432..2359e5123c 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx @@ -24,6 +24,8 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { map, startCase } from 'lodash-es'; import { v4 as uuidv4 } from 'uuid'; import { CloseIcon } from '@chakra-ui/icons'; +import ControlNetMini from 'features/controlNet/components/ControlNetMini'; +import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; const selector = createSelector( controlNetSelector, @@ -38,6 +40,7 @@ const selector = createSelector( const ParamControlNetCollapse = () => { const { t } = useTranslation(); const { controlNetsArray, isEnabled } = useAppSelector(selector); + const isControlNetDisabled = useFeatureStatus('controlNet').isFeatureDisabled; const dispatch = useAppDispatch(); const handleClickControlNetToggle = useCallback(() => { @@ -48,6 +51,18 @@ const ParamControlNetCollapse = () => { dispatch(controlNetAdded({ controlNetId: uuidv4() })); }, [dispatch]); + if (isControlNetDisabled) { + return null; + } + + return ( + <> + {controlNetsArray.map((c) => ( + + ))} + + ); + return ( { {controlNetsArray.map((c) => ( + {/* */} ))} diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx index 2a0ed4ab5d..d8687581d6 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx @@ -12,8 +12,9 @@ import { generationSelector } from 'features/parameters/store/generationSelector import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { configSelector } from '../../../../system/store/configSelectors'; import { useAppToaster } from 'app/components/Toaster'; -import IAISelectableImage from 'features/controlNet/components/parameters/IAISelectableImage'; +import IAIDndImage from 'features/controlNet/components/parameters/IAISelectableImage'; import { ImageDTO } from 'services/api'; +import { IAIImageFallback } from 'common/components/IAIImageFallback'; const selector = createSelector( [generationSelector], @@ -73,10 +74,11 @@ const InitialImagePreview = () => { justifyContent: 'center', }} > - } /> ); diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock index e3b2978457..a4c9bf3633 100644 --- a/invokeai/frontend/web/yarn.lock +++ b/invokeai/frontend/web/yarn.lock @@ -953,6 +953,14 @@ "@dnd-kit/utilities" "^3.2.1" tslib "^2.0.0" +"@dnd-kit/modifiers@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz#9e39b25fd6e323659604cc74488fe044d33188c8" + integrity sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A== + dependencies: + "@dnd-kit/utilities" "^3.2.1" + tslib "^2.0.0" + "@dnd-kit/utilities@^3.2.1": version "3.2.1" resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.1.tgz#53f9e2016fd2506ec49e404c289392cfff30332a" From d92c7f5483389210314803fe6df90e18007e98ba Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 3 Jun 2023 11:17:02 +1000 Subject: [PATCH 22/47] feat(ui): organize IAIDndImage component --- .../components/IAIDndImage.tsx} | 15 ++++++++------- .../components/ControlNetImagePreview.tsx | 2 +- .../gallery/components/CurrentImagePreview.tsx | 2 +- .../fields/ImageInputFieldComponent.tsx | 2 +- .../ImageToImage/InitialImagePreview.tsx | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) rename invokeai/frontend/web/src/{features/controlNet/components/parameters/IAISelectableImage.tsx => common/components/IAIDndImage.tsx} (96%) diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx similarity index 96% rename from invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx rename to invokeai/frontend/web/src/common/components/IAIDndImage.tsx index 26da39baf2..a6667e73be 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx +++ b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx @@ -19,9 +19,7 @@ import { FaImage, FaTimes } from 'react-icons/fa'; import { ImageDTO } from 'services/api'; import { v4 as uuidv4 } from 'uuid'; -const PLACEHOLDER_MIN_HEIGHT = 36; - -type IAISelectableImageProps = { +type IAIDndImageProps = { image: ImageDTO | null | undefined; onDrop: (image: ImageDTO) => void; onReset?: () => void; @@ -34,9 +32,10 @@ type IAISelectableImageProps = { isDropDisabled?: boolean; fallback?: ReactElement; payloadImage?: ImageDTO | null | undefined; + minSize?: number; }; -const IAIDndImage = (props: IAISelectableImageProps) => { +const IAIDndImage = (props: IAIDndImageProps) => { const { image, onDrop, @@ -49,6 +48,7 @@ const IAIDndImage = (props: IAISelectableImageProps) => { isDragDisabled = false, fallback = , payloadImage, + minSize = 36, } = props; const dndId = useRef(uuidv4()); const { getUrl } = useGetUrl(); @@ -86,8 +86,9 @@ const IAIDndImage = (props: IAISelectableImageProps) => { alignItems: 'center', justifyContent: 'center', position: 'relative', - minW: 36, - minH: 36, + minW: minSize, + minH: minSize, + userSelect: 'none', }} {...attributes} {...listeners} @@ -144,7 +145,7 @@ const IAIDndImage = (props: IAISelectableImageProps) => { <> Date: Sat, 3 Jun 2023 12:41:31 +1000 Subject: [PATCH 23/47] feat(ui): add alpha colors --- .../frontend/web/src/theme/colors/greenTea.ts | 6 +++ .../frontend/web/src/theme/colors/invokeAI.ts | 6 +++ .../web/src/theme/colors/lightTheme.ts | 6 +++ .../web/src/theme/colors/oceanBlue.ts | 6 +++ .../frontend/web/src/theme/themeTypes.d.ts | 6 +++ .../src/theme/util/generateColorPalette.ts | 41 +++++++------------ 6 files changed, 45 insertions(+), 26 deletions(-) diff --git a/invokeai/frontend/web/src/theme/colors/greenTea.ts b/invokeai/frontend/web/src/theme/colors/greenTea.ts index 06476c0513..ffecbf2ffa 100644 --- a/invokeai/frontend/web/src/theme/colors/greenTea.ts +++ b/invokeai/frontend/web/src/theme/colors/greenTea.ts @@ -3,10 +3,16 @@ import { generateColorPalette } from '../util/generateColorPalette'; export const greenTeaThemeColors: InvokeAIThemeColors = { base: generateColorPalette(223, 10), + baseAlpha: generateColorPalette(223, 10, false, true), accent: generateColorPalette(155, 80), + accentAlpha: generateColorPalette(155, 80, false, true), working: generateColorPalette(47, 68), + workingAlpha: generateColorPalette(47, 68, false, true), warning: generateColorPalette(28, 75), + warningAlpha: generateColorPalette(28, 75, false, true), ok: generateColorPalette(122, 49), + okAlpha: generateColorPalette(122, 49, false, true), error: generateColorPalette(0, 50), + errorAlpha: generateColorPalette(0, 50, false, true), gridLineColor: 'rgba(255, 255, 255, 0.2)', }; diff --git a/invokeai/frontend/web/src/theme/colors/invokeAI.ts b/invokeai/frontend/web/src/theme/colors/invokeAI.ts index a523ae38c8..c39b3bed81 100644 --- a/invokeai/frontend/web/src/theme/colors/invokeAI.ts +++ b/invokeai/frontend/web/src/theme/colors/invokeAI.ts @@ -3,10 +3,16 @@ import { generateColorPalette } from 'theme/util/generateColorPalette'; export const invokeAIThemeColors: InvokeAIThemeColors = { base: generateColorPalette(225, 15), + baseAlpha: generateColorPalette(225, 15, false, true), accent: generateColorPalette(250, 50), + accentAlpha: generateColorPalette(250, 50, false, true), working: generateColorPalette(47, 67), + workingAlpha: generateColorPalette(47, 67, false, true), warning: generateColorPalette(28, 75), + warningAlpha: generateColorPalette(28, 75, false, true), ok: generateColorPalette(113, 70), + okAlpha: generateColorPalette(113, 70, false, true), error: generateColorPalette(0, 76), + errorAlpha: generateColorPalette(0, 76, false, true), gridLineColor: 'rgba(255, 255, 255, 0.2)', }; diff --git a/invokeai/frontend/web/src/theme/colors/lightTheme.ts b/invokeai/frontend/web/src/theme/colors/lightTheme.ts index 8fdf199bb8..2a7a05bbd2 100644 --- a/invokeai/frontend/web/src/theme/colors/lightTheme.ts +++ b/invokeai/frontend/web/src/theme/colors/lightTheme.ts @@ -3,10 +3,16 @@ import { generateColorPalette } from '../util/generateColorPalette'; export const lightThemeColors: InvokeAIThemeColors = { base: generateColorPalette(223, 10, true), + baseAlpha: generateColorPalette(223, 10, true, true), accent: generateColorPalette(40, 80, true), + accentAlpha: generateColorPalette(40, 80, true, true), working: generateColorPalette(47, 68, true), + workingAlpha: generateColorPalette(47, 68, true, true), warning: generateColorPalette(28, 75, true), + warningAlpha: generateColorPalette(28, 75, true, true), ok: generateColorPalette(122, 49, true), + okAlpha: generateColorPalette(122, 49, true, true), error: generateColorPalette(0, 50, true), + errorAlpha: generateColorPalette(0, 50, true, true), gridLineColor: 'rgba(0, 0, 0, 0.2)', }; diff --git a/invokeai/frontend/web/src/theme/colors/oceanBlue.ts b/invokeai/frontend/web/src/theme/colors/oceanBlue.ts index 3462459c1c..adfb8ab288 100644 --- a/invokeai/frontend/web/src/theme/colors/oceanBlue.ts +++ b/invokeai/frontend/web/src/theme/colors/oceanBlue.ts @@ -3,10 +3,16 @@ import { generateColorPalette } from '../util/generateColorPalette'; export const oceanBlueColors: InvokeAIThemeColors = { base: generateColorPalette(220, 30), + baseAlpha: generateColorPalette(220, 30, false, true), accent: generateColorPalette(210, 80), + accentAlpha: generateColorPalette(210, 80, false, true), working: generateColorPalette(47, 68), + workingAlpha: generateColorPalette(47, 68, false, true), warning: generateColorPalette(28, 75), + warningAlpha: generateColorPalette(28, 75, false, true), ok: generateColorPalette(122, 49), + okAlpha: generateColorPalette(122, 49, false, true), error: generateColorPalette(0, 100), + errorAlpha: generateColorPalette(0, 100, false, true), gridLineColor: 'rgba(136, 148, 184, 0.2)', }; diff --git a/invokeai/frontend/web/src/theme/themeTypes.d.ts b/invokeai/frontend/web/src/theme/themeTypes.d.ts index dce386168d..46144f39ab 100644 --- a/invokeai/frontend/web/src/theme/themeTypes.d.ts +++ b/invokeai/frontend/web/src/theme/themeTypes.d.ts @@ -1,10 +1,16 @@ export type InvokeAIThemeColors = { base: Partial; + baseAlpha: Partial; accent: Partial; + accentAlpha: Partial; working: Partial; + workingAlpha: Partial; warning: Partial; + warningAlpha: Partial; ok: Partial; + okAlpha: Partial; error: Partial; + errorAlpha: Partial; gridLineColor: string; }; diff --git a/invokeai/frontend/web/src/theme/util/generateColorPalette.ts b/invokeai/frontend/web/src/theme/util/generateColorPalette.ts index ed346c684a..4cb5fbd57d 100644 --- a/invokeai/frontend/web/src/theme/util/generateColorPalette.ts +++ b/invokeai/frontend/web/src/theme/util/generateColorPalette.ts @@ -9,49 +9,38 @@ import { InvokeAIPaletteSteps } from 'theme/themeTypes'; export function generateColorPalette( hue: string | number, saturation: string | number, - light = false + light = false, + alpha = false ) { hue = String(hue); saturation = String(saturation); const colorSteps = Array.from({ length: 21 }, (_, i) => i * 50); const lightnessSteps = [ - '0', - '5', - '10', - '15', - '20', - '25', - '30', - '35', - '40', - '45', - '50', - '55', - '59', - '64', - '68', - '73', - '77', - '82', - '86', - '95', - '100', + 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 59, 64, 68, 73, 77, 82, 86, + 95, 100, ]; const darkPalette: Partial = {}; const lightPalette: Partial = {}; colorSteps.forEach((colorStep, index) => { + const A = alpha ? lightnessSteps[index] / 100 : 1; + + // Lightness should be 50% for alpha colors + const darkPaletteLightness = alpha + ? 50 + : lightnessSteps[colorSteps.length - 1 - index]; + darkPalette[ colorStep as keyof typeof darkPalette - ] = `hsl(${hue}, ${saturation}%, ${ - lightnessSteps[colorSteps.length - 1 - index] - }%)`; + ] = `hsl(${hue} ${saturation}% ${darkPaletteLightness}% / ${A})`; + + const lightPaletteLightness = alpha ? 50 : lightnessSteps[index]; lightPalette[ colorStep as keyof typeof lightPalette - ] = `hsl(${hue}, ${saturation}%, ${lightnessSteps[index]}%)`; + ] = `hsl(${hue} ${saturation}% ${lightPaletteLightness}% / ${A})`; }); return light ? lightPalette : darkPalette; From b6b3b9f99cf1f2500610c94c48e5b90cd30dfefc Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 3 Jun 2023 12:41:51 +1000 Subject: [PATCH 24/47] feat(ui): make scrollbar less bright --- .../frontend/web/src/theme/css/overlayscrollbars.css | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/theme/css/overlayscrollbars.css b/invokeai/frontend/web/src/theme/css/overlayscrollbars.css index b5acaca75d..8f6f267095 100644 --- a/invokeai/frontend/web/src/theme/css/overlayscrollbars.css +++ b/invokeai/frontend/web/src/theme/css/overlayscrollbars.css @@ -8,11 +8,11 @@ /* The border radius of the scrollbar track */ /* --os-track-border-radius: 0; */ /* The background of the scrollbar track */ - --os-track-bg: rgba(0, 0, 0, 0.3); + /* --os-track-bg: rgba(0, 0, 0, 0.3); */ /* The :hover background of the scrollbar track */ - --os-track-bg-hover: rgba(0, 0, 0, 0.3); + /* --os-track-bg-hover: rgba(0, 0, 0, 0.3); */ /* The :active background of the scrollbar track */ - --os-track-bg-active: rgba(0, 0, 0, 0.3); + /* --os-track-bg-active: rgba(0, 0, 0, 0.3); */ /* The border of the scrollbar track */ /* --os-track-border: none; */ /* The :hover background of the scrollbar track */ @@ -22,11 +22,11 @@ /* The border radius of the scrollbar handle */ /* --os-handle-border-radius: 0; */ /* The background of the scrollbar handle */ - --os-handle-bg: var(--invokeai-colors-accent-500); + --os-handle-bg: var(--invokeai-colors-accentAlpha-500); /* The :hover background of the scrollbar handle */ - --os-handle-bg-hover: var(--invokeai-colors-accent-450); + --os-handle-bg-hover: var(--invokeai-colors-accentAlpha-700); /* The :active background of the scrollbar handle */ - --os-handle-bg-active: var(--invokeai-colors-accent-400); + --os-handle-bg-active: var(--invokeai-colors-accentAlpha-800); /* The border of the scrollbar handle */ /* --os-handle-border: none; */ /* The :hover border of the scrollbar handle */ From a0dde66b5d027150d64bcd3edc301dd4453cd141 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 3 Jun 2023 13:06:37 +1000 Subject: [PATCH 25/47] feat(ui): more work on controlnet mini --- .../web/src/common/components/IAIDndImage.tsx | 2 +- .../common/components/IAISimpleCheckbox.tsx | 4 +- .../controlNet/components/ControlNet.tsx | 6 +- .../components/ControlNetImagePreview.tsx | 32 ++-- .../controlNet/components/ControlNetMini.tsx | 152 ++++++++++++------ .../parameters/ParamControlNetBeginEnd.tsx | 3 +- .../controlNet/store/controlNetSlice.ts | 12 +- .../graphBuilders/buildTextToImageGraph.ts | 14 +- .../ControlNet/ParamControlNetCollapse.tsx | 33 ++-- 9 files changed, 166 insertions(+), 92 deletions(-) diff --git a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx index a6667e73be..1451f82677 100644 --- a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx +++ b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx @@ -48,7 +48,7 @@ const IAIDndImage = (props: IAIDndImageProps) => { isDragDisabled = false, fallback = , payloadImage, - minSize = 36, + minSize = 24, } = props; const dndId = useRef(uuidv4()); const { getUrl } = useGetUrl(); diff --git a/invokeai/frontend/web/src/common/components/IAISimpleCheckbox.tsx b/invokeai/frontend/web/src/common/components/IAISimpleCheckbox.tsx index 4d21d3d3d0..2d28b5b72e 100644 --- a/invokeai/frontend/web/src/common/components/IAISimpleCheckbox.tsx +++ b/invokeai/frontend/web/src/common/components/IAISimpleCheckbox.tsx @@ -1,8 +1,8 @@ import { Checkbox, CheckboxProps, Text } from '@chakra-ui/react'; -import { memo, ReactNode } from 'react'; +import { memo, ReactElement } from 'react'; type IAISimpleCheckboxProps = CheckboxProps & { - label: string | ReactNode; + label: string | ReactElement; }; const IAISimpleCheckbox = (props: IAISimpleCheckboxProps) => { diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx index 803444d96b..2b86ca0e4d 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx @@ -58,11 +58,7 @@ const ControlNet = (props: ControlNetProps) => { return ( - + { - const { controlNetId, controlImage, processedControlImage } = props; + const { + controlNetId, + controlImage, + processedControlImage, + isControlImageProcessed, + } = props.controlNet; const dispatch = useAppDispatch(); const { isProcessingControlImage } = useAppSelector(selector); + const containerRef = useRef(null); - const [shouldShowProcessedImage, setShouldShowProcessedImage] = - useState(true); + const isMouseOverImage = useHoverDirty(containerRef); const handleControlImageChanged = useCallback( (controlImage: ImageDTO) => { @@ -46,12 +51,15 @@ const ControlNetImagePreview = (props: Props) => { Number(controlImage?.width) > Number(processedControlImage?.width) || Number(controlImage?.height) > Number(processedControlImage?.height); + const shouldShowProcessedImage = + controlImage && + processedControlImage && + !isMouseOverImage && + !isProcessingControlImage && + !isControlImageProcessed; + return ( - setShouldShowProcessedImage(false)} - onMouseOut={() => setShouldShowProcessedImage(true)} - > + { processorNode, } = props.controlNet; const dispatch = useAppDispatch(); - const handleReset = useCallback(() => { - dispatch( - controlNetProcessedImageChanged({ - controlNetId, - processedControlImage: null, - }) - ); + + const handleDelete = useCallback(() => { + dispatch(controlNetRemoved(controlNetId)); }, [controlNetId, dispatch]); - const handleControlNetRemoved = useCallback(() => { - dispatch(controlNetRemoved(controlNetId)); + const handleDuplicate = useCallback(() => { + dispatch( + controlNetAdded({ controlNetId: uuidv4(), controlNet: props.controlNet }) + ); + }, [dispatch, props.controlNet]); + + const handleToggleIsEnabled = useCallback(() => { + dispatch(controlNetToggled(controlNetId)); + }, [controlNetId, dispatch]); + + const handleToggleIsPreprocessed = useCallback(() => { + dispatch(isControlNetImageProcessedToggled(controlNetId)); }, [controlNetId, dispatch]); return ( + + + } + /> + } + /> + - - - - - - + + + + + + + + + + + Enabled + + + + + + Preprocessed + + + + ); 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 e258d4cf29..bb2f151193 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginEnd.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginEnd.tsx @@ -50,7 +50,7 @@ const ParamControlNetBeginEnd = (props: Props) => { return ( - Begin & End Step % + Begin / End Step Percentage { {!mini && ( <> - {' '} + action: PayloadAction<{ controlNetId: string; controlNet?: ControlNet }> ) => { - const { controlNetId } = action.payload; + const { controlNetId, controlNet } = action.payload; state.controlNets[controlNetId] = { - ...initialControlNet, + ...(controlNet ?? initialControlNet), controlNetId, }; }, @@ -116,11 +116,9 @@ export const controlNetSlice = createSlice({ }, isControlNetImageProcessedToggled: ( state, - action: PayloadAction<{ - controlNetId: string; - }> + action: PayloadAction ) => { - const { controlNetId } = action.payload; + const controlNetId = action.payload; state.controlNets[controlNetId].isControlImageProcessed = !state.controlNets[controlNetId].isControlImageProcessed; }, diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts index 9975d446a3..161f857bd7 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts @@ -346,6 +346,11 @@ export const buildTextToImageGraph = (state: RootState): Graph => { weight, } = controlNet; + if (!isEnabled) { + // Skip disabled ControlNets + return; + } + const controlNetNode: ControlNetInvocation = { id: `control_net_${controlNetId}`, type: 'controlnet', @@ -355,14 +360,14 @@ export const buildTextToImageGraph = (state: RootState): Graph => { control_weight: weight, }; - if (processedControlImage) { + if (processedControlImage && !isControlImageProcessed) { // We've already processed the image in the app, so we can just use the processed image const { image_name, image_origin } = processedControlImage; controlNetNode.image = { image_name, image_origin, }; - } else if (controlImage) { + } else if (controlImage && isControlImageProcessed) { // The control image is preprocessed const { image_name, image_origin } = controlImage; controlNetNode.image = { @@ -370,9 +375,10 @@ export const buildTextToImageGraph = (state: RootState): Graph => { image_origin, }; } else { - // The control image is not processed, so we need to add a preprocess node - // TODO: Add preprocess node + // Skip ControlNets without an unprocessed image - should never happen if everything is working correctly + return; } + graph.nodes[controlNetNode.id] = controlNetNode; if (size(controlNets) > 1) { diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx index 2359e5123c..6369ab56de 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx @@ -1,6 +1,6 @@ import { + Divider, Flex, - Spacer, Tab, TabList, TabPanel, @@ -9,7 +9,7 @@ import { } from '@chakra-ui/react'; import { useTranslation } from 'react-i18next'; import IAICollapse from 'common/components/IAICollapse'; -import { memo, useCallback } from 'react'; +import { Fragment, memo, useCallback } from 'react'; import IAIIconButton from 'common/components/IAIIconButton'; import { FaPlus } from 'react-icons/fa'; import ControlNet from 'features/controlNet/components/ControlNet'; @@ -21,11 +21,11 @@ import { isControlNetEnabledToggled, } from 'features/controlNet/store/controlNetSlice'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import { map, startCase } from 'lodash-es'; +import { map } from 'lodash-es'; import { v4 as uuidv4 } from 'uuid'; -import { CloseIcon } from '@chakra-ui/icons'; import ControlNetMini from 'features/controlNet/components/ControlNetMini'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; +import IAIButton from 'common/components/IAIButton'; const selector = createSelector( controlNetSelector, @@ -56,11 +56,26 @@ const ParamControlNetCollapse = () => { } return ( - <> - {controlNetsArray.map((c) => ( - - ))} - + + {controlNetsArray.length === 0 && ( + + Add ControlNet + + )} + + {controlNetsArray.map((c, i) => ( + + {i > 0 && } + + + ))} + + ); return ( From 54b7ddd63ff619177855d37caa5e27fe3b25b5ca Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 3 Jun 2023 13:33:57 +1000 Subject: [PATCH 26/47] feat(ui): IAIDndImage `cursor: 'grab'` --- invokeai/frontend/web/src/common/components/IAIDndImage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx index 1451f82677..8b94cd7b03 100644 --- a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx +++ b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx @@ -89,6 +89,7 @@ const IAIDndImage = (props: IAIDndImageProps) => { minW: minSize, minH: minSize, userSelect: 'none', + cursor: 'grab', }} {...attributes} {...listeners} From 828c86964da29b1a1094617c7fc0f98c4dbf9b3a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 3 Jun 2023 14:05:03 +1000 Subject: [PATCH 27/47] feat(ui): IAICustomSelect prevent label wrap --- .../web/src/common/components/IAICustomSelect.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx b/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx index 548b4d73e0..e7c7fd8f97 100644 --- a/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx +++ b/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx @@ -90,9 +90,20 @@ const IAICustomSelect = (props: IAICustomSelectProps) => { alignItems: 'center', userSelect: 'none', cursor: 'pointer', + overflow: 'hidden', + width: 'full', }} > - + {selectedItem} @@ -105,7 +116,6 @@ const IAICustomSelect = (props: IAICustomSelectProps) => { sx={{ ...floatingStyles, width: 'full', - // width: 'max-content', top: 0, left: 0, flexDirection: 'column', From 69f0ba65f1c87d8051efc956362600743a9d9362 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 3 Jun 2023 15:05:17 +1000 Subject: [PATCH 28/47] chore(ui): bump `react-icons` --- invokeai/frontend/web/package.json | 2 +- invokeai/frontend/web/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index a9d0bfba7e..64b9a828cd 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -89,7 +89,7 @@ "react-dropzone": "^14.2.3", "react-hotkeys-hook": "4.4.0", "react-i18next": "^12.2.2", - "react-icons": "^4.7.1", + "react-icons": "^4.9.0", "react-konva": "^18.2.7", "react-redux": "^8.0.5", "react-resizable-panels": "^0.0.42", diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock index a4c9bf3633..b14d83ae2b 100644 --- a/invokeai/frontend/web/yarn.lock +++ b/invokeai/frontend/web/yarn.lock @@ -5540,10 +5540,10 @@ react-i18next@^12.2.2: "@babel/runtime" "^7.20.6" html-parse-stringify "^3.0.1" -react-icons@^4.7.1: - version "4.8.0" - resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.8.0.tgz#621e900caa23b912f737e41be57f27f6b2bff445" - integrity sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg== +react-icons@^4.9.0: + version "4.9.0" + resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.9.0.tgz#ba44f436a053393adb1bdcafbc5c158b7b70d2a3" + integrity sha512-ijUnFr//ycebOqujtqtV9PFS7JjhWg0QU6ykURVHuL4cbofvRCf3f6GMn9+fBktEFQOIVZnuAYLZdiyadRQRFg== react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" From d6c08ba46921cdadccbba767bc39de8f0337faf7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 3 Jun 2023 15:05:49 +1000 Subject: [PATCH 29/47] feat(ui): add mini/advanced controlnet ui --- .../middleware/listenerMiddleware/index.ts | 2 +- ...amsChanged.ts => controlNetAutoProcess.ts} | 52 ++--- .../web/src/common/components/IAISwitch.tsx | 8 +- .../controlNet/components/ControlNet.tsx | 194 +++++++++++++++--- .../components/ControlNetImagePreview.tsx | 95 +++++---- .../controlNet/components/ControlNetMini.tsx | 153 -------------- .../components/ControlNetPreprocessButton.tsx | 4 +- .../ParamControlNetBeginStepPct.tsx | 58 ------ .../parameters/ParamControlNetEndStepPct.tsx | 42 ---- .../parameters/ParamControlNetIsEnabled.tsx | 2 +- .../ParamControlNetIsPreprocessed.tsx | 4 +- .../parameters/ParamControlNetModel.tsx | 4 +- .../features/controlNet/store/constants.ts | 26 ++- .../controlNet/store/controlNetSlice.ts | 71 +++---- .../nodes/util/addControlNetToLinearGraph.ts | 100 +++++++++ .../graphBuilders/buildImageToImageGraph.ts | 3 + .../graphBuilders/buildTextToImageGraph.ts | 93 +-------- .../ControlNet/ParamControlNetCollapse.tsx | 67 +----- 18 files changed, 430 insertions(+), 548 deletions(-) rename invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/{controlNetProcessorParamsChanged.ts => controlNetAutoProcess.ts} (54%) delete mode 100644 invokeai/frontend/web/src/features/controlNet/components/ControlNetMini.tsx delete mode 100644 invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginStepPct.tsx delete mode 100644 invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetEndStepPct.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/util/addControlNetToLinearGraph.ts 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 8c6503521c..a9349dc863 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -71,7 +71,7 @@ import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSa import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingAreaImageListener'; import { addImageCategoriesChangedListener } from './listeners/imageCategoriesChanged'; import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed'; -import { addControlNetAutoProcessListener } from './listeners/controlNetProcessorParamsChanged'; +import { addControlNetAutoProcessListener } from './listeners/controlNetAutoProcess'; export const listenerMiddleware = createListenerMiddleware(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetProcessorParamsChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess.ts similarity index 54% rename from invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetProcessorParamsChanged.ts rename to invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess.ts index 11237c9d27..d53907e673 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetProcessorParamsChanged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess.ts @@ -1,3 +1,4 @@ +import { AnyAction } from '@reduxjs/toolkit'; import { startAppListening } from '..'; import { log } from 'app/logging/useLogger'; import { controlNetImageProcessed } from 'features/controlNet/store/actions'; @@ -5,10 +6,37 @@ import { controlNetImageChanged, controlNetProcessorParamsChanged, controlNetProcessorTypeChanged, + isControlNetImagePreprocessedToggled, } from 'features/controlNet/store/controlNetSlice'; +import { RootState } from 'app/store/store'; const moduleLog = log.child({ namespace: 'controlNet' }); +const predicate = (action: AnyAction, state: RootState) => { + const isActionMatched = + controlNetProcessorParamsChanged.match(action) || + controlNetImageChanged.match(action) || + controlNetProcessorTypeChanged.match(action) || + isControlNetImagePreprocessedToggled.match(action); + + if (!isActionMatched) { + return false; + } + + const { controlNetId } = action.payload; + + const shouldAutoProcess = + !state.controlNet.controlNets[controlNetId].isPreprocessed; + + const isBusy = state.system.isProcessing; + + const hasControlImage = Boolean( + state.controlNet.controlNets[controlNetId].controlImage + ); + + return shouldAutoProcess && !isBusy && hasControlImage; +}; + /** * Listener that automatically processes a ControlNet image when its processor parameters are changed. * @@ -16,35 +44,13 @@ const moduleLog = log.child({ namespace: 'controlNet' }); */ export const addControlNetAutoProcessListener = () => { startAppListening({ - predicate: (action) => - controlNetProcessorParamsChanged.match(action) || - controlNetImageChanged.match(action) || - controlNetProcessorTypeChanged.match(action), + predicate, effect: async ( action, { dispatch, getState, cancelActiveListeners, delay } ) => { - const state = getState(); - if (!state.controlNet.shouldAutoProcess) { - // silently skip - return; - } - - if (state.system.isProcessing) { - moduleLog.trace('System busy, skipping ControlNet auto-processing'); - return; - } - const { controlNetId } = action.payload; - if (!state.controlNet.controlNets[controlNetId].controlImage) { - moduleLog.trace( - { data: { controlNetId } }, - 'No ControlNet image to auto-process' - ); - return; - } - // Cancel any in-progress instances of this listener cancelActiveListeners(); diff --git a/invokeai/frontend/web/src/common/components/IAISwitch.tsx b/invokeai/frontend/web/src/common/components/IAISwitch.tsx index e1bddb9f43..9a7ba7eb76 100644 --- a/invokeai/frontend/web/src/common/components/IAISwitch.tsx +++ b/invokeai/frontend/web/src/common/components/IAISwitch.tsx @@ -36,9 +36,11 @@ const IAISwitch = (props: Props) => { alignItems="center" {...formControlProps} > - - {label} - + {label && ( + + {label} + + )} ); diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx index 2b86ca0e4d..fe5f07f89f 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx @@ -1,32 +1,42 @@ import { memo, useCallback } from 'react'; import { - ControlNet, - controlNetProcessedImageChanged, + ControlNetConfig, + controlNetAdded, controlNetRemoved, + controlNetToggled, + isControlNetImagePreprocessedToggled, } from '../store/controlNetSlice'; import { useAppDispatch } from 'app/store/storeHooks'; import ParamControlNetModel from './parameters/ParamControlNetModel'; import ParamControlNetWeight from './parameters/ParamControlNetWeight'; import { - Box, + Checkbox, Flex, - Tab, + FormControl, + FormLabel, + HStack, TabList, - TabPanel, TabPanels, Tabs, - Text, + Tab, + TabPanel, + Box, } from '@chakra-ui/react'; -import IAIButton from 'common/components/IAIButton'; -import { FaUndo } from 'react-icons/fa'; +import { FaCopy, FaTrash } from 'react-icons/fa'; + +import ParamControlNetBeginEnd from './parameters/ParamControlNetBeginEnd'; +import ControlNetImagePreview from './ControlNetImagePreview'; +import IAIIconButton from 'common/components/IAIIconButton'; +import { v4 as uuidv4 } from 'uuid'; +import { useToggle } from 'react-use'; import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect'; import ControlNetProcessorComponent from './ControlNetProcessorComponent'; import ControlNetPreprocessButton from './ControlNetPreprocessButton'; -import ParamControlNetBeginEnd from './parameters/ParamControlNetBeginEnd'; -import ControlNetImagePreview from './ControlNetImagePreview'; +import IAIButton from 'common/components/IAIButton'; +import IAISwitch from 'common/components/IAISwitch'; type ControlNetProps = { - controlNet: ControlNet; + controlNet: ControlNetConfig; }; const ControlNet = (props: ControlNetProps) => { @@ -38,24 +48,160 @@ const ControlNet = (props: ControlNetProps) => { beginStepPct, endStepPct, controlImage, - isControlImageProcessed, + isPreprocessed: isControlImageProcessed, processedControlImage, processorNode, } = props.controlNet; const dispatch = useAppDispatch(); - const handleReset = useCallback(() => { - dispatch( - controlNetProcessedImageChanged({ - controlNetId, - processedControlImage: null, - }) - ); + const [shouldShowAdvanced, onToggleAdvanced] = useToggle(true); + + const handleDelete = useCallback(() => { + dispatch(controlNetRemoved({ controlNetId })); }, [controlNetId, dispatch]); - const handleControlNetRemoved = useCallback(() => { - dispatch(controlNetRemoved(controlNetId)); + const handleDuplicate = useCallback(() => { + dispatch( + controlNetAdded({ controlNetId: uuidv4(), controlNet: props.controlNet }) + ); + }, [dispatch, props.controlNet]); + + const handleToggleIsEnabled = useCallback(() => { + dispatch(controlNetToggled({ controlNetId })); }, [controlNetId, dispatch]); + const handleToggleIsPreprocessed = useCallback(() => { + dispatch(isControlNetImagePreprocessedToggled({ controlNetId })); + }, [controlNetId, dispatch]); + + return ( + + + + + + + } + /> + } + /> + + {isEnabled && ( + <> + + {!shouldShowAdvanced && ( + + + + )} + + + + + + Preprocessed + + + + + + Advanced + + + + + + + + {shouldShowAdvanced && ( + <> + + + + {!isControlImageProcessed && ( + <> + + + + )} + + )} + + )} + + ); + return ( @@ -101,18 +247,18 @@ const ControlNet = (props: ControlNetProps) => { processorNode={processorNode} /> - } onClick={handleReset} isDisabled={Boolean(!processedControlImage)} > Reset Processing - + */} - Remove ControlNet + Remove ControlNet ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx index 86a5a06569..099e58ce80 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx @@ -1,7 +1,7 @@ import { memo, useCallback, useRef, useState } from 'react'; import { ImageDTO } from 'services/api'; import { - ControlNet, + ControlNetConfig, controlNetImageChanged, controlNetSelector, } from '../store/controlNetSlice'; @@ -24,7 +24,7 @@ const selector = createSelector( ); type Props = { - controlNet: ControlNet; + controlNet: ControlNetConfig; }; const ControlNetImagePreview = (props: Props) => { @@ -32,7 +32,7 @@ const ControlNetImagePreview = (props: Props) => { controlNetId, controlImage, processedControlImage, - isControlImageProcessed, + isPreprocessed: isControlImageProcessed, } = props.controlNet; const dispatch = useAppDispatch(); const { isProcessingControlImage } = useAppSelector(selector); @@ -63,63 +63,62 @@ const ControlNetImagePreview = (props: Props) => { - {controlImage && - processedControlImage && - shouldShowProcessedImage && - !isProcessingControlImage && ( - + + {shouldShowProcessedImageBackdrop && ( + + )} - {shouldShowProcessedImageBackdrop && ( - - )} - - - + - - )} + + + )} {isProcessingControlImage && ( { - const { - controlNetId, - isEnabled, - model, - weight, - beginStepPct, - endStepPct, - controlImage, - isControlImageProcessed, - processedControlImage, - processorNode, - } = props.controlNet; - const dispatch = useAppDispatch(); - - const handleDelete = useCallback(() => { - dispatch(controlNetRemoved(controlNetId)); - }, [controlNetId, dispatch]); - - const handleDuplicate = useCallback(() => { - dispatch( - controlNetAdded({ controlNetId: uuidv4(), controlNet: props.controlNet }) - ); - }, [dispatch, props.controlNet]); - - const handleToggleIsEnabled = useCallback(() => { - dispatch(controlNetToggled(controlNetId)); - }, [controlNetId, dispatch]); - - const handleToggleIsPreprocessed = useCallback(() => { - dispatch(isControlNetImageProcessedToggled(controlNetId)); - }, [controlNetId, dispatch]); - - return ( - - - - } - /> - } - /> - - - - - - - - - - - - - Enabled - - - - - - Preprocessed - - - - - - - ); -}; - -export default memo(ControlNet); diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNetPreprocessButton.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNetPreprocessButton.tsx index 94b1f86501..95a4f968e5 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNetPreprocessButton.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNetPreprocessButton.tsx @@ -1,12 +1,12 @@ import IAIButton from 'common/components/IAIButton'; import { memo, useCallback } from 'react'; -import { ControlNet } from '../store/controlNetSlice'; +import { ControlNetConfig } from '../store/controlNetSlice'; import { useAppDispatch } from 'app/store/storeHooks'; import { controlNetImageProcessed } from '../store/actions'; import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke'; type Props = { - controlNet: ControlNet; + controlNet: ControlNetConfig; }; const ControlNetPreprocessButton = (props: Props) => { diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginStepPct.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginStepPct.tsx deleted file mode 100644 index d94db5e272..0000000000 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginStepPct.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useAppDispatch } from 'app/store/storeHooks'; -import IAISlider from 'common/components/IAISlider'; -import { - controlNetBeginStepPctChanged, - controlNetEndStepPctChanged, -} from 'features/controlNet/store/controlNetSlice'; -import { memo, useCallback } from 'react'; - -type ParamControlNetBeginStepPctProps = { - controlNetId: string; - beginStepPct: number; -}; - -const ParamControlNetBeginStepPct = ( - props: ParamControlNetBeginStepPctProps -) => { - const { controlNetId, beginStepPct } = props; - const dispatch = useAppDispatch(); - - const handleBeginStepPctChanged = useCallback( - (beginStepPct: number) => { - dispatch(controlNetBeginStepPctChanged({ controlNetId, beginStepPct })); - }, - [controlNetId, dispatch] - ); - - const handleBeginStepPctReset = useCallback(() => { - dispatch(controlNetBeginStepPctChanged({ controlNetId, beginStepPct: 0 })); - }, [controlNetId, dispatch]); - - const handleEndStepPctChanged = useCallback( - (endStepPct: number) => { - dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct })); - }, - [controlNetId, dispatch] - ); - - const handleEndStepPctReset = useCallback(() => { - dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct: 0 })); - }, [controlNetId, dispatch]); - - return ( - - ); -}; - -export default memo(ParamControlNetBeginStepPct); diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetEndStepPct.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetEndStepPct.tsx deleted file mode 100644 index d3d831cf31..0000000000 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetEndStepPct.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { useAppDispatch } from 'app/store/storeHooks'; -import IAISlider from 'common/components/IAISlider'; -import { controlNetEndStepPctChanged } from 'features/controlNet/store/controlNetSlice'; -import { memo, useCallback } from 'react'; - -type ParamControlNetEndStepPctProps = { - controlNetId: string; - endStepPct: number; -}; - -const ParamControlNetEndStepPct = (props: ParamControlNetEndStepPctProps) => { - const { controlNetId, endStepPct } = props; - const dispatch = useAppDispatch(); - - const handleEndStepPctChanged = useCallback( - (endStepPct: number) => { - dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct })); - }, - [controlNetId, dispatch] - ); - - const handleEndStepPctReset = () => { - dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct: 0 })); - }; - - return ( - - ); -}; - -export default memo(ParamControlNetEndStepPct); diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsEnabled.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsEnabled.tsx index f42265cb22..d7f519a7b6 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsEnabled.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsEnabled.tsx @@ -13,7 +13,7 @@ const ParamControlNetIsEnabled = (props: ParamControlNetIsEnabledProps) => { const dispatch = useAppDispatch(); const handleIsEnabledChanged = useCallback(() => { - dispatch(controlNetToggled(controlNetId)); + dispatch(controlNetToggled({ controlNetId })); }, [dispatch, controlNetId]); return ( diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsPreprocessed.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsPreprocessed.tsx index 9e2658964d..6db61a0d15 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsPreprocessed.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetIsPreprocessed.tsx @@ -3,7 +3,7 @@ import IAIFullCheckbox from 'common/components/IAIFullCheckbox'; import IAISwitch from 'common/components/IAISwitch'; import { controlNetToggled, - isControlNetImageProcessedToggled, + isControlNetImagePreprocessedToggled, } from 'features/controlNet/store/controlNetSlice'; import { memo, useCallback } from 'react'; @@ -18,7 +18,7 @@ const ParamControlNetIsEnabled = (props: ParamControlNetIsEnabledProps) => { const handleIsControlImageProcessedToggled = useCallback(() => { dispatch( - isControlNetImageProcessedToggled({ + isControlNetImagePreprocessedToggled({ controlNetId, }) ); diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetModel.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetModel.tsx index e5a1ca1c39..beb34e9d24 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetModel.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetModel.tsx @@ -3,8 +3,8 @@ import IAICustomSelect from 'common/components/IAICustomSelect'; import { CONTROLNET_MODELS, ControlNetModel, - controlNetModelChanged, -} from 'features/controlNet/store/controlNetSlice'; +} from 'features/controlNet/store/constants'; +import { controlNetModelChanged } from 'features/controlNet/store/controlNetSlice'; import { memo, useCallback } from 'react'; type ParamIsControlNetModelProps = { diff --git a/invokeai/frontend/web/src/features/controlNet/store/constants.ts b/invokeai/frontend/web/src/features/controlNet/store/constants.ts index a7e20a78d7..da3a9c57b5 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/constants.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/constants.ts @@ -22,7 +22,7 @@ type ControlNetProcessorsDict = Record< * * TODO: Generate from the OpenAPI schema */ -export const CONTROLNET_PROCESSORS = { +export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = { canny_image_processor: { type: 'canny_image_processor', label: 'Canny', @@ -164,3 +164,27 @@ export const CONTROLNET_PROCESSORS = { }, }, }; + +export const CONTROLNET_MODELS = [ + 'lllyasviel/sd-controlnet-canny', + 'lllyasviel/sd-controlnet-depth', + 'lllyasviel/sd-controlnet-hed', + 'lllyasviel/sd-controlnet-seg', + 'lllyasviel/sd-controlnet-openpose', + 'lllyasviel/sd-controlnet-scribble', + 'lllyasviel/sd-controlnet-normal', + 'lllyasviel/sd-controlnet-mlsd', +]; + +export type ControlNetModel = (typeof CONTROLNET_MODELS)[number]; + +export const CONTROLNET_MODEL_MAP: Record< + ControlNetModel, + ControlNetProcessorType +> = { + 'lllyasviel/sd-controlnet-canny': 'canny_image_processor', + 'lllyasviel/sd-controlnet-depth': 'midas_depth_image_processor', + 'lllyasviel/sd-controlnet-hed': 'hed_image_processor', + 'lllyasviel/sd-controlnet-openpose': 'openpose_image_processor', + 'lllyasviel/sd-controlnet-mlsd': 'mlsd_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 1155567d73..4847e3c1a5 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts @@ -7,36 +7,27 @@ import { RequiredCannyImageProcessorInvocation, RequiredControlNetProcessorNode, } from './types'; -import { CONTROLNET_PROCESSORS } from './constants'; +import { + CONTROLNET_MODELS, + CONTROLNET_PROCESSORS, + ControlNetModel, +} from './constants'; import { controlNetImageProcessed } from './actions'; -export const CONTROLNET_MODELS = [ - 'lllyasviel/sd-controlnet-canny', - 'lllyasviel/sd-controlnet-depth', - 'lllyasviel/sd-controlnet-hed', - 'lllyasviel/sd-controlnet-seg', - 'lllyasviel/sd-controlnet-openpose', - 'lllyasviel/sd-controlnet-scribble', - 'lllyasviel/sd-controlnet-normal', - 'lllyasviel/sd-controlnet-mlsd', -]; - -export type ControlNetModel = (typeof CONTROLNET_MODELS)[number]; - -export const initialControlNet: Omit = { +export const initialControlNet: Omit = { isEnabled: true, model: CONTROLNET_MODELS[0], weight: 1, beginStepPct: 0, endStepPct: 1, controlImage: null, - isControlImageProcessed: false, + isPreprocessed: false, processedControlImage: null, processorNode: CONTROLNET_PROCESSORS.canny_image_processor .default as RequiredCannyImageProcessorInvocation, }; -export type ControlNet = { +export type ControlNetConfig = { controlNetId: string; isEnabled: boolean; model: ControlNetModel; @@ -44,22 +35,20 @@ export type ControlNet = { beginStepPct: number; endStepPct: number; controlImage: ImageDTO | null; - isControlImageProcessed: boolean; + isPreprocessed: boolean; processedControlImage: ImageDTO | null; processorNode: RequiredControlNetProcessorNode; }; export type ControlNetState = { - controlNets: Record; + controlNets: Record; isEnabled: boolean; - shouldAutoProcess: boolean; isProcessingControlImage: boolean; }; export const initialControlNetState: ControlNetState = { controlNets: {}, isEnabled: false, - shouldAutoProcess: true, isProcessingControlImage: false, }; @@ -72,7 +61,10 @@ export const controlNetSlice = createSlice({ }, controlNetAdded: ( state, - action: PayloadAction<{ controlNetId: string; controlNet?: ControlNet }> + action: PayloadAction<{ + controlNetId: string; + controlNet?: ControlNetConfig; + }> ) => { const { controlNetId, controlNet } = action.payload; state.controlNets[controlNetId] = { @@ -91,12 +83,18 @@ export const controlNetSlice = createSlice({ controlImage, }; }, - controlNetRemoved: (state, action: PayloadAction) => { - const controlNetId = action.payload; + controlNetRemoved: ( + state, + action: PayloadAction<{ controlNetId: string }> + ) => { + const { controlNetId } = action.payload; delete state.controlNets[controlNetId]; }, - controlNetToggled: (state, action: PayloadAction) => { - const controlNetId = action.payload; + controlNetToggled: ( + state, + action: PayloadAction<{ controlNetId: string }> + ) => { + const { controlNetId } = action.payload; state.controlNets[controlNetId].isEnabled = !state.controlNets[controlNetId].isEnabled; }, @@ -110,17 +108,20 @@ export const controlNetSlice = createSlice({ const { controlNetId, controlImage } = action.payload; state.controlNets[controlNetId].controlImage = controlImage; state.controlNets[controlNetId].processedControlImage = null; - if (state.shouldAutoProcess && controlImage !== null) { + if ( + controlImage !== null && + !state.controlNets[controlNetId].isPreprocessed + ) { state.isProcessingControlImage = true; } }, - isControlNetImageProcessedToggled: ( + isControlNetImagePreprocessedToggled: ( state, - action: PayloadAction + action: PayloadAction<{ controlNetId: string }> ) => { - const controlNetId = action.payload; - state.controlNets[controlNetId].isControlImageProcessed = - !state.controlNets[controlNetId].isControlImageProcessed; + const { controlNetId } = action.payload; + state.controlNets[controlNetId].isPreprocessed = + !state.controlNets[controlNetId].isPreprocessed; }, controlNetProcessedImageChanged: ( state, @@ -191,9 +192,6 @@ export const controlNetSlice = createSlice({ processorType ].default as RequiredControlNetProcessorNode; }, - shouldAutoProcessToggled: (state) => { - state.shouldAutoProcess = !state.shouldAutoProcess; - }, }, extraReducers: (builder) => { builder.addCase(controlNetImageProcessed, (state, action) => { @@ -212,7 +210,7 @@ export const { controlNetAddedFromImage, controlNetRemoved, controlNetImageChanged, - isControlNetImageProcessedToggled, + isControlNetImagePreprocessedToggled, controlNetProcessedImageChanged, controlNetToggled, controlNetModelChanged, @@ -221,7 +219,6 @@ export const { controlNetEndStepPctChanged, controlNetProcessorParamsChanged, controlNetProcessorTypeChanged, - shouldAutoProcessToggled, } = controlNetSlice.actions; export default controlNetSlice.reducer; diff --git a/invokeai/frontend/web/src/features/nodes/util/addControlNetToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/addControlNetToLinearGraph.ts new file mode 100644 index 0000000000..b386b41dc7 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/addControlNetToLinearGraph.ts @@ -0,0 +1,100 @@ +import { RootState } from 'app/store/store'; +import { forEach, size } from 'lodash-es'; +import { CollectInvocation, ControlNetInvocation } from 'services/api'; +import { NonNullableGraph } from '../types/types'; + +const CONTROL_NET_COLLECT = 'control_net_collect'; + +export const addControlNetToLinearGraph = ( + graph: NonNullableGraph, + baseNodeId: string, + state: RootState +): void => { + const { isEnabled: isControlNetEnabled, controlNets } = state.controlNet; + + // Add ControlNet + if (isControlNetEnabled) { + if (size(controlNets) > 1) { + const controlNetIterateNode: CollectInvocation = { + id: CONTROL_NET_COLLECT, + type: 'collect', + }; + graph.nodes[controlNetIterateNode.id] = controlNetIterateNode; + graph.edges.push({ + source: { node_id: controlNetIterateNode.id, field: 'collection' }, + destination: { + node_id: baseNodeId, + field: 'control', + }, + }); + } + + forEach(controlNets, (controlNet, index) => { + const { + controlNetId, + isEnabled, + isPreprocessed: isControlImageProcessed, + controlImage, + processedControlImage, + beginStepPct, + endStepPct, + model, + processorNode, + weight, + } = controlNet; + + if (!isEnabled) { + // Skip disabled ControlNets + return; + } + + const controlNetNode: ControlNetInvocation = { + id: `control_net_${controlNetId}`, + type: 'controlnet', + begin_step_percent: beginStepPct, + end_step_percent: endStepPct, + control_model: model as ControlNetInvocation['control_model'], + control_weight: weight, + }; + + if (processedControlImage && !isControlImageProcessed) { + // We've already processed the image in the app, so we can just use the processed image + const { image_name, image_origin } = processedControlImage; + controlNetNode.image = { + image_name, + image_origin, + }; + } else if (controlImage && isControlImageProcessed) { + // The control image is preprocessed + const { image_name, image_origin } = controlImage; + controlNetNode.image = { + image_name, + image_origin, + }; + } else { + // Skip ControlNets without an unprocessed image - should never happen if everything is working correctly + return; + } + + graph.nodes[controlNetNode.id] = controlNetNode; + + if (size(controlNets) > 1) { + graph.edges.push({ + source: { node_id: controlNetNode.id, field: 'control' }, + destination: { + node_id: CONTROL_NET_COLLECT, + field: 'item', + }, + }); + } else { + graph.edges.push({ + source: { node_id: controlNetNode.id, field: 'control' }, + destination: { + node_id: baseNodeId, + field: 'control', + }, + }); + } + }); + } +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts index fe4f6c63b5..a1dc5d48ab 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts @@ -14,6 +14,7 @@ import { import { NonNullableGraph } from 'features/nodes/types/types'; import { log } from 'app/logging/useLogger'; import { set } from 'lodash-es'; +import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph'; const moduleLog = log.child({ namespace: 'nodes' }); @@ -408,5 +409,7 @@ export const buildImageToImageGraph = (state: RootState): Graph => { }); } + addControlNetToLinearGraph(graph, LATENTS_TO_LATENTS, state); + return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts index 161f857bd7..ae71f569b6 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts @@ -1,8 +1,6 @@ import { RootState } from 'app/store/store'; import { - CollectInvocation, CompelInvocation, - ControlNetInvocation, Graph, IterateInvocation, LatentsToImageInvocation, @@ -12,7 +10,7 @@ import { TextToLatentsInvocation, } from 'services/api'; import { NonNullableGraph } from 'features/nodes/types/types'; -import { forEach, size } from 'lodash-es'; +import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph'; const POSITIVE_CONDITIONING = 'positive_conditioning'; const NEGATIVE_CONDITIONING = 'negative_conditioning'; @@ -22,7 +20,6 @@ const NOISE = 'noise'; const RANDOM_INT = 'rand_int'; const RANGE_OF_SIZE = 'range_of_size'; const ITERATE = 'iterate'; -const CONTROL_NET_COLLECT = 'control_net_collect'; /** * Builds the Text to Image tab graph. @@ -42,8 +39,6 @@ export const buildTextToImageGraph = (state: RootState): Graph => { shouldRandomizeSeed, } = state.generation; - const { isEnabled: isControlNetEnabled, controlNets } = state.controlNet; - const graph: NonNullableGraph = { nodes: {}, edges: [], @@ -315,91 +310,7 @@ export const buildTextToImageGraph = (state: RootState): Graph => { }); } - // Add ControlNet - if (isControlNetEnabled) { - if (size(controlNets) > 1) { - const controlNetIterateNode: CollectInvocation = { - id: CONTROL_NET_COLLECT, - type: 'collect', - }; - graph.nodes[controlNetIterateNode.id] = controlNetIterateNode; - graph.edges.push({ - source: { node_id: controlNetIterateNode.id, field: 'collection' }, - destination: { - node_id: TEXT_TO_LATENTS, - field: 'control', - }, - }); - } - - forEach(controlNets, (controlNet, index) => { - const { - controlNetId, - isEnabled, - isControlImageProcessed, - controlImage, - processedControlImage, - beginStepPct, - endStepPct, - model, - processorNode, - weight, - } = controlNet; - - if (!isEnabled) { - // Skip disabled ControlNets - return; - } - - const controlNetNode: ControlNetInvocation = { - id: `control_net_${controlNetId}`, - type: 'controlnet', - begin_step_percent: beginStepPct, - end_step_percent: endStepPct, - control_model: model as ControlNetInvocation['control_model'], - control_weight: weight, - }; - - if (processedControlImage && !isControlImageProcessed) { - // We've already processed the image in the app, so we can just use the processed image - const { image_name, image_origin } = processedControlImage; - controlNetNode.image = { - image_name, - image_origin, - }; - } else if (controlImage && isControlImageProcessed) { - // The control image is preprocessed - const { image_name, image_origin } = controlImage; - controlNetNode.image = { - image_name, - image_origin, - }; - } else { - // Skip ControlNets without an unprocessed image - should never happen if everything is working correctly - return; - } - - graph.nodes[controlNetNode.id] = controlNetNode; - - if (size(controlNets) > 1) { - graph.edges.push({ - source: { node_id: controlNetNode.id, field: 'control' }, - destination: { - node_id: CONTROL_NET_COLLECT, - field: 'item', - }, - }); - } else { - graph.edges.push({ - source: { node_id: controlNetNode.id, field: 'control' }, - destination: { - node_id: TEXT_TO_LATENTS, - field: 'control', - }, - }); - } - }); - } + addControlNetToLinearGraph(graph, TEXT_TO_LATENTS, state); return graph; }; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx index 6369ab56de..06c6108dcb 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx @@ -1,18 +1,7 @@ -import { - Divider, - Flex, - Tab, - TabList, - TabPanel, - TabPanels, - Tabs, -} from '@chakra-ui/react'; +import { Divider, Flex } from '@chakra-ui/react'; import { useTranslation } from 'react-i18next'; import IAICollapse from 'common/components/IAICollapse'; import { Fragment, memo, useCallback } from 'react'; -import IAIIconButton from 'common/components/IAIIconButton'; -import { FaPlus } from 'react-icons/fa'; -import ControlNet from 'features/controlNet/components/ControlNet'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { createSelector } from '@reduxjs/toolkit'; import { @@ -23,9 +12,9 @@ import { import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { map } from 'lodash-es'; import { v4 as uuidv4 } from 'uuid'; -import ControlNetMini from 'features/controlNet/components/ControlNetMini'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import IAIButton from 'common/components/IAIButton'; +import ControlNet from 'features/controlNet/components/ControlNet'; const selector = createSelector( controlNetSelector, @@ -62,61 +51,19 @@ const ParamControlNetCollapse = () => { onToggle={handleClickControlNetToggle} withSwitch > - {controlNetsArray.length === 0 && ( - - Add ControlNet - - )} - + {controlNetsArray.map((c, i) => ( {i > 0 && } - + ))} + + Add ControlNet + ); - - return ( - - - - {controlNetsArray.map((c, i) => ( - - {i + 1} - - ))} - } - /> - - - {controlNetsArray.map((c) => ( - - - {/* */} - - ))} - - - - ); }; export default memo(ParamControlNetCollapse); From bf4fe3c1acec8ed0f81806fccd65ec06ed6f41f5 Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Sat, 3 Jun 2023 17:25:03 +1200 Subject: [PATCH 30/47] wip: Fixing layout shifts with the ControlNet tab --- .../controlNet/components/ControlNet.tsx | 111 ++++++++++-------- 1 file changed, 61 insertions(+), 50 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx index fe5f07f89f..6109c75451 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx @@ -1,4 +1,19 @@ +import { + Box, + Checkbox, + Flex, + FormControl, + FormLabel, + HStack, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, +} from '@chakra-ui/react'; +import { useAppDispatch } from 'app/store/storeHooks'; import { memo, useCallback } from 'react'; +import { FaCopy, FaTrash } from 'react-icons/fa'; import { ControlNetConfig, controlNetAdded, @@ -6,34 +21,19 @@ import { controlNetToggled, isControlNetImagePreprocessedToggled, } from '../store/controlNetSlice'; -import { useAppDispatch } from 'app/store/storeHooks'; import ParamControlNetModel from './parameters/ParamControlNetModel'; import ParamControlNetWeight from './parameters/ParamControlNetWeight'; -import { - Checkbox, - Flex, - FormControl, - FormLabel, - HStack, - TabList, - TabPanels, - Tabs, - Tab, - TabPanel, - Box, -} from '@chakra-ui/react'; -import { FaCopy, FaTrash } from 'react-icons/fa'; -import ParamControlNetBeginEnd from './parameters/ParamControlNetBeginEnd'; -import ControlNetImagePreview from './ControlNetImagePreview'; -import IAIIconButton from 'common/components/IAIIconButton'; -import { v4 as uuidv4 } from 'uuid'; -import { useToggle } from 'react-use'; -import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect'; -import ControlNetProcessorComponent from './ControlNetProcessorComponent'; -import ControlNetPreprocessButton from './ControlNetPreprocessButton'; import IAIButton from 'common/components/IAIButton'; +import IAIIconButton from 'common/components/IAIIconButton'; import IAISwitch from 'common/components/IAISwitch'; +import { useToggle } from 'react-use'; +import { v4 as uuidv4 } from 'uuid'; +import ControlNetImagePreview from './ControlNetImagePreview'; +import ControlNetPreprocessButton from './ControlNetPreprocessButton'; +import ControlNetProcessorComponent from './ControlNetProcessorComponent'; +import ParamControlNetBeginEnd from './parameters/ParamControlNetBeginEnd'; +import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect'; type ControlNetProps = { controlNet: ControlNetConfig; @@ -117,19 +117,6 @@ const ControlNet = (props: ControlNetProps) => { {isEnabled && ( <> - {!shouldShowAdvanced && ( - - - - )} { w: 'full', paddingInlineEnd: 2, pb: shouldShowAdvanced ? 0 : 2, - justifyContent: 'space-between', }} > @@ -165,17 +152,41 @@ const ControlNet = (props: ControlNetProps) => { - - + + + + + + {!shouldShowAdvanced && ( + + + + )} + {shouldShowAdvanced && ( From 73a95973a87788ae43a95626371e3f000165619b Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Sat, 3 Jun 2023 17:56:55 +1200 Subject: [PATCH 31/47] wip: Add Wrapper Container for Preprocessor Options For fast altering of the layout across all pre-preocessors. --- .../components/processors/CannyProcessor.tsx | 10 +++++----- .../processors/ContentShuffleProcessor.tsx | 10 +++++----- .../components/processors/HedProcessor.tsx | 10 +++++----- .../components/processors/LineartAnimeProcessor.tsx | 10 +++++----- .../components/processors/LineartProcessor.tsx | 12 ++++++------ .../components/processors/MediapipeFaceProcessor.tsx | 10 +++++----- .../components/processors/MidasDepthProcessor.tsx | 10 +++++----- .../components/processors/MlsdImageProcessor.tsx | 10 +++++----- .../components/processors/NormalBaeProcessor.tsx | 10 +++++----- .../components/processors/OpenposeProcessor.tsx | 12 ++++++------ .../components/processors/PidiProcessor.tsx | 12 ++++++------ .../components/processors/ZoeDepthProcessor.tsx | 2 +- .../processors/shared/ProcessorOptionsContainer.tsx | 12 ++++++++++++ 13 files changed, 71 insertions(+), 59 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlNet/components/processors/shared/ProcessorOptionsContainer.tsx diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx index 336d7d8bab..54aaca4eaf 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx @@ -1,9 +1,9 @@ -import { Flex } from '@chakra-ui/react'; import IAISlider from 'common/components/IAISlider'; +import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; +import { RequiredCannyImageProcessorInvocation } from 'features/controlNet/store/types'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import { RequiredCannyImageProcessorInvocation } from 'features/controlNet/store/types'; -import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; +import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; const DEFAULTS = CONTROLNET_PROCESSORS.canny_image_processor.default; @@ -44,7 +44,7 @@ const CannyProcessor = (props: CannyProcessorProps) => { }, [controlNetId, processorChanged]); return ( - + { max={255} withInput /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/ContentShuffleProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/ContentShuffleProcessor.tsx index 0d8b85b89b..5c83cca2ea 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/ContentShuffleProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/ContentShuffleProcessor.tsx @@ -1,9 +1,9 @@ -import { Flex } from '@chakra-ui/react'; import IAISlider from 'common/components/IAISlider'; +import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; +import { RequiredContentShuffleImageProcessorInvocation } from 'features/controlNet/store/types'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import { RequiredContentShuffleImageProcessorInvocation } from 'features/controlNet/store/types'; -import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; +import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; const DEFAULTS = CONTROLNET_PROCESSORS.content_shuffle_image_processor.default; @@ -83,7 +83,7 @@ const ContentShuffleProcessor = (props: Props) => { }, [controlNetId, processorChanged]); return ( - + { max={4096} withInput /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx index 23f79d69e2..4a55c2694f 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx @@ -1,10 +1,10 @@ -import { Flex } from '@chakra-ui/react'; import IAISlider from 'common/components/IAISlider'; import IAISwitch from 'common/components/IAISwitch'; +import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; +import { RequiredHedImageProcessorInvocation } from 'features/controlNet/store/types'; import { ChangeEvent, memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import { RequiredHedImageProcessorInvocation } from 'features/controlNet/store/types'; -import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; +import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; const DEFAULTS = CONTROLNET_PROCESSORS.hed_image_processor.default; @@ -55,7 +55,7 @@ const HedPreprocessor = (props: HedProcessorProps) => { }, [controlNetId, processorChanged]); return ( - + { isChecked={scribble} onChange={handleScribbleChanged} /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx index 1ccdcbd197..f64508e48c 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx @@ -1,9 +1,9 @@ -import { Flex } from '@chakra-ui/react'; import IAISlider from 'common/components/IAISlider'; +import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; +import { RequiredLineartAnimeImageProcessorInvocation } from 'features/controlNet/store/types'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import { RequiredLineartAnimeImageProcessorInvocation } from 'features/controlNet/store/types'; -import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; +import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; const DEFAULTS = CONTROLNET_PROCESSORS.lineart_anime_image_processor.default; @@ -44,7 +44,7 @@ const LineartAnimeProcessor = (props: Props) => { }, [controlNetId, processorChanged]); return ( - + { max={4096} withInput /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx index 4376a0cbc3..13f889f245 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx @@ -1,10 +1,10 @@ -import { Flex } from '@chakra-ui/react'; import IAISlider from 'common/components/IAISlider'; -import { ChangeEvent, memo, useCallback } from 'react'; -import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import { RequiredLineartImageProcessorInvocation } from 'features/controlNet/store/types'; import IAISwitch from 'common/components/IAISwitch'; import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; +import { RequiredLineartImageProcessorInvocation } from 'features/controlNet/store/types'; +import { ChangeEvent, memo, useCallback } from 'react'; +import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; +import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; const DEFAULTS = CONTROLNET_PROCESSORS.lineart_image_processor.default; @@ -52,7 +52,7 @@ const LineartProcessor = (props: LineartProcessorProps) => { ); return ( - + { isChecked={coarse} onChange={handleCoarseChanged} /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/MediapipeFaceProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/MediapipeFaceProcessor.tsx index 9a044560cf..b7a56a9a6b 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/MediapipeFaceProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/MediapipeFaceProcessor.tsx @@ -1,9 +1,9 @@ -import { Flex } from '@chakra-ui/react'; import IAISlider from 'common/components/IAISlider'; +import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; +import { RequiredMediapipeFaceProcessorInvocation } from 'features/controlNet/store/types'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import { RequiredMediapipeFaceProcessorInvocation } from 'features/controlNet/store/types'; -import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; +import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; const DEFAULTS = CONTROLNET_PROCESSORS.mediapipe_face_processor.default; @@ -40,7 +40,7 @@ const MediapipeFaceProcessor = (props: Props) => { }, [controlNetId, processorChanged]); return ( - + { step={0.01} withInput /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/MidasDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/MidasDepthProcessor.tsx index ece69e7f34..84363b03bb 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/MidasDepthProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/MidasDepthProcessor.tsx @@ -1,9 +1,9 @@ -import { Flex } from '@chakra-ui/react'; import IAISlider from 'common/components/IAISlider'; +import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; +import { RequiredMidasDepthImageProcessorInvocation } from 'features/controlNet/store/types'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import { RequiredMidasDepthImageProcessorInvocation } from 'features/controlNet/store/types'; -import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; +import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; const DEFAULTS = CONTROLNET_PROCESSORS.midas_depth_image_processor.default; @@ -40,7 +40,7 @@ const MidasDepthProcessor = (props: Props) => { }, [controlNetId, processorChanged]); return ( - + { step={0.01} withInput /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/MlsdImageProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/MlsdImageProcessor.tsx index 9b15935ea7..271a55cd83 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/MlsdImageProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/MlsdImageProcessor.tsx @@ -1,9 +1,9 @@ -import { Flex } from '@chakra-ui/react'; import IAISlider from 'common/components/IAISlider'; +import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; +import { RequiredMlsdImageProcessorInvocation } from 'features/controlNet/store/types'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import { RequiredMlsdImageProcessorInvocation } from 'features/controlNet/store/types'; -import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; +import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; const DEFAULTS = CONTROLNET_PROCESSORS.mlsd_image_processor.default; @@ -66,7 +66,7 @@ const MlsdImageProcessor = (props: Props) => { }, [controlNetId, processorChanged]); return ( - + { step={0.01} withInput /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/NormalBaeProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/NormalBaeProcessor.tsx index 79b6885669..bd5477e716 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/NormalBaeProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/NormalBaeProcessor.tsx @@ -1,9 +1,9 @@ -import { Flex } from '@chakra-ui/react'; import IAISlider from 'common/components/IAISlider'; +import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; +import { RequiredNormalbaeImageProcessorInvocation } from 'features/controlNet/store/types'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import { RequiredNormalbaeImageProcessorInvocation } from 'features/controlNet/store/types'; -import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; +import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; const DEFAULTS = CONTROLNET_PROCESSORS.normalbae_image_processor.default; @@ -44,7 +44,7 @@ const NormalBaeProcessor = (props: Props) => { }, [controlNetId, processorChanged]); return ( - + { max={4096} withInput /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/OpenposeProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/OpenposeProcessor.tsx index 40619a6d5f..bbb7db316f 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/OpenposeProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/OpenposeProcessor.tsx @@ -1,10 +1,10 @@ -import { Flex } from '@chakra-ui/react'; import IAISlider from 'common/components/IAISlider'; -import { ChangeEvent, memo, useCallback } from 'react'; -import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import { RequiredOpenposeImageProcessorInvocation } from 'features/controlNet/store/types'; import IAISwitch from 'common/components/IAISwitch'; import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; +import { RequiredOpenposeImageProcessorInvocation } from 'features/controlNet/store/types'; +import { ChangeEvent, memo, useCallback } from 'react'; +import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; +import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; const DEFAULTS = CONTROLNET_PROCESSORS.openpose_image_processor.default; @@ -52,7 +52,7 @@ const OpenposeProcessor = (props: Props) => { ); return ( - + { isChecked={hand_and_face} onChange={handleHandAndFaceChanged} /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/PidiProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/PidiProcessor.tsx index a5e82ee8d0..2309c375d6 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/PidiProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/PidiProcessor.tsx @@ -1,10 +1,10 @@ -import { Flex } from '@chakra-ui/react'; import IAISlider from 'common/components/IAISlider'; -import { ChangeEvent, memo, useCallback } from 'react'; -import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import { RequiredPidiImageProcessorInvocation } from 'features/controlNet/store/types'; import IAISwitch from 'common/components/IAISwitch'; import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; +import { RequiredPidiImageProcessorInvocation } from 'features/controlNet/store/types'; +import { ChangeEvent, memo, useCallback } from 'react'; +import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; +import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; const DEFAULTS = CONTROLNET_PROCESSORS.pidi_image_processor.default; @@ -59,7 +59,7 @@ const PidiProcessor = (props: Props) => { ); return ( - + { onChange={handleScribbleChanged} /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/ZoeDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/ZoeDepthProcessor.tsx index 20a1ec4493..d0a34784bf 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/ZoeDepthProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/ZoeDepthProcessor.tsx @@ -1,5 +1,5 @@ -import { memo } from 'react'; import { RequiredZoeDepthImageProcessorInvocation } from 'features/controlNet/store/types'; +import { memo } from 'react'; type Props = { controlNetId: string; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/shared/ProcessorOptionsContainer.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/shared/ProcessorOptionsContainer.tsx new file mode 100644 index 0000000000..5cd7ff61ae --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/shared/ProcessorOptionsContainer.tsx @@ -0,0 +1,12 @@ +import { Flex } from '@chakra-ui/react'; +import { PropsWithChildren } from 'react'; + +type ProcessorOptionsContainerProps = PropsWithChildren; + +export default function ProcessorOptionsContainer( + props: ProcessorOptionsContainerProps +) { + return ( + {props.children} + ); +} From aa3a969bd2a267973e601dd812bb0f29126a17d3 Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Sat, 3 Jun 2023 18:21:06 +1200 Subject: [PATCH 32/47] feat: Update ControlNet Model List & Map --- .../features/controlNet/store/constants.ts | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlNet/store/constants.ts b/invokeai/frontend/web/src/features/controlNet/store/constants.ts index da3a9c57b5..310bea4d12 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/constants.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/constants.ts @@ -166,14 +166,21 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = { }; export const CONTROLNET_MODELS = [ - 'lllyasviel/sd-controlnet-canny', - 'lllyasviel/sd-controlnet-depth', - 'lllyasviel/sd-controlnet-hed', - 'lllyasviel/sd-controlnet-seg', - 'lllyasviel/sd-controlnet-openpose', - 'lllyasviel/sd-controlnet-scribble', - 'lllyasviel/sd-controlnet-normal', - 'lllyasviel/sd-controlnet-mlsd', + 'lllyasviel/control_v11p_sd15_canny', + 'lllyasviel/control_v11p_sd15_inpaint', + 'lllyasviel/control_v11p_sd15_mlsd', + 'lllyasviel/control_v11f1p_sd15_depth', + 'lllyasviel/control_v11p_sd15_normalbae', + 'lllyasviel/control_v11p_sd15_seg', + 'lllyasviel/control_v11p_sd15_lineart', + 'lllyasviel/control_v11p_sd15s2_lineart_anime', + 'lllyasviel/control_v11p_sd15_scribble', + 'lllyasviel/control_v11p_sd15_softedge', + 'lllyasviel/control_v11e_sd15_shuffle', + 'lllyasviel/control_v11p_sd15_openpose', + 'lllyasviel/control_v11f1e_sd15_tile', + 'lllyasviel/control_v11e_sd15_ip2p', + 'CrucibleAI/ControlNetMediaPipeFace', ]; export type ControlNetModel = (typeof CONTROLNET_MODELS)[number]; @@ -182,9 +189,15 @@ export const CONTROLNET_MODEL_MAP: Record< ControlNetModel, ControlNetProcessorType > = { - 'lllyasviel/sd-controlnet-canny': 'canny_image_processor', - 'lllyasviel/sd-controlnet-depth': 'midas_depth_image_processor', - 'lllyasviel/sd-controlnet-hed': 'hed_image_processor', - 'lllyasviel/sd-controlnet-openpose': 'openpose_image_processor', - 'lllyasviel/sd-controlnet-mlsd': 'mlsd_image_processor', + 'lllyasviel/control_v11p_sd15_canny': 'canny_image_processor', + 'lllyasviel/control_v11p_sd15_mlsd': 'mlsd_image_processor', + 'lllyasviel/control_v11f1p_sd15_depth': 'midas_depth_image_processor', + 'lllyasviel/control_v11p_sd15_normalbae': 'normalbae_image_processor', + 'lllyasviel/control_v11p_sd15_lineart': 'lineart_image_processor', + 'lllyasviel/control_v11p_sd15s2_lineart_anime': + 'lineart_anime_image_processor', + 'lllyasviel/control_v11p_sd15_softedge': 'hed_image_processor', + 'lllyasviel/control_v11e_sd15_shuffle': 'content_shuffle_image_processor', + 'lllyasviel/control_v11p_sd15_openpose': 'openpose_image_processor', + 'CrucibleAI/ControlNetMediaPipeFace': 'mediapipe_face_processor', }; From d0406024e30df2fe15015f94cf4b6f7a2850bda7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 3 Jun 2023 20:39:14 +1000 Subject: [PATCH 33/47] feat(ui): IAICustomSelect tweak styles --- .../src/common/components/IAICustomSelect.tsx | 135 ++++++++++-------- .../src/theme/util/getInputOutlineStyles.ts | 2 +- 2 files changed, 79 insertions(+), 58 deletions(-) diff --git a/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx b/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx index e7c7fd8f97..d1a6ff97e2 100644 --- a/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx +++ b/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx @@ -1,4 +1,4 @@ -import { CheckIcon } from '@chakra-ui/icons'; +import { CheckIcon, ChevronUpIcon } from '@chakra-ui/icons'; import { Box, Flex, @@ -10,7 +10,6 @@ import { GridItem, List, ListItem, - Select, Text, Tooltip, TooltipProps, @@ -20,6 +19,7 @@ import { useSelect } from 'downshift'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import { memo } from 'react'; +import { getInputOutlineStyles } from 'theme/util/getInputOutlineStyles'; export type ItemTooltips = { [key: string]: string }; @@ -82,16 +82,20 @@ const IAICustomSelect = (props: IAICustomSelectProps) => { )} - + + {isOpen && ( @@ -115,9 +127,8 @@ const IAICustomSelect = (props: IAICustomSelectProps) => { ref={refs.setFloating} sx={{ ...floatingStyles, - width: 'full', top: 0, - left: 0, + insetInlineStart: 0, flexDirection: 'column', zIndex: 2, bg: 'base.800', @@ -133,58 +144,68 @@ const IAICustomSelect = (props: IAICustomSelectProps) => { }} > - {items.map((item, index) => ( - - { + const isSelected = selectedItem === item; + const isHighlighted = highlightedIndex === index; + const fontWeight = isSelected ? 700 : 500; + const bg = isHighlighted + ? 'base.700' + : isSelected + ? 'base.750' + : undefined; + return ( + - {withCheckIcon ? ( - - - {selectedItem === item && } - - - - {item} - - - - ) : ( - - {item} - - )} - - - ))} + + {withCheckIcon ? ( + + + {isSelected && } + + + + {item} + + + + ) : ( + + {item} + + )} + + + ); + })} )} diff --git a/invokeai/frontend/web/src/theme/util/getInputOutlineStyles.ts b/invokeai/frontend/web/src/theme/util/getInputOutlineStyles.ts index 85e9d109c5..469bf47be4 100644 --- a/invokeai/frontend/web/src/theme/util/getInputOutlineStyles.ts +++ b/invokeai/frontend/web/src/theme/util/getInputOutlineStyles.ts @@ -1,6 +1,6 @@ import { StyleFunctionProps } from '@chakra-ui/theme-tools'; -export const getInputOutlineStyles = (_props: StyleFunctionProps) => ({ +export const getInputOutlineStyles = (_props?: StyleFunctionProps) => ({ outline: 'none', borderWidth: 2, borderStyle: 'solid', From f269377a01f26baf1c1bdaea47a7dd9068436865 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 3 Jun 2023 20:42:51 +1000 Subject: [PATCH 34/47] feat(ui): "ProcessorOptionsContainer" -> "ProcessorWrapper", organise --- .../controlNet/components/processors/CannyProcessor.tsx | 6 +++--- .../components/processors/ContentShuffleProcessor.tsx | 6 +++--- .../controlNet/components/processors/HedProcessor.tsx | 6 +++--- .../components/processors/LineartAnimeProcessor.tsx | 6 +++--- .../controlNet/components/processors/LineartProcessor.tsx | 6 +++--- .../components/processors/MediapipeFaceProcessor.tsx | 6 +++--- .../components/processors/MidasDepthProcessor.tsx | 6 +++--- .../controlNet/components/processors/MlsdImageProcessor.tsx | 6 +++--- .../controlNet/components/processors/NormalBaeProcessor.tsx | 6 +++--- .../controlNet/components/processors/OpenposeProcessor.tsx | 6 +++--- .../controlNet/components/processors/PidiProcessor.tsx | 6 +++--- .../ProcessorWrapper.tsx} | 6 ++---- 12 files changed, 35 insertions(+), 37 deletions(-) rename invokeai/frontend/web/src/features/controlNet/components/processors/{shared/ProcessorOptionsContainer.tsx => common/ProcessorWrapper.tsx} (54%) diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx index 54aaca4eaf..6887d1abb0 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/CannyProcessor.tsx @@ -3,7 +3,7 @@ import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; import { RequiredCannyImageProcessorInvocation } from 'features/controlNet/store/types'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; +import ProcessorWrapper from './common/ProcessorWrapper'; const DEFAULTS = CONTROLNET_PROCESSORS.canny_image_processor.default; @@ -44,7 +44,7 @@ const CannyProcessor = (props: CannyProcessorProps) => { }, [controlNetId, processorChanged]); return ( - + { max={255} withInput /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/ContentShuffleProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/ContentShuffleProcessor.tsx index 5c83cca2ea..7ce6ab2297 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/ContentShuffleProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/ContentShuffleProcessor.tsx @@ -3,7 +3,7 @@ import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; import { RequiredContentShuffleImageProcessorInvocation } from 'features/controlNet/store/types'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; +import ProcessorWrapper from './common/ProcessorWrapper'; const DEFAULTS = CONTROLNET_PROCESSORS.content_shuffle_image_processor.default; @@ -83,7 +83,7 @@ const ContentShuffleProcessor = (props: Props) => { }, [controlNetId, processorChanged]); return ( - + { max={4096} withInput /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx index 4a55c2694f..a1aced5a8f 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/HedProcessor.tsx @@ -4,7 +4,7 @@ import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; import { RequiredHedImageProcessorInvocation } from 'features/controlNet/store/types'; import { ChangeEvent, memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; +import ProcessorWrapper from './common/ProcessorWrapper'; const DEFAULTS = CONTROLNET_PROCESSORS.hed_image_processor.default; @@ -55,7 +55,7 @@ const HedPreprocessor = (props: HedProcessorProps) => { }, [controlNetId, processorChanged]); return ( - + { isChecked={scribble} onChange={handleScribbleChanged} /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx index f64508e48c..17dc9b43df 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartAnimeProcessor.tsx @@ -3,7 +3,7 @@ import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; import { RequiredLineartAnimeImageProcessorInvocation } from 'features/controlNet/store/types'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; +import ProcessorWrapper from './common/ProcessorWrapper'; const DEFAULTS = CONTROLNET_PROCESSORS.lineart_anime_image_processor.default; @@ -44,7 +44,7 @@ const LineartAnimeProcessor = (props: Props) => { }, [controlNetId, processorChanged]); return ( - + { max={4096} withInput /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx index 13f889f245..99765ff62f 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/LineartProcessor.tsx @@ -4,7 +4,7 @@ import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; import { RequiredLineartImageProcessorInvocation } from 'features/controlNet/store/types'; import { ChangeEvent, memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; +import ProcessorWrapper from './common/ProcessorWrapper'; const DEFAULTS = CONTROLNET_PROCESSORS.lineart_image_processor.default; @@ -52,7 +52,7 @@ const LineartProcessor = (props: LineartProcessorProps) => { ); return ( - + { isChecked={coarse} onChange={handleCoarseChanged} /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/MediapipeFaceProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/MediapipeFaceProcessor.tsx index b7a56a9a6b..6e1a3959f2 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/MediapipeFaceProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/MediapipeFaceProcessor.tsx @@ -3,7 +3,7 @@ import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; import { RequiredMediapipeFaceProcessorInvocation } from 'features/controlNet/store/types'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; +import ProcessorWrapper from './common/ProcessorWrapper'; const DEFAULTS = CONTROLNET_PROCESSORS.mediapipe_face_processor.default; @@ -40,7 +40,7 @@ const MediapipeFaceProcessor = (props: Props) => { }, [controlNetId, processorChanged]); return ( - + { step={0.01} withInput /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/MidasDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/MidasDepthProcessor.tsx index 84363b03bb..a552c90f3a 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/MidasDepthProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/MidasDepthProcessor.tsx @@ -3,7 +3,7 @@ import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; import { RequiredMidasDepthImageProcessorInvocation } from 'features/controlNet/store/types'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; +import ProcessorWrapper from './common/ProcessorWrapper'; const DEFAULTS = CONTROLNET_PROCESSORS.midas_depth_image_processor.default; @@ -40,7 +40,7 @@ const MidasDepthProcessor = (props: Props) => { }, [controlNetId, processorChanged]); return ( - + { step={0.01} withInput /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/MlsdImageProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/MlsdImageProcessor.tsx index 271a55cd83..d753d3b266 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/MlsdImageProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/MlsdImageProcessor.tsx @@ -3,7 +3,7 @@ import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; import { RequiredMlsdImageProcessorInvocation } from 'features/controlNet/store/types'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; +import ProcessorWrapper from './common/ProcessorWrapper'; const DEFAULTS = CONTROLNET_PROCESSORS.mlsd_image_processor.default; @@ -66,7 +66,7 @@ const MlsdImageProcessor = (props: Props) => { }, [controlNetId, processorChanged]); return ( - + { step={0.01} withInput /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/NormalBaeProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/NormalBaeProcessor.tsx index bd5477e716..ea3270adb3 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/NormalBaeProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/NormalBaeProcessor.tsx @@ -3,7 +3,7 @@ import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; import { RequiredNormalbaeImageProcessorInvocation } from 'features/controlNet/store/types'; import { memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; +import ProcessorWrapper from './common/ProcessorWrapper'; const DEFAULTS = CONTROLNET_PROCESSORS.normalbae_image_processor.default; @@ -44,7 +44,7 @@ const NormalBaeProcessor = (props: Props) => { }, [controlNetId, processorChanged]); return ( - + { max={4096} withInput /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/OpenposeProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/OpenposeProcessor.tsx index bbb7db316f..57b45fffa4 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/OpenposeProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/OpenposeProcessor.tsx @@ -4,7 +4,7 @@ import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; import { RequiredOpenposeImageProcessorInvocation } from 'features/controlNet/store/types'; import { ChangeEvent, memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; +import ProcessorWrapper from './common/ProcessorWrapper'; const DEFAULTS = CONTROLNET_PROCESSORS.openpose_image_processor.default; @@ -52,7 +52,7 @@ const OpenposeProcessor = (props: Props) => { ); return ( - + { isChecked={hand_and_face} onChange={handleHandAndFaceChanged} /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/PidiProcessor.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/PidiProcessor.tsx index 2309c375d6..7fb5b92b9c 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/PidiProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/PidiProcessor.tsx @@ -4,7 +4,7 @@ import { CONTROLNET_PROCESSORS } from 'features/controlNet/store/constants'; import { RequiredPidiImageProcessorInvocation } from 'features/controlNet/store/types'; import { ChangeEvent, memo, useCallback } from 'react'; import { useProcessorNodeChanged } from '../hooks/useProcessorNodeChanged'; -import ProcessorOptionsContainer from './shared/ProcessorOptionsContainer'; +import ProcessorWrapper from './common/ProcessorWrapper'; const DEFAULTS = CONTROLNET_PROCESSORS.pidi_image_processor.default; @@ -59,7 +59,7 @@ const PidiProcessor = (props: Props) => { ); return ( - + { onChange={handleScribbleChanged} /> - + ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/processors/shared/ProcessorOptionsContainer.tsx b/invokeai/frontend/web/src/features/controlNet/components/processors/common/ProcessorWrapper.tsx similarity index 54% rename from invokeai/frontend/web/src/features/controlNet/components/processors/shared/ProcessorOptionsContainer.tsx rename to invokeai/frontend/web/src/features/controlNet/components/processors/common/ProcessorWrapper.tsx index 5cd7ff61ae..f1c9cb8048 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/processors/shared/ProcessorOptionsContainer.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/processors/common/ProcessorWrapper.tsx @@ -1,11 +1,9 @@ import { Flex } from '@chakra-ui/react'; import { PropsWithChildren } from 'react'; -type ProcessorOptionsContainerProps = PropsWithChildren; +type Props = PropsWithChildren; -export default function ProcessorOptionsContainer( - props: ProcessorOptionsContainerProps -) { +export default function ProcessorWrapper(props: Props) { return ( {props.children} ); From 5dc0250b00fa3e01bdefa3a866e73fa5bf21e0ac Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 3 Jun 2023 20:43:53 +1000 Subject: [PATCH 35/47] feat(ui): ControlNet layout tweaks --- .../controlNet/components/ControlNet.tsx | 132 +++++++++--------- .../parameters/ParamControlNetModel.tsx | 1 + .../ParamControlNetProcessorSelect.tsx | 1 + .../features/controlNet/store/constants.ts | 3 +- 4 files changed, 67 insertions(+), 70 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx index 6109c75451..2f6e1f676b 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx @@ -1,19 +1,4 @@ -import { - Box, - Checkbox, - Flex, - FormControl, - FormLabel, - HStack, - Tab, - TabList, - TabPanel, - TabPanels, - Tabs, -} from '@chakra-ui/react'; -import { useAppDispatch } from 'app/store/storeHooks'; import { memo, useCallback } from 'react'; -import { FaCopy, FaTrash } from 'react-icons/fa'; import { ControlNetConfig, controlNetAdded, @@ -21,19 +6,34 @@ import { controlNetToggled, isControlNetImagePreprocessedToggled, } from '../store/controlNetSlice'; +import { useAppDispatch } from 'app/store/storeHooks'; import ParamControlNetModel from './parameters/ParamControlNetModel'; import ParamControlNetWeight from './parameters/ParamControlNetWeight'; +import { + Checkbox, + Flex, + FormControl, + FormLabel, + HStack, + TabList, + TabPanels, + Tabs, + Tab, + TabPanel, + Box, +} from '@chakra-ui/react'; +import { FaCopy, FaTrash } from 'react-icons/fa'; -import IAIButton from 'common/components/IAIButton'; -import IAIIconButton from 'common/components/IAIIconButton'; -import IAISwitch from 'common/components/IAISwitch'; -import { useToggle } from 'react-use'; -import { v4 as uuidv4 } from 'uuid'; -import ControlNetImagePreview from './ControlNetImagePreview'; -import ControlNetPreprocessButton from './ControlNetPreprocessButton'; -import ControlNetProcessorComponent from './ControlNetProcessorComponent'; import ParamControlNetBeginEnd from './parameters/ParamControlNetBeginEnd'; +import ControlNetImagePreview from './ControlNetImagePreview'; +import IAIIconButton from 'common/components/IAIIconButton'; +import { v4 as uuidv4 } from 'uuid'; +import { useToggle } from 'react-use'; import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect'; +import ControlNetProcessorComponent from './ControlNetProcessorComponent'; +import ControlNetPreprocessButton from './ControlNetPreprocessButton'; +import IAIButton from 'common/components/IAIButton'; +import IAISwitch from 'common/components/IAISwitch'; type ControlNetProps = { controlNet: ControlNetConfig; @@ -48,7 +48,7 @@ const ControlNet = (props: ControlNetProps) => { beginStepPct, endStepPct, controlImage, - isPreprocessed: isControlImageProcessed, + isPreprocessed, processedControlImage, processorNode, } = props.controlNet; @@ -83,18 +83,21 @@ const ControlNet = (props: ControlNetProps) => { borderRadius: 'base', }} > - + @@ -113,7 +116,7 @@ const ControlNet = (props: ControlNetProps) => { onClick={handleDelete} icon={} /> - + {isEnabled && ( <> @@ -122,21 +125,23 @@ const ControlNet = (props: ControlNetProps) => { flexDir: 'column', gap: 2, w: 'full', - paddingInlineEnd: 2, - pb: shouldShowAdvanced ? 0 : 2, + h: 32, + paddingInlineStart: 2, + paddingInlineEnd: shouldShowAdvanced ? 2 : 0, + pb: 2, + justifyContent: 'space-between', }} > Preprocessed @@ -152,49 +157,38 @@ const ControlNet = (props: ControlNetProps) => { + + + + {!shouldShowAdvanced && ( - - - - - {!shouldShowAdvanced && ( - - - - )} + - + )} {shouldShowAdvanced && ( <> - {!isControlImageProcessed && ( + {!isPreprocessed && ( <> { items={CONTROLNET_MODELS} selectedItem={model} setSelectedItem={handleModelChanged} + withCheckIcon /> ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetProcessorSelect.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetProcessorSelect.tsx index 9d21727a3c..019b5ef849 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetProcessorSelect.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetProcessorSelect.tsx @@ -39,6 +39,7 @@ const ParamControlNetProcessorSelect = ( items={CONTROLNET_PROCESSOR_TYPES} selectedItem={processorNode.type ?? 'canny_image_processor'} setSelectedItem={handleProcessorTypeChanged} + withCheckIcon /> ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/store/constants.ts b/invokeai/frontend/web/src/features/controlNet/store/constants.ts index 310bea4d12..b022d81b3a 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/constants.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/constants.ts @@ -1,5 +1,6 @@ import { ControlNetProcessorType, + RequiredCannyImageProcessorInvocation, RequiredControlNetProcessorNode, } from './types'; @@ -22,7 +23,7 @@ type ControlNetProcessorsDict = Record< * * TODO: Generate from the OpenAPI schema */ -export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = { +export const CONTROLNET_PROCESSORS = { canny_image_processor: { type: 'canny_image_processor', label: 'Canny', From 474fca8e6a3c62fa4b994a67ba89f38c8a45b6ef Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 3 Jun 2023 21:00:57 +1000 Subject: [PATCH 36/47] feat(ui): add controlNetDenylist --- .../src/app/store/enhancers/reduxRemember/serialize.ts | 2 ++ .../src/features/controlNet/store/controlNetDenylist.ts | 8 ++++++++ 2 files changed, 10 insertions(+) create mode 100644 invokeai/frontend/web/src/features/controlNet/store/controlNetDenylist.ts diff --git a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/serialize.ts b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/serialize.ts index 9fb4ceae32..5025ca081a 100644 --- a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/serialize.ts +++ b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/serialize.ts @@ -1,4 +1,5 @@ import { canvasPersistDenylist } from 'features/canvas/store/canvasPersistDenylist'; +import { controlNetDenylist } from 'features/controlNet/store/controlNetDenylist'; import { galleryPersistDenylist } from 'features/gallery/store/galleryPersistDenylist'; import { lightboxPersistDenylist } from 'features/lightbox/store/lightboxPersistDenylist'; import { nodesPersistDenylist } from 'features/nodes/store/nodesPersistDenylist'; @@ -23,6 +24,7 @@ const serializationDenylist: { system: systemPersistDenylist, // config: configPersistDenyList, ui: uiPersistDenylist, + controlNet: controlNetDenylist, // hotkeys: hotkeysPersistDenylist, }; diff --git a/invokeai/frontend/web/src/features/controlNet/store/controlNetDenylist.ts b/invokeai/frontend/web/src/features/controlNet/store/controlNetDenylist.ts new file mode 100644 index 0000000000..07eab8120f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/store/controlNetDenylist.ts @@ -0,0 +1,8 @@ +import { ControlNetState } from './controlNetSlice'; + +/** + * ControlNet slice persist denylist + */ +export const controlNetDenylist: (keyof ControlNetState)[] = [ + 'isProcessingControlImage', +]; From fa285883ad3557cc60f61096d953030eb71f399f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 3 Jun 2023 22:46:33 +1000 Subject: [PATCH 37/47] feat(ui): make OverlayDragImage translucent --- .../web/src/app/components/ImageDnd/OverlayDragImage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx b/invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx index deec1e96d2..510dadc823 100644 --- a/invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx +++ b/invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx @@ -17,6 +17,7 @@ const OverlayDragImage = (props: OverlayDragImageProps) => { justifyContent: 'center', userSelect: 'none', cursor: 'grabbing', + opacity: 0.5, }} > Date: Sat, 3 Jun 2023 22:47:21 +1000 Subject: [PATCH 38/47] feat(ui): add ellipsis direction to IAICustomSelect --- .../web/src/common/components/IAICustomSelect.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx b/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx index d1a6ff97e2..9accceb846 100644 --- a/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx +++ b/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx @@ -18,7 +18,7 @@ import { autoUpdate, offset, shift, useFloating } from '@floating-ui/react-dom'; import { useSelect } from 'downshift'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { getInputOutlineStyles } from 'theme/util/getInputOutlineStyles'; export type ItemTooltips = { [key: string]: string }; @@ -34,6 +34,7 @@ type IAICustomSelectProps = { buttonProps?: FlexProps; tooltip?: string; tooltipProps?: Omit; + ellipsisPosition?: 'start' | 'end'; }; const IAICustomSelect = (props: IAICustomSelectProps) => { @@ -48,6 +49,7 @@ const IAICustomSelect = (props: IAICustomSelectProps) => { tooltip, buttonProps, tooltipProps, + ellipsisPosition = 'end', } = props; const { @@ -69,6 +71,14 @@ const IAICustomSelect = (props: IAICustomSelectProps) => { middleware: [offset(4), shift({ crossAxis: true, padding: 8 })], }); + const labelTextDirection = useMemo(() => { + if (ellipsisPosition === 'start') { + return document.dir === 'rtl' ? 'ltr' : 'rtl'; + } + + return document.dir; + }, [ellipsisPosition]); + return ( {label && ( @@ -106,6 +116,7 @@ const IAICustomSelect = (props: IAICustomSelectProps) => { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', + direction: labelTextDirection, }} > {selectedItem} From 2270c270ef99461266316b1dd47e4d70d8528add Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 3 Jun 2023 22:47:51 +1000 Subject: [PATCH 39/47] feat(ui): add tooltip to IAISwitch --- .../web/src/common/components/IAISwitch.tsx | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/invokeai/frontend/web/src/common/components/IAISwitch.tsx b/invokeai/frontend/web/src/common/components/IAISwitch.tsx index 9a7ba7eb76..33c46c4aeb 100644 --- a/invokeai/frontend/web/src/common/components/IAISwitch.tsx +++ b/invokeai/frontend/web/src/common/components/IAISwitch.tsx @@ -5,6 +5,7 @@ import { FormLabelProps, Switch, SwitchProps, + Tooltip, } from '@chakra-ui/react'; import { memo } from 'react'; @@ -13,6 +14,7 @@ interface Props extends SwitchProps { width?: string | number; formControlProps?: FormControlProps; formLabelProps?: FormLabelProps; + tooltip?: string; } /** @@ -25,24 +27,27 @@ const IAISwitch = (props: Props) => { width = 'auto', formControlProps, formLabelProps, + tooltip, ...rest } = props; return ( - - {label && ( - - {label} - - )} - - + + + {label && ( + + {label} + + )} + + + ); }; From 03f3ad435a691ee2370af94caabbfa09ba247e76 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 3 Jun 2023 22:48:16 +1000 Subject: [PATCH 40/47] feat(ui): updated controlnet logic/ui --- .../listeners/controlNetAutoProcess.ts | 16 ++- .../controlNet/components/ControlNet.tsx | 99 ++++++++----------- .../components/ControlNetImagePreview.tsx | 12 +-- .../parameters/ParamControlNetModel.tsx | 3 + .../processors/common/ProcessorWrapper.tsx | 4 +- .../features/controlNet/store/constants.ts | 8 ++ .../controlNet/store/controlNetSlice.ts | 16 +-- .../src/features/controlNet/store/types.ts | 2 +- .../nodes/util/addControlNetToLinearGraph.ts | 7 +- 9 files changed, 73 insertions(+), 94 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess.ts index d53907e673..9f98b8f25e 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess.ts @@ -6,7 +6,6 @@ import { controlNetImageChanged, controlNetProcessorParamsChanged, controlNetProcessorTypeChanged, - isControlNetImagePreprocessedToggled, } from 'features/controlNet/store/controlNetSlice'; import { RootState } from 'app/store/store'; @@ -16,25 +15,22 @@ const predicate = (action: AnyAction, state: RootState) => { const isActionMatched = controlNetProcessorParamsChanged.match(action) || controlNetImageChanged.match(action) || - controlNetProcessorTypeChanged.match(action) || - isControlNetImagePreprocessedToggled.match(action); + controlNetProcessorTypeChanged.match(action); if (!isActionMatched) { return false; } - const { controlNetId } = action.payload; + const { controlImage, processorType } = + state.controlNet.controlNets[action.payload.controlNetId]; - const shouldAutoProcess = - !state.controlNet.controlNets[controlNetId].isPreprocessed; + const isProcessorSelected = processorType !== 'none'; const isBusy = state.system.isProcessing; - const hasControlImage = Boolean( - state.controlNet.controlNets[controlNetId].controlImage - ); + const hasControlImage = Boolean(controlImage); - return shouldAutoProcess && !isBusy && hasControlImage; + return isProcessorSelected && !isBusy && hasControlImage; }; /** diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx index 2f6e1f676b..903d453446 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx @@ -4,7 +4,6 @@ import { controlNetAdded, controlNetRemoved, controlNetToggled, - isControlNetImagePreprocessedToggled, } from '../store/controlNetSlice'; import { useAppDispatch } from 'app/store/storeHooks'; import ParamControlNetModel from './parameters/ParamControlNetModel'; @@ -22,7 +21,7 @@ import { TabPanel, Box, } from '@chakra-ui/react'; -import { FaCopy, FaTrash } from 'react-icons/fa'; +import { FaCopy, FaPlus, FaTrash, FaWrench } from 'react-icons/fa'; import ParamControlNetBeginEnd from './parameters/ParamControlNetBeginEnd'; import ControlNetImagePreview from './ControlNetImagePreview'; @@ -34,6 +33,7 @@ import ControlNetProcessorComponent from './ControlNetProcessorComponent'; import ControlNetPreprocessButton from './ControlNetPreprocessButton'; import IAIButton from 'common/components/IAIButton'; import IAISwitch from 'common/components/IAISwitch'; +import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons'; type ControlNetProps = { controlNet: ControlNetConfig; @@ -48,12 +48,12 @@ const ControlNet = (props: ControlNetProps) => { beginStepPct, endStepPct, controlImage, - isPreprocessed, processedControlImage, processorNode, + processorType, } = props.controlNet; const dispatch = useAppDispatch(); - const [shouldShowAdvanced, onToggleAdvanced] = useToggle(true); + const [shouldShowAdvanced, onToggleAdvanced] = useToggle(false); const handleDelete = useCallback(() => { dispatch(controlNetRemoved({ controlNetId })); @@ -69,23 +69,20 @@ const ControlNet = (props: ControlNetProps) => { dispatch(controlNetToggled({ controlNetId })); }, [controlNetId, dispatch]); - const handleToggleIsPreprocessed = useCallback(() => { - dispatch(isControlNetImagePreprocessedToggled({ controlNetId })); - }, [controlNetId, dispatch]); - return ( @@ -103,19 +100,38 @@ const ControlNet = (props: ControlNetProps) => { } /> } /> + + } + /> {isEnabled && ( <> @@ -125,38 +141,13 @@ const ControlNet = (props: ControlNetProps) => { flexDir: 'column', gap: 2, w: 'full', - h: 32, - paddingInlineStart: 2, - paddingInlineEnd: shouldShowAdvanced ? 2 : 0, + h: 24, + paddingInlineStart: 1, + paddingInlineEnd: shouldShowAdvanced ? 1 : 0, pb: 2, justifyContent: 'space-between', }} > - - - - - Preprocessed - - - - - - Advanced - - - { sx={{ alignItems: 'center', justifyContent: 'center', - h: 32, - w: 32, + h: 24, + w: 24, aspectRatio: '1/1', }} > @@ -188,18 +179,14 @@ const ControlNet = (props: ControlNetProps) => { - {!isPreprocessed && ( - <> - - - - )} + + )} diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx index 099e58ce80..2183945e9b 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx @@ -28,12 +28,8 @@ type Props = { }; const ControlNetImagePreview = (props: Props) => { - const { - controlNetId, - controlImage, - processedControlImage, - isPreprocessed: isControlImageProcessed, - } = props.controlNet; + const { controlNetId, controlImage, processedControlImage, processorType } = + props.controlNet; const dispatch = useAppDispatch(); const { isProcessingControlImage } = useAppSelector(selector); const containerRef = useRef(null); @@ -56,7 +52,7 @@ const ControlNetImagePreview = (props: Props) => { processedControlImage && !isMouseOverImage && !isProcessingControlImage && - !isControlImageProcessed; + processorType !== 'none'; return ( @@ -64,7 +60,7 @@ const ControlNetImagePreview = (props: Props) => { image={controlImage} onDrop={handleControlImageChanged} isDropDisabled={Boolean( - processedControlImage && !isControlImageProcessed + processedControlImage && processorType !== 'none' )} /> diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetModel.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetModel.tsx index 7de32bb107..113b1148f4 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetModel.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetModel.tsx @@ -27,9 +27,12 @@ const ParamIsControlNetModel = (props: ParamIsControlNetModelProps) => { return ( ); 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 f1c9cb8048..5dc0a909d5 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,7 +4,5 @@ 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 b022d81b3a..c8689badf5 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/constants.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/constants.ts @@ -24,6 +24,14 @@ type ControlNetProcessorsDict = Record< * TODO: Generate from the OpenAPI schema */ export const CONTROLNET_PROCESSORS = { + none: { + type: 'none', + label: 'None', + description: '', + default: { + type: 'none', + }, + }, canny_image_processor: { type: 'canny_image_processor', label: 'Canny', diff --git a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts index 4847e3c1a5..1389457aba 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts @@ -21,8 +21,8 @@ export const initialControlNet: Omit = { beginStepPct: 0, endStepPct: 1, controlImage: null, - isPreprocessed: false, processedControlImage: null, + processorType: 'canny_image_processor', processorNode: CONTROLNET_PROCESSORS.canny_image_processor .default as RequiredCannyImageProcessorInvocation, }; @@ -35,8 +35,8 @@ export type ControlNetConfig = { beginStepPct: number; endStepPct: number; controlImage: ImageDTO | null; - isPreprocessed: boolean; processedControlImage: ImageDTO | null; + processorType: ControlNetProcessorType; processorNode: RequiredControlNetProcessorNode; }; @@ -110,19 +110,11 @@ export const controlNetSlice = createSlice({ state.controlNets[controlNetId].processedControlImage = null; if ( controlImage !== null && - !state.controlNets[controlNetId].isPreprocessed + state.controlNets[controlNetId].processorType !== 'none' ) { state.isProcessingControlImage = true; } }, - isControlNetImagePreprocessedToggled: ( - state, - action: PayloadAction<{ controlNetId: string }> - ) => { - const { controlNetId } = action.payload; - state.controlNets[controlNetId].isPreprocessed = - !state.controlNets[controlNetId].isPreprocessed; - }, controlNetProcessedImageChanged: ( state, action: PayloadAction<{ @@ -188,6 +180,7 @@ export const controlNetSlice = createSlice({ }> ) => { const { controlNetId, processorType } = action.payload; + state.controlNets[controlNetId].processorType = processorType; state.controlNets[controlNetId].processorNode = CONTROLNET_PROCESSORS[ processorType ].default as RequiredControlNetProcessorNode; @@ -210,7 +203,6 @@ export const { controlNetAddedFromImage, controlNetRemoved, controlNetImageChanged, - isControlNetImagePreprocessedToggled, controlNetProcessedImageChanged, controlNetToggled, controlNetModelChanged, diff --git a/invokeai/frontend/web/src/features/controlNet/store/types.ts b/invokeai/frontend/web/src/features/controlNet/store/types.ts index 808a50010b..4ee15b39b9 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/types.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/types.ts @@ -36,7 +36,7 @@ export type ControlNetProcessorNode = * Any ControlNet processor type */ export type ControlNetProcessorType = NonNullable< - ControlNetProcessorNode['type'] + ControlNetProcessorNode['type'] | 'none' >; /** diff --git a/invokeai/frontend/web/src/features/nodes/util/addControlNetToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/addControlNetToLinearGraph.ts index b386b41dc7..9c77681d18 100644 --- a/invokeai/frontend/web/src/features/nodes/util/addControlNetToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/addControlNetToLinearGraph.ts @@ -33,13 +33,12 @@ export const addControlNetToLinearGraph = ( const { controlNetId, isEnabled, - isPreprocessed: isControlImageProcessed, controlImage, processedControlImage, beginStepPct, endStepPct, model, - processorNode, + processorType, weight, } = controlNet; @@ -57,14 +56,14 @@ export const addControlNetToLinearGraph = ( control_weight: weight, }; - if (processedControlImage && !isControlImageProcessed) { + if (processedControlImage && processorType !== 'none') { // We've already processed the image in the app, so we can just use the processed image const { image_name, image_origin } = processedControlImage; controlNetNode.image = { image_name, image_origin, }; - } else if (controlImage && isControlImageProcessed) { + } else if (controlImage && processorType !== 'none') { // The control image is preprocessed const { image_name, image_origin } = controlImage; controlNetNode.image = { From a664ee30a2aade60f39121cb15adb0df61ce7ff7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 4 Jun 2023 17:16:50 +1000 Subject: [PATCH 41/47] feat(ui): do not change images if the dropped image is the same image --- .../web/src/common/components/IAIDndImage.tsx | 2 +- .../components/ControlNetImagePreview.tsx | 17 +++++++++++------ .../gallery/components/CurrentImagePreview.tsx | 5 ++++- .../fields/ImageInputFieldComponent.tsx | 14 +++++++++----- .../ImageToImage/InitialImagePreview.tsx | 13 ++++++++----- 5 files changed, 33 insertions(+), 18 deletions(-) diff --git a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx index 8b94cd7b03..b9b9e56722 100644 --- a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx +++ b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx @@ -21,7 +21,7 @@ import { v4 as uuidv4 } from 'uuid'; type IAIDndImageProps = { image: ImageDTO | null | undefined; - onDrop: (image: ImageDTO) => void; + onDrop: (droppedImage: ImageDTO) => void; onReset?: () => void; onError?: (event: SyntheticEvent) => void; onLoad?: (event: SyntheticEvent) => void; diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx index 2183945e9b..632f88b57b 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx @@ -36,11 +36,16 @@ const ControlNetImagePreview = (props: Props) => { const isMouseOverImage = useHoverDirty(containerRef); - const handleControlImageChanged = useCallback( - (controlImage: ImageDTO) => { - dispatch(controlNetImageChanged({ controlNetId, controlImage })); + const handleDrop = useCallback( + (droppedImage: ImageDTO) => { + if (controlImage?.image_name === droppedImage.image_name) { + return; + } + dispatch( + controlNetImageChanged({ controlNetId, controlImage: droppedImage }) + ); }, - [controlNetId, dispatch] + [controlImage, controlNetId, dispatch] ); const shouldShowProcessedImageBackdrop = @@ -58,7 +63,7 @@ const ControlNetImagePreview = (props: Props) => { { > diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx index d102f00b3b..c879bd4869 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx @@ -67,9 +67,12 @@ const CurrentImagePreview = () => { const handleDrop = useCallback( (droppedImage: ImageDTO) => { + if (droppedImage.image_name === image?.image_name) { + return; + } dispatch(imageSelected(droppedImage)); }, - [dispatch] + [dispatch, image?.image_name] ); return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx index 8275e70f62..9ea65911a2 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx @@ -19,17 +19,21 @@ const ImageInputFieldComponent = ( const dispatch = useAppDispatch(); - const handleChange = useCallback( - (image: ImageDTO) => { + const handleDrop = useCallback( + (droppedImage: ImageDTO) => { + if (field.value?.image_name === droppedImage.image_name) { + return; + } + dispatch( fieldValueChanged({ nodeId, fieldName: field.name, - value: image, + value: droppedImage, }) ); }, - [dispatch, field.name, nodeId] + [dispatch, field.name, field.value?.image_name, nodeId] ); const handleReset = useCallback(() => { @@ -53,7 +57,7 @@ const ImageInputFieldComponent = ( > diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx index 817ae197b6..a1c4d5acab 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx @@ -53,11 +53,14 @@ const InitialImagePreview = () => { } }, [dispatch, t, toaster, shouldFetchImages]); - const handleChange = useCallback( - (image: ImageDTO) => { - dispatch(initialImageChanged(image)); + const handleDrop = useCallback( + (droppedImage: ImageDTO) => { + if (droppedImage.image_name === initialImage?.image_name) { + return; + } + dispatch(initialImageChanged(droppedImage)); }, - [dispatch] + [dispatch, initialImage?.image_name] ); const handleReset = useCallback(() => { @@ -76,7 +79,7 @@ const InitialImagePreview = () => { > } /> From 065fff7db5e9d8296882a9bd57f556412b4ec213 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 4 Jun 2023 18:12:14 +1000 Subject: [PATCH 42/47] fix(ui): fix wonkiness with image dnd --- .../components/CurrentImageDisplay.tsx | 31 ++++++----------- .../components/CurrentImagePreview.tsx | 34 +++++++++---------- 2 files changed, 26 insertions(+), 39 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx index 621ec8864b..e4e50e6c5d 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx @@ -1,4 +1,4 @@ -import { Box, Flex, Icon } from '@chakra-ui/react'; +import { Box, Flex } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { systemSelector } from 'features/system/store/systemSelectors'; @@ -7,7 +7,7 @@ import { isEqual } from 'lodash-es'; import { gallerySelector } from '../store/gallerySelectors'; import CurrentImageButtons from './CurrentImageButtons'; import CurrentImagePreview from './CurrentImagePreview'; -import { FaImage } from 'react-icons/fa'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; export const currentImageDisplaySelector = createSelector( [systemSelector, gallerySelector], @@ -15,21 +15,20 @@ export const currentImageDisplaySelector = createSelector( const { progressImage } = system; return { - hasAnImageToDisplay: gallery.selectedImage || progressImage, + hasSelectedImage: Boolean(gallery.selectedImage), + hasProgressImage: Boolean(progressImage), }; }, - { - memoizeOptions: { - resultEqualityCheck: isEqual, - }, - } + defaultSelectorOptions ); /** * Displays the current image if there is one, plus associated actions. */ const CurrentImageDisplay = () => { - const { hasAnImageToDisplay } = useAppSelector(currentImageDisplaySelector); + const { hasSelectedImage, hasProgressImage } = useAppSelector( + currentImageDisplaySelector + ); return ( { gap: 4, }} > - {hasAnImageToDisplay ? ( - - ) : ( - - )} + - {hasAnImageToDisplay && ( + {hasSelectedImage && ( diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx index c879bd4869..12d62ead70 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx @@ -101,23 +101,21 @@ const CurrentImagePreview = () => { }} /> ) : ( - image && ( - - } - /> - - ) + + } + /> + )} {shouldShowImageDetails && image && image.metadata && ( { )} - {!shouldShowImageDetails && ( + {!shouldShowImageDetails && image && ( Date: Sun, 4 Jun 2023 22:00:36 +1000 Subject: [PATCH 43/47] fix(ui): fix rebase issue --- .../src/features/nodes/components/ui/NodeInvokeButton.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/ui/NodeInvokeButton.tsx b/invokeai/frontend/web/src/features/nodes/components/ui/NodeInvokeButton.tsx index 4b916abd2e..be5e5a943e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/ui/NodeInvokeButton.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/ui/NodeInvokeButton.tsx @@ -1,11 +1,11 @@ import { Box } from '@chakra-ui/react'; -import { readinessSelector } from 'app/selectors/readinessSelector'; import { userInvoked } from 'app/store/actions'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIButton, { IAIButtonProps } from 'common/components/IAIButton'; import IAIIconButton, { IAIIconButtonProps, } from 'common/components/IAIIconButton'; +import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke'; import ProgressBar from 'features/system/components/ProgressBar'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { useCallback } from 'react'; @@ -21,9 +21,8 @@ interface InvokeButton export default function NodeInvokeButton(props: InvokeButton) { const { iconButton = false, ...rest } = props; const dispatch = useAppDispatch(); - const { isReady } = useAppSelector(readinessSelector); const activeTabName = useAppSelector(activeTabNameSelector); - + const isReady = useIsReadyToInvoke(); const handleInvoke = useCallback(() => { dispatch(userInvoked('nodes')); }, [dispatch]); From 5831364f9c30b598088d8eb792020a715c0ffdb3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 4 Jun 2023 22:44:18 +1000 Subject: [PATCH 44/47] Update web README.md --- invokeai/frontend/web/docs/README.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/docs/README.md b/invokeai/frontend/web/docs/README.md index 323dcc5bc7..e8b150e71e 100644 --- a/invokeai/frontend/web/docs/README.md +++ b/invokeai/frontend/web/docs/README.md @@ -12,7 +12,14 @@ Code in `invokeai/frontend/web/` if you want to have a look. ## Stack -State management is Redux via [Redux Toolkit](https://github.com/reduxjs/redux-toolkit). Communication with server is a mix of HTTP and [socket.io](https://github.com/socketio/socket.io-client) (with a custom redux middleware to help). +State management is Redux via [Redux Toolkit](https://github.com/reduxjs/redux-toolkit). We lean heavily on RTK: +- `createAsyncThunk` for HTTP requests +- `createEntityAdapter` for fetching images and models +- `createListenerMiddleware` for workflows + +The API client and associated types are generated from the OpenAPI schema. See API_CLIENT.md. + +Communication with server is a mix of HTTP and [socket.io](https://github.com/socketio/socket.io-client) (with a simple socket.io redux middleware to help). [Chakra-UI](https://github.com/chakra-ui/chakra-ui) for components and styling. @@ -37,9 +44,15 @@ From `invokeai/frontend/web/` run `yarn install` to get everything set up. Start everything in dev mode: 1. Start the dev server: `yarn dev` -2. Start the InvokeAI Nodes backend: `python scripts/invokeai-new.py --web # run from the repo root` +2. Start the InvokeAI Nodes backend: `python scripts/invokeai-web.py # run from the repo root` 3. Point your browser to the dev server address e.g. +#### VSCode Remote Dev + +We've noticed an intermittent issue with the VSCode Remote Dev port forwarding. If you use this feature of VSCode, you may intermittently click the Invoke button and then get nothing until the request times out. Suggest disabling the IDE's port forwarding feature and doing it manually via SSH: + +`ssh -L 9090:localhost:9090 -L 5173:localhost:5173 user@host` + ### Production builds For a number of technical and logistical reasons, we need to commit UI build artefacts to the repo. From 6247b79111291dd27f897eb11316eb93c46bab63 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 4 Jun 2023 22:46:53 +1000 Subject: [PATCH 45/47] docs(ui): update API_CLIENT --- invokeai/frontend/web/docs/API_CLIENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/docs/API_CLIENT.md b/invokeai/frontend/web/docs/API_CLIENT.md index 51f3a6510c..d4a71350dd 100644 --- a/invokeai/frontend/web/docs/API_CLIENT.md +++ b/invokeai/frontend/web/docs/API_CLIENT.md @@ -26,7 +26,7 @@ We need to start the nodes web server, which serves the OpenAPI schema to the ge ```bash # from the repo root -python scripts/invoke-new.py --web +python scripts/invokeai-web.py ``` 2. Generate the API client. From 95fa66661c14642802bcd61dae067905cee6ce19 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 4 Jun 2023 22:55:35 +1000 Subject: [PATCH 46/47] dummy commit to make github actions run --- invokeai/frontend/web/docs/API_CLIENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/docs/API_CLIENT.md b/invokeai/frontend/web/docs/API_CLIENT.md index d4a71350dd..5072aa2c42 100644 --- a/invokeai/frontend/web/docs/API_CLIENT.md +++ b/invokeai/frontend/web/docs/API_CLIENT.md @@ -29,7 +29,7 @@ We need to start the nodes web server, which serves the OpenAPI schema to the ge python scripts/invokeai-web.py ``` -2. Generate the API client. +2. Generate the API client. ```bash # from invokeai/frontend/web/ From cdcfda164d784a435c19ac88dd0227b696057f39 Mon Sep 17 00:00:00 2001 From: Damian Stewart Date: Sun, 4 Jun 2023 15:30:54 +0200 Subject: [PATCH 47/47] enable long prompts, upgrade compel to enable .and() (concatenating prompts) --- invokeai/app/invocations/compel.py | 54 +++++++++++++++------- invokeai/app/invocations/latent.py | 10 ++++ invokeai/backend/prompting/conditioning.py | 4 +- pyproject.toml | 2 +- 4 files changed, 52 insertions(+), 18 deletions(-) diff --git a/invokeai/app/invocations/compel.py b/invokeai/app/invocations/compel.py index 076ce81021..58dc661baf 100644 --- a/invokeai/app/invocations/compel.py +++ b/invokeai/app/invocations/compel.py @@ -3,6 +3,7 @@ from pydantic import BaseModel, Field from invokeai.app.invocations.util.choose_model import choose_model from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationContext, InvocationConfig +from ...backend.prompting.conditioning import try_parse_legacy_blend from ...backend.util.devices import choose_torch_device, torch_dtype from ...backend.stable_diffusion.diffusion import InvokeAIDiffuserComponent @@ -13,7 +14,7 @@ from compel.prompt_parser import ( Blend, CrossAttentionControlSubstitute, FlattenedPrompt, - Fragment, + Fragment, Conjunction, ) @@ -93,25 +94,22 @@ class CompelInvocation(BaseInvocation): text_encoder=text_encoder, textual_inversion_manager=pipeline.textual_inversion_manager, dtype_for_device_getter=torch_dtype, - truncate_long_prompts=True, # TODO: + truncate_long_prompts=False, ) - # TODO: support legacy blend? - - conjunction = Compel.parse_prompt_string(prompt_str) - prompt: Union[FlattenedPrompt, Blend] = conjunction.prompts[0] + legacy_blend = try_parse_legacy_blend(prompt_str, skip_normalize=False) + if legacy_blend is not None: + conjunction = legacy_blend + else: + conjunction = Compel.parse_prompt_string(prompt_str) if context.services.configuration.log_tokenization: - log_tokenization_for_prompt_object(prompt, tokenizer) + log_tokenization_for_conjunction(conjunction, tokenizer) - c, options = compel.build_conditioning_tensor_for_prompt_object(prompt) - - # TODO: long prompt support - #if not self.truncate_long_prompts: - # [c, uc] = compel.pad_conditioning_tensors_to_same_length([c, uc]) + c, options = compel.build_conditioning_tensor_for_conjunction(conjunction) ec = InvokeAIDiffuserComponent.ExtraConditioningInfo( - tokens_count_including_eos_bos=get_max_token_count(tokenizer, prompt), + tokens_count_including_eos_bos=get_max_token_count(tokenizer, conjunction), cross_attention_control_args=options.get("cross_attention_control", None), ) @@ -128,14 +126,22 @@ class CompelInvocation(BaseInvocation): def get_max_token_count( - tokenizer, prompt: Union[FlattenedPrompt, Blend], truncate_if_too_long=False + tokenizer, prompt: Union[FlattenedPrompt, Blend, Conjunction], truncate_if_too_long=False ) -> int: if type(prompt) is Blend: blend: Blend = prompt return max( [ - get_max_token_count(tokenizer, c, truncate_if_too_long) - for c in blend.prompts + get_max_token_count(tokenizer, p, truncate_if_too_long) + for p in blend.prompts + ] + ) + elif type(prompt) is Conjunction: + conjunction: Conjunction = prompt + return sum( + [ + get_max_token_count(tokenizer, p, truncate_if_too_long) + for p in conjunction.prompts ] ) else: @@ -170,6 +176,22 @@ def get_tokens_for_prompt_object( return tokens +def log_tokenization_for_conjunction( + c: Conjunction, tokenizer, display_label_prefix=None +): + display_label_prefix = display_label_prefix or "" + for i, p in enumerate(c.prompts): + if len(c.prompts)>1: + this_display_label_prefix = f"{display_label_prefix}(conjunction part {i + 1}, weight={c.weights[i]})" + else: + this_display_label_prefix = display_label_prefix + log_tokenization_for_prompt_object( + p, + tokenizer, + display_label_prefix=this_display_label_prefix + ) + + def log_tokenization_for_prompt_object( p: Union[Blend, FlattenedPrompt], tokenizer, display_label_prefix=None ): diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 4dc1f6456c..ba65e214c3 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -4,6 +4,7 @@ import random import einops from typing import Literal, Optional, Union, List +from compel import Compel from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_controlnet import MultiControlNetModel from pydantic import BaseModel, Field, validator @@ -233,6 +234,15 @@ class TextToLatentsInvocation(BaseInvocation): c, extra_conditioning_info = context.services.latents.get(self.positive_conditioning.conditioning_name) uc, _ = context.services.latents.get(self.negative_conditioning.conditioning_name) + compel = Compel( + tokenizer=model.tokenizer, + text_encoder=model.text_encoder, + textual_inversion_manager=model.textual_inversion_manager, + dtype_for_device_getter=torch_dtype, + truncate_long_prompts=False, + ) + [c, uc] = compel.pad_conditioning_tensors_to_same_length([c, uc]) + conditioning_data = ConditioningData( uc, c, diff --git a/invokeai/backend/prompting/conditioning.py b/invokeai/backend/prompting/conditioning.py index 2e62853872..46201a5284 100644 --- a/invokeai/backend/prompting/conditioning.py +++ b/invokeai/backend/prompting/conditioning.py @@ -38,7 +38,7 @@ def get_uc_and_c_and_ec(prompt_string, dtype_for_device_getter=torch_dtype, truncate_long_prompts=False, ) - + config = get_invokeai_config() # get rid of any newline characters @@ -282,6 +282,8 @@ def split_weighted_subprompts(text, skip_normalize=False) -> list: (match.group("prompt").replace("\\:", ":"), float(match.group("weight") or 1)) for match in re.finditer(prompt_parser, text) ] + if len(parsed_prompts) == 0: + return [] if skip_normalize: return parsed_prompts weight_sum = sum(map(lambda x: x[1], parsed_prompts)) diff --git a/pyproject.toml b/pyproject.toml index 38aa71bd0e..38f4b7673f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ "albumentations", "click", "clip_anytorch", # replacing "clip @ https://github.com/openai/CLIP/archive/eaa22acb90a5876642d0507623e859909230a52d.zip", - "compel~=1.1.5", + "compel>=1.2.1", "controlnet-aux>=0.0.4", "timm==0.6.13", # needed to override timm latest in controlnet_aux, see https://github.com/isl-org/ZoeDepth/issues/26 "datasets",