From b87f3043ae59d7d155223505074e502a3f5acd09 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Wed, 24 May 2023 23:57:15 -0400 Subject: [PATCH 01/67] 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/67] 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/67] 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 2273b3a8c8707b50490fc6d96f6926c800591ca4 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Thu, 25 May 2023 20:41:26 -0400 Subject: [PATCH 04/67] fix potential race condition in config system --- invokeai/app/api_app.py | 3 +- invokeai/app/cli_app.py | 5 +- invokeai/app/services/config.py | 63 ++++++++++--------- invokeai/app/services/sqlite.py | 1 - invokeai/backend/config/invokeai_configure.py | 11 ++-- .../backend/config/model_install_backend.py | 4 +- invokeai/backend/image_util/patchmatch.py | 4 +- invokeai/backend/image_util/txt2mask.py | 4 +- .../convert_ckpt_to_diffusers.py | 10 +-- .../backend/model_management/model_manager.py | 10 +-- invokeai/backend/prompting/conditioning.py | 6 +- invokeai/backend/restoration/codeformer.py | 4 +- invokeai/backend/restoration/gfpgan.py | 4 +- invokeai/backend/restoration/realesrgan.py | 4 +- invokeai/backend/safety_checker.py | 5 +- .../backend/stable_diffusion/concepts_lib.py | 7 ++- .../stable_diffusion/diffusers_pipeline.py | 4 +- .../diffusion/shared_invokeai_diffusion.py | 40 ++++++------ invokeai/backend/util/devices.py | 5 +- invokeai/frontend/install/model_install.py | 4 +- invokeai/frontend/merge/merge_diffusers.py | 4 +- .../frontend/training/textual_inversion.py | 6 +- tests/test_config.py | 32 ++++++---- 23 files changed, 128 insertions(+), 112 deletions(-) diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index 69d322578d..96a22466b5 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -39,7 +39,8 @@ socket_io = SocketIO(app) # initialize config # this is a module global -app_config = InvokeAIAppConfig() +app_config = InvokeAIAppConfig.get_config() +app_config.parse_args() # Add startup event to load dependencies @app.on_event("startup") diff --git a/invokeai/app/cli_app.py b/invokeai/app/cli_app.py index de543d2d85..5f0d22a7d1 100644 --- a/invokeai/app/cli_app.py +++ b/invokeai/app/cli_app.py @@ -37,7 +37,7 @@ from .services.invocation_services import InvocationServices from .services.invoker import Invoker from .services.processor import DefaultInvocationProcessor from .services.sqlite import SqliteItemStorage -from .services.config import get_invokeai_config +from .services.config import InvokeAIAppConfig class CliCommand(BaseModel): command: Union[BaseCommand.get_commands() + BaseInvocation.get_invocations()] = Field(discriminator="type") # type: ignore @@ -196,7 +196,8 @@ logger = logger.InvokeAILogger.getLogger() def invoke_cli(): # this gets the basic configuration - config = get_invokeai_config() + config = InvokeAIAppConfig.get_config() + config.parse_args() # get the optional list of invocations to execute on the command line parser = config.get_parser() diff --git a/invokeai/app/services/config.py b/invokeai/app/services/config.py index 49e0b6bed4..25b2241d58 100644 --- a/invokeai/app/services/config.py +++ b/invokeai/app/services/config.py @@ -82,13 +82,10 @@ Typical usage: from invokeai.invocations.generate import TextToImageInvocation # get global configuration and print its nsfw_checker value - conf = InvokeAIAppConfig() + conf = InvokeAIAppConfig.get_config() + conf.parse_args() print(conf.nsfw_checker) - # get the text2image invocation and print its step value - text2image = TextToImageInvocation() - print(text2image.steps) - Computed properties: The InvokeAIAppConfig object has a series of properties that @@ -103,10 +100,11 @@ a Path object: lora_path - path to the LoRA directory In most cases, you will want to create a single InvokeAIAppConfig -object for the entire application. The get_invokeai_config() function +object for the entire application. The InvokeAIAppConfig.get_config() function does this: - config = get_invokeai_config() + config = InvokeAIAppConfig.get_config() + config.parse_args() # read values from the command line/config file print(config.root) # Subclassing @@ -141,6 +139,7 @@ two configs are kept in separate sections of the config file: outdir: outputs ... ''' +from __future__ import annotations import argparse import pydoc import typing @@ -155,9 +154,6 @@ from typing import Any, ClassVar, Dict, List, Literal, Type, Union, get_origin, INIT_FILE = Path('invokeai.yaml') LEGACY_INIT_FILE = Path('invokeai.init') -# This global stores a singleton InvokeAIAppConfig configuration object -global_config = None - class InvokeAISettings(BaseSettings): ''' Runtime configuration settings in which default values are @@ -330,6 +326,9 @@ the command-line client (recommended for experts only), or can be changed by editing the file "INVOKEAI_ROOT/invokeai.yaml" or by setting environment variables INVOKEAI_. ''' + singleton_config: ClassVar[InvokeAIAppConfig] = None + singleton_init: ClassVar[Dict] = None + #fmt: off type: Literal["InvokeAI"] = "InvokeAI" host : str = Field(default="127.0.0.1", description="IP address to bind to", category='Web Server') @@ -369,33 +368,44 @@ setting environment variables INVOKEAI_. embeddings : bool = Field(default=True, description='Load contents of embeddings directory', category='Models') #fmt: on - def __init__(self, conf: DictConfig = None, argv: List[str]=None, **kwargs): + def parse_args(self, argv: List[str]=None, conf: DictConfig = None, clobber=False): ''' - Initialize InvokeAIAppconfig. + Update settings with contents of init file, environment, and + command-line settings. :param conf: alternate Omegaconf dictionary object :param argv: aternate sys.argv list - :param **kwargs: attributes to initialize with + :param clobber: ovewrite any initialization parameters passed during initialization ''' - super().__init__(**kwargs) - # Set the runtime root directory. We parse command-line switches here # in order to pick up the --root_dir option. - self.parse_args(argv) + super().parse_args(argv) if conf is None: try: conf = OmegaConf.load(self.root_dir / INIT_FILE) except: pass InvokeAISettings.initconf = conf - + # parse args again in order to pick up settings in configuration file - self.parse_args(argv) + super().parse_args(argv) - # restore initialization values - hints = get_type_hints(self) - for k in kwargs: - setattr(self,k,parse_obj_as(hints[k],kwargs[k])) + if self.singleton_init and not clobber: + hints = get_type_hints(self.__class__) + for k in self.singleton_init: + setattr(self,k,parse_obj_as(hints[k],self.singleton_init[k])) + @classmethod + def get_config(cls,**kwargs)->InvokeAIAppConfig: + ''' + This returns a singleton InvokeAIAppConfig configuration object. + ''' + if cls.singleton_config is None \ + or type(cls.singleton_config)!=cls \ + or (kwargs and cls.singleton_init != kwargs): + cls.singleton_config = cls(**kwargs) + cls.singleton_init = kwargs + return cls.singleton_config + @property def root_path(self)->Path: ''' @@ -513,11 +523,8 @@ class PagingArgumentParser(argparse.ArgumentParser): text = self.format_help() pydoc.pager(text) -def get_invokeai_config(cls:Type[InvokeAISettings]=InvokeAIAppConfig,**kwargs)->InvokeAIAppConfig: +def get_invokeai_config(**kwargs)->InvokeAIAppConfig: ''' - This returns a singleton InvokeAIAppConfig configuration object. + Legacy function which returns InvokeAIAppConfig.get_config() ''' - global global_config - if global_config is None or type(global_config)!=cls: - global_config = cls(**kwargs) - return global_config + return InvokeAIAppConfig.get_config(**kwargs) diff --git a/invokeai/app/services/sqlite.py b/invokeai/app/services/sqlite.py index fd089014bb..a62fff88a1 100644 --- a/invokeai/app/services/sqlite.py +++ b/invokeai/app/services/sqlite.py @@ -26,7 +26,6 @@ class SqliteItemStorage(ItemStorageABC, Generic[T]): self._table_name = table_name self._id_field = id_field # TODO: validate that T has this field self._lock = Lock() - self._conn = sqlite3.connect( self._filename, check_same_thread=False ) # TODO: figure out a better threading solution diff --git a/invokeai/backend/config/invokeai_configure.py b/invokeai/backend/config/invokeai_configure.py index 59f11d35bc..cf38dd93a6 100755 --- a/invokeai/backend/config/invokeai_configure.py +++ b/invokeai/backend/config/invokeai_configure.py @@ -51,10 +51,7 @@ from invokeai.backend.config.model_install_backend import ( hf_download_with_resume, recommended_datasets, ) -from invokeai.app.services.config import ( - get_invokeai_config, - InvokeAIAppConfig, -) +from invokeai.app.services.config import InvokeAIAppConfig warnings.filterwarnings("ignore") @@ -62,7 +59,7 @@ transformers.logging.set_verbosity_error() # --------------------------globals----------------------- -config = get_invokeai_config() +config = InvokeAIAppConfig.get_config() Model_dir = "models" Weights_dir = "ldm/stable-diffusion-v1/" @@ -820,8 +817,8 @@ 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 - + # Load new init file into config + config.parse_args(argv=[],conf=OmegaConf.load(new_init_file)) if not config.model_conf_path.exists(): initialize_rootdir(config.root, opt.yes_to_all) diff --git a/invokeai/backend/config/model_install_backend.py b/invokeai/backend/config/model_install_backend.py index cb76f955bc..96468dee4b 100644 --- a/invokeai/backend/config/model_install_backend.py +++ b/invokeai/backend/config/model_install_backend.py @@ -19,7 +19,7 @@ from tqdm import tqdm import invokeai.configs as configs -from invokeai.app.services.config import get_invokeai_config +from invokeai.app.services.config import InvokeAIAppConfig from ..model_management import ModelManager from ..stable_diffusion import StableDiffusionGeneratorPipeline @@ -27,7 +27,7 @@ from ..stable_diffusion import StableDiffusionGeneratorPipeline warnings.filterwarnings("ignore") # --------------------------globals----------------------- -config = get_invokeai_config() +config = InvokeAIAppConfig.get_config() Model_dir = "models" Weights_dir = "ldm/stable-diffusion-v1/" diff --git a/invokeai/backend/image_util/patchmatch.py b/invokeai/backend/image_util/patchmatch.py index 0d2221be41..2e65f08d9f 100644 --- a/invokeai/backend/image_util/patchmatch.py +++ b/invokeai/backend/image_util/patchmatch.py @@ -6,7 +6,8 @@ be suppressed or deferred """ import numpy as np import invokeai.backend.util.logging as logger -from invokeai.app.services.config import get_invokeai_config +from invokeai.app.services.config import InvokeAIAppConfig +config = InvokeAIAppConfig.get_config() class PatchMatch: """ @@ -21,7 +22,6 @@ class PatchMatch: @classmethod def _load_patch_match(self): - config = get_invokeai_config() if self.tried_load: return if config.try_patchmatch: diff --git a/invokeai/backend/image_util/txt2mask.py b/invokeai/backend/image_util/txt2mask.py index 1a8fcfeb90..429c9b63fb 100644 --- a/invokeai/backend/image_util/txt2mask.py +++ b/invokeai/backend/image_util/txt2mask.py @@ -33,10 +33,11 @@ from PIL import Image, ImageOps from transformers import AutoProcessor, CLIPSegForImageSegmentation import invokeai.backend.util.logging as logger -from invokeai.app.services.config import get_invokeai_config +from invokeai.app.services.config import InvokeAIAppConfig CLIPSEG_MODEL = "CIDAS/clipseg-rd64-refined" CLIPSEG_SIZE = 352 +config = InvokeAIAppConfig.get_config() class SegmentedGrayscale(object): def __init__(self, image: Image, heatmap: torch.Tensor): @@ -83,7 +84,6 @@ class Txt2Mask(object): def __init__(self, device="cpu", refined=False): logger.info("Initializing clipseg model for text to mask inference") - config = get_invokeai_config() # BUG: we are not doing anything with the device option at this time self.device = device diff --git a/invokeai/backend/model_management/convert_ckpt_to_diffusers.py b/invokeai/backend/model_management/convert_ckpt_to_diffusers.py index 467fe39155..acf93d9ab6 100644 --- a/invokeai/backend/model_management/convert_ckpt_to_diffusers.py +++ b/invokeai/backend/model_management/convert_ckpt_to_diffusers.py @@ -26,7 +26,7 @@ import torch from safetensors.torch import load_file import invokeai.backend.util.logging as logger -from invokeai.app.services.config import get_invokeai_config +from invokeai.app.services.config import InvokeAIAppConfig from .model_manager import ModelManager, SDLegacyType @@ -842,7 +842,7 @@ def convert_ldm_bert_checkpoint(checkpoint, config): def convert_ldm_clip_checkpoint(checkpoint): text_model = CLIPTextModel.from_pretrained( - "openai/clip-vit-large-patch14", cache_dir=get_invokeai_config().cache_dir + "openai/clip-vit-large-patch14", cache_dir=InvokeAIAppConfig.get_config().cache_dir ) keys = list(checkpoint.keys()) @@ -897,7 +897,7 @@ textenc_pattern = re.compile("|".join(protected.keys())) def convert_paint_by_example_checkpoint(checkpoint): - cache_dir = get_invokeai_config().cache_dir + cache_dir = InvokeAIAppConfig.get_config().cache_dir config = CLIPVisionConfig.from_pretrained( "openai/clip-vit-large-patch14", cache_dir=cache_dir ) @@ -969,7 +969,7 @@ def convert_paint_by_example_checkpoint(checkpoint): def convert_open_clip_checkpoint(checkpoint): - cache_dir = get_invokeai_config().cache_dir + cache_dir = InvokeAIAppConfig.get_config().cache_dir text_model = CLIPTextModel.from_pretrained( "stabilityai/stable-diffusion-2", subfolder="text_encoder", cache_dir=cache_dir ) @@ -1092,7 +1092,7 @@ def load_pipeline_from_original_stable_diffusion_ckpt( :param vae: A diffusers VAE to load into the pipeline. :param vae_path: Path to a checkpoint VAE that will be converted into diffusers and loaded into the pipeline. """ - config = get_invokeai_config() + config = InvokeAIAppConfig.get_config() with warnings.catch_warnings(): warnings.simplefilter("ignore") verbosity = dlogging.get_verbosity() diff --git a/invokeai/backend/model_management/model_manager.py b/invokeai/backend/model_management/model_manager.py index bdbca195bd..8e80c0b5c4 100644 --- a/invokeai/backend/model_management/model_manager.py +++ b/invokeai/backend/model_management/model_manager.py @@ -47,7 +47,7 @@ from diffusers.pipelines.stable_diffusion.safety_checker import ( from ..stable_diffusion import ( StableDiffusionGeneratorPipeline, ) -from invokeai.app.services.config import get_invokeai_config +from invokeai.app.services.config import InvokeAIAppConfig from ..util import CUDA_DEVICE, ask_user, download_with_resume class SDLegacyType(Enum): @@ -98,7 +98,7 @@ class ModelManager(object): if not isinstance(config, DictConfig): config = OmegaConf.load(config) self.config = config - self.globals = get_invokeai_config() + self.globals = InvokeAIAppConfig.get_config() self.precision = precision self.device = torch.device(device_type) self.max_loaded_models = max_loaded_models @@ -1057,7 +1057,7 @@ class ModelManager(object): """ # Three transformer models to check: bert, clip and safety checker, and # the diffusers as well - config = get_invokeai_config() + config = InvokeAIAppConfig.get_config() models_dir = config.root_dir / "models" legacy_locations = [ Path( @@ -1287,7 +1287,7 @@ class ModelManager(object): @classmethod def _delete_model_from_cache(cls,repo_id): - cache_info = scan_cache_dir(get_invokeai_config().cache_dir) + cache_info = scan_cache_dir(InvokeAIAppConfig.get_config().cache_dir) # I'm sure there is a way to do this with comprehensions # but the code quickly became incomprehensible! @@ -1304,7 +1304,7 @@ class ModelManager(object): @staticmethod def _abs_path(path: str | Path) -> Path: - globals = get_invokeai_config() + globals = InvokeAIAppConfig.get_config() if path is None or Path(path).is_absolute(): return path return Path(globals.root_dir, path).resolve() diff --git a/invokeai/backend/prompting/conditioning.py b/invokeai/backend/prompting/conditioning.py index 2e62853872..7a26be9800 100644 --- a/invokeai/backend/prompting/conditioning.py +++ b/invokeai/backend/prompting/conditioning.py @@ -21,10 +21,12 @@ from compel.prompt_parser import ( import invokeai.backend.util.logging as logger -from invokeai.app.services.config import get_invokeai_config +from invokeai.app.services.config import InvokeAIAppConfig from ..stable_diffusion import InvokeAIDiffuserComponent from ..util import torch_dtype +config = InvokeAIAppConfig.get_config() + def get_uc_and_c_and_ec(prompt_string, model: InvokeAIDiffuserComponent, log_tokens=False, skip_normalize_legacy_blend=False): @@ -39,8 +41,6 @@ def get_uc_and_c_and_ec(prompt_string, truncate_long_prompts=False, ) - config = get_invokeai_config() - # get rid of any newline characters prompt_string = prompt_string.replace("\n", " ") positive_prompt_string, negative_prompt_string = split_prompt_to_positive_and_negative(prompt_string) diff --git a/invokeai/backend/restoration/codeformer.py b/invokeai/backend/restoration/codeformer.py index b7073f8f8b..2a39a5c365 100644 --- a/invokeai/backend/restoration/codeformer.py +++ b/invokeai/backend/restoration/codeformer.py @@ -6,7 +6,7 @@ import numpy as np import torch import invokeai.backend.util.logging as logger -from invokeai.app.services.config import get_invokeai_config +from invokeai.app.services.config import InvokeAIAppConfig pretrained_model_url = ( "https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/codeformer.pth" @@ -18,7 +18,7 @@ class CodeFormerRestoration: self, codeformer_dir="models/codeformer", codeformer_model_path="codeformer.pth" ) -> None: - self.globals = get_invokeai_config() + self.globals = InvokeAIAppConfig.get_config() codeformer_dir = self.globals.root_dir / codeformer_dir self.model_path = codeformer_dir / codeformer_model_path self.codeformer_model_exists = self.model_path.exists() diff --git a/invokeai/backend/restoration/gfpgan.py b/invokeai/backend/restoration/gfpgan.py index 063feaa89a..5021e8d7d2 100644 --- a/invokeai/backend/restoration/gfpgan.py +++ b/invokeai/backend/restoration/gfpgan.py @@ -7,11 +7,11 @@ import torch from PIL import Image import invokeai.backend.util.logging as logger -from invokeai.app.services.config import get_invokeai_config +from invokeai.app.services.config import InvokeAIAppConfig class GFPGAN: def __init__(self, gfpgan_model_path="models/gfpgan/GFPGANv1.4.pth") -> None: - self.globals = get_invokeai_config() + self.globals = InvokeAIAppConfig.get_config() if not os.path.isabs(gfpgan_model_path): gfpgan_model_path = self.globals.root_dir / gfpgan_model_path self.model_path = gfpgan_model_path diff --git a/invokeai/backend/restoration/realesrgan.py b/invokeai/backend/restoration/realesrgan.py index c6c6d2d3b4..e08978adc2 100644 --- a/invokeai/backend/restoration/realesrgan.py +++ b/invokeai/backend/restoration/realesrgan.py @@ -6,8 +6,8 @@ from PIL import Image from PIL.Image import Image as ImageType import invokeai.backend.util.logging as logger -from invokeai.app.services.config import get_invokeai_config -config = get_invokeai_config() +from invokeai.app.services.config import InvokeAIAppConfig +config = InvokeAIAppConfig.get_config() class ESRGAN: def __init__(self, bg_tile_size=400) -> None: diff --git a/invokeai/backend/safety_checker.py b/invokeai/backend/safety_checker.py index 55e8eb1987..5ff69fe86c 100644 --- a/invokeai/backend/safety_checker.py +++ b/invokeai/backend/safety_checker.py @@ -15,9 +15,11 @@ from transformers import AutoFeatureExtractor import invokeai.assets.web as web_assets import invokeai.backend.util.logging as logger -from invokeai.app.services.config import get_invokeai_config +from invokeai.app.services.config import InvokeAIAppConfig from .util import CPU_DEVICE +config = InvokeAIAppConfig.get_config() + class SafetyChecker(object): CAUTION_IMG = "caution.png" @@ -26,7 +28,6 @@ class SafetyChecker(object): caution = Image.open(path) self.caution_img = caution.resize((caution.width // 2, caution.height // 2)) self.device = device - config = get_invokeai_config() try: safety_model_id = "CompVis/stable-diffusion-safety-checker" diff --git a/invokeai/backend/stable_diffusion/concepts_lib.py b/invokeai/backend/stable_diffusion/concepts_lib.py index beb884b012..5294150783 100644 --- a/invokeai/backend/stable_diffusion/concepts_lib.py +++ b/invokeai/backend/stable_diffusion/concepts_lib.py @@ -17,15 +17,16 @@ from huggingface_hub import ( hf_hub_url, ) -import invokeai.backend.util.logging as logger -from invokeai.app.services.config import get_invokeai_config +from invokeai.backend.util.logging import InvokeAILogger +from invokeai.app.services.config import InvokeAIAppConfig +logger = InvokeAILogger.getLogger() class HuggingFaceConceptsLibrary(object): def __init__(self, root=None): """ Initialize the Concepts object. May optionally pass a root directory. """ - self.config = get_invokeai_config() + self.config = InvokeAIAppConfig.get_config() self.root = root or self.config.root self.hf_api = HfApi() self.local_concepts = dict() diff --git a/invokeai/backend/stable_diffusion/diffusers_pipeline.py b/invokeai/backend/stable_diffusion/diffusers_pipeline.py index 4ca2a5cb30..1f188d8dd9 100644 --- a/invokeai/backend/stable_diffusion/diffusers_pipeline.py +++ b/invokeai/backend/stable_diffusion/diffusers_pipeline.py @@ -33,7 +33,7 @@ from torchvision.transforms.functional import resize as tv_resize from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer from typing_extensions import ParamSpec -from invokeai.app.services.config import get_invokeai_config +from invokeai.app.services.config import InvokeAIAppConfig from ..util import CPU_DEVICE, normalize_device from .diffusion import ( AttentionMapSaver, @@ -346,7 +346,7 @@ class StableDiffusionGeneratorPipeline(StableDiffusionPipeline): """ if xformers is available, use it, otherwise use sliced attention. """ - config = get_invokeai_config() + config = InvokeAIAppConfig.get_config() if ( torch.cuda.is_available() and is_xformers_available() diff --git a/invokeai/backend/stable_diffusion/diffusion/shared_invokeai_diffusion.py b/invokeai/backend/stable_diffusion/diffusion/shared_invokeai_diffusion.py index 4131837b41..ea01301fa0 100644 --- a/invokeai/backend/stable_diffusion/diffusion/shared_invokeai_diffusion.py +++ b/invokeai/backend/stable_diffusion/diffusion/shared_invokeai_diffusion.py @@ -10,7 +10,7 @@ from diffusers.models.attention_processor import AttentionProcessor from typing_extensions import TypeAlias import invokeai.backend.util.logging as logger -from invokeai.app.services.config import get_invokeai_config +from invokeai.app.services.config import InvokeAIAppConfig from .cross_attention_control import ( Arguments, @@ -72,7 +72,7 @@ class InvokeAIDiffuserComponent: :param model: the unet model to pass through to cross attention control :param model_forward_callback: a lambda with arguments (x, sigma, conditioning_to_apply). will be called repeatedly. most likely, this should simply call model.forward(x, sigma, conditioning) """ - config = get_invokeai_config() + config = InvokeAIAppConfig.get_config() self.conditioning = None self.model = model self.is_running_diffusers = is_running_diffusers @@ -112,23 +112,25 @@ class InvokeAIDiffuserComponent: # TODO resuscitate attention map saving # self.remove_attention_map_saving() - def override_cross_attention( - self, conditioning: ExtraConditioningInfo, step_count: int - ) -> Dict[str, AttentionProcessor]: - """ - setup cross attention .swap control. for diffusers this replaces the attention processor, so - the previous attention processor is returned so that the caller can restore it later. - """ - self.conditioning = conditioning - self.cross_attention_control_context = Context( - arguments=self.conditioning.cross_attention_control_args, - step_count=step_count, - ) - return override_cross_attention( - self.model, - self.cross_attention_control_context, - is_running_diffusers=self.is_running_diffusers, - ) + # apparently unused code + # TODO: delete + # def override_cross_attention( + # self, conditioning: ExtraConditioningInfo, step_count: int + # ) -> Dict[str, AttentionProcessor]: + # """ + # setup cross attention .swap control. for diffusers this replaces the attention processor, so + # the previous attention processor is returned so that the caller can restore it later. + # """ + # self.conditioning = conditioning + # self.cross_attention_control_context = Context( + # arguments=self.conditioning.cross_attention_control_args, + # step_count=step_count, + # ) + # return override_cross_attention( + # self.model, + # self.cross_attention_control_context, + # is_running_diffusers=self.is_running_diffusers, + # ) def restore_default_cross_attention( self, restore_attention_processor: Optional["AttentionProcessor"] = None diff --git a/invokeai/backend/util/devices.py b/invokeai/backend/util/devices.py index c6c0819df8..615209d98d 100644 --- a/invokeai/backend/util/devices.py +++ b/invokeai/backend/util/devices.py @@ -4,15 +4,15 @@ from contextlib import nullcontext import torch from torch import autocast -from invokeai.app.services.config import get_invokeai_config +from invokeai.app.services.config import InvokeAIAppConfig CPU_DEVICE = torch.device("cpu") CUDA_DEVICE = torch.device("cuda") MPS_DEVICE = torch.device("mps") +config = InvokeAIAppConfig.get_config() def choose_torch_device() -> torch.device: """Convenience routine for guessing which GPU device to run model on""" - config = get_invokeai_config() if config.always_use_cpu: return CPU_DEVICE if torch.cuda.is_available(): @@ -32,7 +32,6 @@ def choose_precision(device: torch.device) -> str: def torch_dtype(device: torch.device) -> torch.dtype: - config = get_invokeai_config() if config.full_precision: return torch.float32 if choose_precision(device) == "float16": diff --git a/invokeai/frontend/install/model_install.py b/invokeai/frontend/install/model_install.py index a283b4952d..375fdf7ba1 100644 --- a/invokeai/frontend/install/model_install.py +++ b/invokeai/frontend/install/model_install.py @@ -40,13 +40,13 @@ from .widgets import ( TextBox, set_min_terminal_size, ) -from invokeai.app.services.config import get_invokeai_config +from invokeai.app.services.config import InvokeAIAppConfig # minimum size for the UI MIN_COLS = 120 MIN_LINES = 45 -config = get_invokeai_config() +config = InvokeAIAppConfig.get_config() class addModelsForm(npyscreen.FormMultiPage): # for responsive resizing - disabled diff --git a/invokeai/frontend/merge/merge_diffusers.py b/invokeai/frontend/merge/merge_diffusers.py index 882a4587b6..9da04b97f8 100644 --- a/invokeai/frontend/merge/merge_diffusers.py +++ b/invokeai/frontend/merge/merge_diffusers.py @@ -20,12 +20,12 @@ from npyscreen import widget from omegaconf import OmegaConf import invokeai.backend.util.logging as logger -from invokeai.services.config import get_invokeai_config +from invokeai.services.config import InvokeAIAppConfig from ...backend.model_management import ModelManager from ...frontend.install.widgets import FloatTitleSlider DEST_MERGED_MODEL_DIR = "merged_models" -config = get_invokeai_config() +config = InvokeAIAppConfig.get_config() def merge_diffusion_models( model_ids_or_paths: List[Union[str, Path]], diff --git a/invokeai/frontend/training/textual_inversion.py b/invokeai/frontend/training/textual_inversion.py index 90e402f48b..e1c7b3749f 100755 --- a/invokeai/frontend/training/textual_inversion.py +++ b/invokeai/frontend/training/textual_inversion.py @@ -22,7 +22,7 @@ from omegaconf import OmegaConf import invokeai.backend.util.logging as logger -from invokeai.app.services.config import get_invokeai_config +from invokeai.app.services.config import InvokeAIAppConfig from ...backend.training import ( do_textual_inversion_training, parse_args @@ -423,7 +423,7 @@ def do_front_end(args: Namespace): save_args(args) try: - do_textual_inversion_training(get_invokeai_config(),**args) + do_textual_inversion_training(InvokeAIAppConfig.get_config(),**args) copy_to_embeddings_folder(args) except Exception as e: logger.error("An exception occurred during training. The exception was:") @@ -436,7 +436,7 @@ def main(): global config args = parse_args() - config = get_invokeai_config(argv=[]) + config = InvokeAIAppConfig.get_config() # change root if needed if args.root_dir: diff --git a/tests/test_config.py b/tests/test_config.py index 6d0586213e..2c883d63f5 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,7 +5,7 @@ from omegaconf import OmegaConf from pathlib import Path os.environ['INVOKEAI_ROOT']='/tmp' -from invokeai.app.services.config import InvokeAIAppConfig, InvokeAISettings +from invokeai.app.services.config import InvokeAIAppConfig from invokeai.app.invocations.generate import TextToImageInvocation init1 = OmegaConf.create( @@ -32,48 +32,56 @@ def test_use_init(): # note that we explicitly set omegaconf dict and argv here # so that the values aren't read from ~invokeai/invokeai.yaml and # sys.argv respectively. - conf1 = InvokeAIAppConfig(init1,[]) + conf1 = InvokeAIAppConfig.get_config() assert conf1 + conf1.parse_args(conf=init1) assert conf1.max_loaded_models==5 assert not conf1.nsfw_checker - conf2 = InvokeAIAppConfig(init2,[]) + conf2 = InvokeAIAppConfig.get_config() assert conf2 + conf2.parse_args(conf=init2) assert conf2.nsfw_checker assert conf2.max_loaded_models==2 assert not hasattr(conf2,'invalid_attribute') def test_argv_override(): - conf = InvokeAIAppConfig(init1,['--nsfw_checker','--max_loaded=10']) + conf = InvokeAIAppConfig.get_config() + conf.parse_args(conf=init1,argv=['--nsfw_checker','--max_loaded=10']) assert conf.nsfw_checker assert conf.max_loaded_models==10 assert conf.outdir==Path('outputs') # this is the default def test_env_override(): # argv overrides - conf = InvokeAIAppConfig(conf=init1,argv=['--max_loaded=10']) + conf = InvokeAIAppConfig() + conf.parse_args(conf=init1,argv=['--max_loaded=10']) assert conf.nsfw_checker==False - os.environ['INVOKEAI_nsfw_checker'] = 'True' - conf = InvokeAIAppConfig(conf=init1,argv=['--max_loaded=10']) + conf.parse_args(conf=init1,argv=['--max_loaded=10']) assert conf.nsfw_checker==True # environment variables should be case insensitive os.environ['InvokeAI_Max_Loaded_Models'] = '15' - conf = InvokeAIAppConfig(conf=init1) + conf = InvokeAIAppConfig() + conf.parse_args(conf=init1) assert conf.max_loaded_models == 15 - conf = InvokeAIAppConfig(conf=init1,argv=['--no-nsfw_checker','--max_loaded=10']) + conf = InvokeAIAppConfig() + conf.parse_args(conf=init1,argv=['--no-nsfw_checker','--max_loaded=10']) assert conf.nsfw_checker==False assert conf.max_loaded_models==10 - conf = InvokeAIAppConfig(conf=init1,argv=[],max_loaded_models=20) + conf = InvokeAIAppConfig.get_config(max_loaded_models=20) + conf.parse_args(conf=init1,argv=[]) assert conf.max_loaded_models==20 def test_type_coercion(): - conf = InvokeAIAppConfig(argv=['--root=/tmp/foobar']) + conf = InvokeAIAppConfig().get_config() + conf.parse_args(argv=['--root=/tmp/foobar']) assert conf.root==Path('/tmp/foobar') assert isinstance(conf.root,Path) - conf = InvokeAIAppConfig(argv=['--root=/tmp/foobar'],root='/tmp/different') + conf = InvokeAIAppConfig.get_config(root='/tmp/different') + conf.parse_args(argv=['--root=/tmp/foobar']) assert conf.root==Path('/tmp/different') assert isinstance(conf.root,Path) From e56965ad76f96624bb3dac30997619858f67663d Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Thu, 25 May 2023 21:10:00 -0400 Subject: [PATCH 05/67] documentation tweaks; fixed initialization in a couple more places --- invokeai/app/services/config.py | 39 +++++++++++++++---- invokeai/backend/config/invokeai_configure.py | 6 +-- .../training/textual_inversion_training.py | 2 +- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/invokeai/app/services/config.py b/invokeai/app/services/config.py index 25b2241d58..208f6d9949 100644 --- a/invokeai/app/services/config.py +++ b/invokeai/app/services/config.py @@ -51,18 +51,32 @@ in INVOKEAI_ROOT. You can replace supersede this by providing any OmegaConf dictionary object initialization time: omegaconf = OmegaConf.load('/tmp/init.yaml') - conf = InvokeAIAppConfig(conf=omegaconf) + conf = InvokeAIAppConfig() + conf.parse_args(conf=omegaconf) -By default, InvokeAIAppConfig will parse the contents of `sys.argv` at -initialization time. You may pass a list of strings in the optional +InvokeAIAppConfig.parse_args() will parse the contents of `sys.argv` +at initialization time. You may pass a list of strings in the optional `argv` argument to use instead of the system argv: - conf = InvokeAIAppConfig(arg=['--xformers_enabled']) + conf.parse_args(argv=['--xformers_enabled']) -It is also possible to set a value at initialization time. This value -has highest priority. +It is also possible to set a value at initialization time. However, if +you call parse_args() it may be overwritten. conf = InvokeAIAppConfig(xformers_enabled=True) + conf.parse_args(argv=['--no-xformers']) + conf.xformers_enabled + # False + + +To avoid this, use `get_config()` to retrieve the application-wide +configuration object. This will retain any properties set at object +creation time: + + conf = InvokeAIAppConfig.get_config(xformers_enabled=True) + conf.parse_args(argv=['--no-xformers']) + conf.xformers_enabled + # True Any setting can be overwritten by setting an environment variable of form: "INVOKEAI_", as in: @@ -76,16 +90,24 @@ Order of precedence (from highest): 4) config file options 5) pydantic defaults -Typical usage: +Typical usage at the top level file: from invokeai.app.services.config import InvokeAIAppConfig - from invokeai.invocations.generate import TextToImageInvocation # get global configuration and print its nsfw_checker value conf = InvokeAIAppConfig.get_config() conf.parse_args() print(conf.nsfw_checker) +Typical usage in a backend module: + + from invokeai.app.services.config import InvokeAIAppConfig + + # get global configuration and print its nsfw_checker value + conf = InvokeAIAppConfig.get_config() + print(conf.nsfw_checker) + + Computed properties: The InvokeAIAppConfig object has a series of properties that @@ -138,6 +160,7 @@ two configs are kept in separate sections of the config file: legacy_conf_dir: configs/stable-diffusion outdir: outputs ... + ''' from __future__ import annotations import argparse diff --git a/invokeai/backend/config/invokeai_configure.py b/invokeai/backend/config/invokeai_configure.py index cf38dd93a6..4c0b0e3641 100755 --- a/invokeai/backend/config/invokeai_configure.py +++ b/invokeai/backend/config/invokeai_configure.py @@ -631,7 +631,7 @@ def edit_opts(program_opts: Namespace, invokeai_opts: Namespace) -> argparse.Nam def default_startup_options(init_file: Path) -> Namespace: - opts = InvokeAIAppConfig(argv=[]) + opts = InvokeAIAppConfig.get_config() outdir = Path(opts.outdir) if not outdir.is_absolute(): opts.outdir = str(config.root / opts.outdir) @@ -696,7 +696,7 @@ def write_opts(opts: Namespace, init_file: Path): """ # this will load current settings - config = InvokeAIAppConfig() + config = InvokeAIAppConfig.get_config() for key,value in opts.__dict__.items(): if hasattr(config,key): setattr(config,key,value) @@ -728,7 +728,7 @@ def write_default_options(program_opts: Namespace, initfile: Path): # yaml format. def migrate_init_file(legacy_format:Path): old = legacy_parser.parse_args([f'@{str(legacy_format)}']) - new = InvokeAIAppConfig(conf={}) + new = InvokeAIAppConfig.get_config() fields = list(get_type_hints(InvokeAIAppConfig).keys()) for attr in fields: diff --git a/invokeai/backend/training/textual_inversion_training.py b/invokeai/backend/training/textual_inversion_training.py index 8c27a6e718..c4290cacb3 100644 --- a/invokeai/backend/training/textual_inversion_training.py +++ b/invokeai/backend/training/textual_inversion_training.py @@ -88,7 +88,7 @@ def save_progress( def parse_args(): - config = InvokeAIAppConfig(argv=[]) + config = InvokeAIAppConfig.get_config() parser = PagingArgumentParser( description="Textual inversion training" ) 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 06/67] 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 07/67] 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 08/67] 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 09/67] 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 10/67] 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 11/67] 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 12/67] 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 13/67] 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 14/67] 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 15/67] 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 16/67] 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 17/67] 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 18/67] 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 19/67] 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 20/67] 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 21/67] 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 22/67] 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 23/67] 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 24/67] 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 25/67] 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 26/67] 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 27/67] 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 28/67] 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 29/67] 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 30/67] 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 31/67] 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 32/67] 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 33/67] 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 34/67] 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 35/67] 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 36/67] 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 37/67] 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 38/67] 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 39/67] 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 40/67] 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 41/67] 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 42/67] 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 43/67] 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 44/67] 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 45/67] 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 46/67] 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 47/67] 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 48/67] 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 49/67] 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", From 2c77563dcc9e5fd06201443cc51f3f696ce1e827 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Jun 2023 13:12:17 +1000 Subject: [PATCH 50/67] feat(ui): move DropOverlay into its own IAIDropOverlay component --- .../web/src/common/components/IAIDndImage.tsx | 99 +------------------ .../src/common/components/IAIDropOverlay.tsx | 91 +++++++++++++++++ 2 files changed, 96 insertions(+), 94 deletions(-) create mode 100644 invokeai/frontend/web/src/common/components/IAIDropOverlay.tsx diff --git a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx index b9b9e56722..5a7f93747b 100644 --- a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx +++ b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx @@ -1,23 +1,17 @@ -import { - Box, - Flex, - Icon, - IconButtonProps, - Image, - Text, -} from '@chakra-ui/react'; +import { Box, Flex, Icon, IconButtonProps, Image } from '@chakra-ui/react'; import { useDraggable, useDroppable } from '@dnd-kit/core'; import { useCombinedRefs } from '@dnd-kit/utilities'; import IAIIconButton from 'common/components/IAIIconButton'; import { IAIImageFallback } from 'common/components/IAIImageFallback'; import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; import { useGetUrl } from 'common/util/getUrl'; -import { AnimatePresence, motion } from 'framer-motion'; +import { AnimatePresence } from 'framer-motion'; import { ReactElement, SyntheticEvent } from 'react'; import { memo, useRef } from 'react'; import { FaImage, FaTimes } from 'react-icons/fa'; import { ImageDTO } from 'services/api'; import { v4 as uuidv4 } from 'uuid'; +import IAIDropOverlay from './IAIDropOverlay'; type IAIDndImageProps = { image: ImageDTO | null | undefined; @@ -138,7 +132,7 @@ const IAIDndImage = (props: IAIDndImageProps) => { )} - {active && } + {active && } )} @@ -164,7 +158,7 @@ const IAIDndImage = (props: IAIDndImageProps) => { /> - {active && } + {active && } )} @@ -173,86 +167,3 @@ const IAIDndImage = (props: IAIDndImageProps) => { }; export default memo(IAIDndImage); - -type DropOverlayProps = { - isOver: boolean; -}; - -const DropOverlay = (props: DropOverlayProps) => { - const { isOver } = props; - return ( - - - - - - - Drop - - - - - ); -}; diff --git a/invokeai/frontend/web/src/common/components/IAIDropOverlay.tsx b/invokeai/frontend/web/src/common/components/IAIDropOverlay.tsx new file mode 100644 index 0000000000..a5ec3b02dc --- /dev/null +++ b/invokeai/frontend/web/src/common/components/IAIDropOverlay.tsx @@ -0,0 +1,91 @@ +import { Flex, Text } from '@chakra-ui/react'; +import { motion } from 'framer-motion'; +import { memo, useRef } from 'react'; +import { v4 as uuidv4 } from 'uuid'; + +type Props = { + isOver: boolean; + label?: string; +}; + +export const IAIDropOverlay = (props: Props) => { + const { isOver, label = 'Drop' } = props; + const motionId = useRef(uuidv4()); + return ( + + + + + + + {label} + + + + + ); +}; + +export default memo(IAIDropOverlay); From 7bd94eac0eeb641b5d89391f4828135199cd46bb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Jun 2023 13:13:39 +1000 Subject: [PATCH 51/67] feat(ui): support image dnd to canvas --- .../src/features/canvas/store/canvasSlice.ts | 5 + .../UnifiedCanvasContentBeta.tsx | 72 ------------ .../UnifiedCanvas/UnifiedCanvasContent.tsx | 104 +++++++++++++++--- .../tabs/UnifiedCanvas/UnifiedCanvasTab.tsx | 20 +--- 4 files changed, 92 insertions(+), 109 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasContentBeta.tsx diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index 7f41066ba1..c0b73ed3ae 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -30,6 +30,7 @@ import { } from './canvasTypes'; import { ImageDTO } from 'services/api'; import { sessionCanceled } from 'services/thunks/session'; +import { setShouldUseCanvasBetaLayout } from 'features/ui/store/uiSlice'; export const initialLayerState: CanvasLayerState = { objects: [], @@ -851,6 +852,10 @@ export const canvasSlice = createSlice({ state.layerState.stagingArea = initialLayerState.stagingArea; } }); + + builder.addCase(setShouldUseCanvasBetaLayout, (state, action) => { + state.doesCanvasNeedScaling = true; + }); }, }); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasContentBeta.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasContentBeta.tsx deleted file mode 100644 index 601c36b9e2..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasContentBeta.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -// import IAICanvas from 'features/canvas/components/IAICanvas'; -import { Box, Flex } from '@chakra-ui/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAICanvas from 'features/canvas/components/IAICanvas'; -import IAICanvasResizer from 'features/canvas/components/IAICanvasResizer'; -import { canvasSelector } from 'features/canvas/store/canvasSelectors'; - -import { isEqual } from 'lodash-es'; -import { useLayoutEffect } from 'react'; -import UnifiedCanvasToolbarBeta from './UnifiedCanvasToolbarBeta'; -import UnifiedCanvasToolSettingsBeta from './UnifiedCanvasToolSettingsBeta'; -import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; - -const selector = createSelector( - [canvasSelector], - (canvas) => { - const { doesCanvasNeedScaling } = canvas; - return { - doesCanvasNeedScaling, - }; - }, - { - memoizeOptions: { - resultEqualityCheck: isEqual, - }, - } -); - -const UnifiedCanvasContentBeta = () => { - const dispatch = useAppDispatch(); - - const { doesCanvasNeedScaling } = useAppSelector(selector); - - useLayoutEffect(() => { - dispatch(requestCanvasRescale()); - const resizeCallback = () => { - dispatch(requestCanvasRescale()); - }; - - window.addEventListener('resize', resizeCallback); - - return () => window.removeEventListener('resize', resizeCallback); - }, [dispatch]); - - return ( - - - - - - {doesCanvasNeedScaling ? : } - - - - ); -}; - -export default UnifiedCanvasContentBeta; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasContent.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasContent.tsx index e56e6126a5..6f667f9f63 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasContent.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasContent.tsx @@ -1,34 +1,58 @@ import { Box, Flex } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAICanvas from 'features/canvas/components/IAICanvas'; import IAICanvasResizer from 'features/canvas/components/IAICanvasResizer'; import IAICanvasToolbar from 'features/canvas/components/IAICanvasToolbar/IAICanvasToolbar'; import { canvasSelector } from 'features/canvas/store/canvasSelectors'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; -import { isEqual } from 'lodash-es'; +import { uiSelector } from 'features/ui/store/uiSelectors'; -import { memo, useLayoutEffect } from 'react'; +import { memo, useCallback, useLayoutEffect } from 'react'; +import UnifiedCanvasToolbarBeta from './UnifiedCanvasBeta/UnifiedCanvasToolbarBeta'; +import UnifiedCanvasToolSettingsBeta from './UnifiedCanvasBeta/UnifiedCanvasToolSettingsBeta'; +import { ImageDTO } from 'services/api'; +import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; +import { useDroppable } from '@dnd-kit/core'; +import IAIDropOverlay from 'common/components/IAIDropOverlay'; const selector = createSelector( - [canvasSelector], - (canvas) => { + [canvasSelector, uiSelector], + (canvas, ui) => { const { doesCanvasNeedScaling } = canvas; + const { shouldUseCanvasBetaLayout } = ui; return { doesCanvasNeedScaling, + shouldUseCanvasBetaLayout, }; }, - { - memoizeOptions: { - resultEqualityCheck: isEqual, - }, - } + defaultSelectorOptions ); const UnifiedCanvasContent = () => { const dispatch = useAppDispatch(); - const { doesCanvasNeedScaling } = useAppSelector(selector); + const { doesCanvasNeedScaling, shouldUseCanvasBetaLayout } = + useAppSelector(selector); + + const onDrop = useCallback( + (droppedImage: ImageDTO) => { + dispatch(setInitialCanvasImage(droppedImage)); + }, + [dispatch] + ); + + const { + isOver, + setNodeRef: setDroppableRef, + active, + } = useDroppable({ + id: 'unifiedCanvas', + data: { + handleDrop: onDrop, + }, + }); useLayoutEffect(() => { dispatch(requestCanvasRescale()); @@ -42,14 +66,55 @@ const UnifiedCanvasContent = () => { return () => window.removeEventListener('resize', resizeCallback); }, [dispatch]); + if (shouldUseCanvasBetaLayout) { + return ( + + + + + + + {doesCanvasNeedScaling ? : } + {active && } + + + + + ); + } + return ( { flexDirection: 'column', alignItems: 'center', gap: 4, - width: '100%', - height: '100%', + w: 'full', + h: 'full', }} > @@ -68,11 +133,14 @@ const UnifiedCanvasContent = () => { alignItems: 'center', justifyContent: 'center', gap: 4, - width: '100%', - height: '100%', + w: 'full', + h: 'full', }} > - {doesCanvasNeedScaling ? : } + + {doesCanvasNeedScaling ? : } + {active && } + diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasTab.tsx index 2d591d1ecc..6905879bdc 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasTab.tsx @@ -1,34 +1,16 @@ import { Flex } from '@chakra-ui/react'; import { memo } from 'react'; -import { createSelector } from '@reduxjs/toolkit'; -import { uiSelector } from 'features/ui/store/uiSelectors'; -import { useAppSelector } from 'app/store/storeHooks'; import UnifiedCanvasContent from './UnifiedCanvasContent'; import UnifiedCanvasParameters from './UnifiedCanvasParameters'; -import UnifiedCanvasContentBeta from './UnifiedCanvasBeta/UnifiedCanvasContentBeta'; import ParametersPinnedWrapper from '../../ParametersPinnedWrapper'; -const selector = createSelector(uiSelector, (ui) => { - const { shouldUseCanvasBetaLayout } = ui; - - return { - shouldUseCanvasBetaLayout, - }; -}); - const UnifiedCanvasTab = () => { - const { shouldUseCanvasBetaLayout } = useAppSelector(selector); - return ( - {shouldUseCanvasBetaLayout ? ( - - ) : ( - - )} + ); }; From b1000e30c1d812de64586c495b69fb5a23525aa4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Jun 2023 15:22:29 +1000 Subject: [PATCH 52/67] feat(ui): disable keyboard dnd Need to fix a bug w/ collision detection before enabling it. Will pursue later. --- .../web/src/app/components/ImageDnd/ImageDndContext.tsx | 7 +++++-- .../components/tabs/UnifiedCanvas/UnifiedCanvasContent.tsx | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx b/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx index 72487f329c..2d669c529a 100644 --- a/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx +++ b/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx @@ -46,9 +46,12 @@ const ImageDndContext = (props: ImageDndContextProps) => { const touchSensor = useSensor(TouchSensor, { activationConstraint: { distance: 15 }, }); - const keyboardSensor = useSensor(KeyboardSensor); + // TODO: Use KeyboardSensor - needs composition of multiple collisionDetection algos + // Alternatively, fix `rectIntersection` collection detection to work with the drag overlay + // (currently the drag element collision rect is not correctly calculated) + // const keyboardSensor = useSensor(KeyboardSensor); - const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor); + const sensors = useSensors(mouseSensor, touchSensor); return ( { return ( { return ( Date: Mon, 5 Jun 2023 16:43:18 +1000 Subject: [PATCH 53/67] feat(ui): fix image fit - Prevent init, current & control images from overflowing --- .../controlNet/components/ControlNetImagePreview.tsx | 5 ++++- .../src/features/gallery/components/CurrentImageDisplay.tsx | 1 + .../Parameters/ImageToImage/InitialImagePreview.tsx | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx index 632f88b57b..b4ed2c254e 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx @@ -60,7 +60,10 @@ const ControlNetImagePreview = (props: Props) => { processorType !== 'none'; return ( - + { alignItems: 'center', justifyContent: 'center', gap: 4, + position: 'absolute', }} > 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 a1c4d5acab..72dec6c149 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 @@ -72,9 +72,10 @@ const InitialImagePreview = () => { sx={{ width: 'full', height: 'full', - position: 'relative', + position: 'absolute', alignItems: 'center', justifyContent: 'center', + p: 4, }} > Date: Tue, 6 Jun 2023 14:08:04 +1000 Subject: [PATCH 54/67] feat(ui): improve UI on smaller screens - responsive changes were causing a lot of weird layout issues, had to remove the rest of them - canvas (non-beta) toolbar now wraps - reduces minH for prompt boxes a bit --- .../frontend/web/src/app/components/App.tsx | 21 ++- .../IAICanvasToolbar/IAICanvasToolbar.tsx | 21 ++- .../nodes/components/FieldTypeLegend.tsx | 2 +- .../features/nodes/components/NodeEditor.tsx | 2 +- .../Core/ParamNegativeConditioning.tsx | 1 + .../Core/ParamPositiveConditioning.tsx | 2 +- .../components/InvokeAILogoComponent.tsx | 22 +-- .../features/system/components/SiteHeader.tsx | 174 +++++++++++++----- .../src/features/ui/components/InvokeTabs.tsx | 16 +- .../components/PinParametersPanelButton.tsx | 1 - 10 files changed, 168 insertions(+), 94 deletions(-) diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 33fa57f0b3..21b3945490 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -76,18 +76,21 @@ const App = ({ {isLightboxEnabled && } {headerComponent || } diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx index 69eed2b46a..30ff6fff81 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx @@ -1,4 +1,4 @@ -import { ButtonGroup, Flex } from '@chakra-ui/react'; +import { Box, ButtonGroup, Flex } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIIconButton from 'common/components/IAIIconButton'; @@ -210,16 +210,19 @@ const IAICanvasToolbar = () => { sx={{ alignItems: 'center', gap: 2, + flexWrap: 'wrap', }} > - + + + diff --git a/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx b/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx index c14c7ebccf..78316cc694 100644 --- a/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx @@ -6,7 +6,7 @@ import { memo } from 'react'; const FieldTypeLegend = () => { return ( - + {map(FIELDS, ({ title, description, color }, key) => ( { sx={{ position: 'relative', width: 'full', - height: { base: '100vh', xl: 'full' }, + height: 'full', borderRadius: 'md', bg: 'base.850', }} diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx index 28ab50ff82..70c342cc3b 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx @@ -25,6 +25,7 @@ const ParamNegativeConditioning = () => { borderColor: 'error.600', }} fontSize="sm" + minH={16} /> ); 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 0980b84ab3..82b43517f8 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 @@ -82,7 +82,7 @@ const ParamPositiveConditioning = () => { onKeyDown={handleKeyDown} resize="vertical" ref={promptRef} - minH={{ base: 20, lg: 40 }} + minH={32} /> diff --git a/invokeai/frontend/web/src/features/system/components/InvokeAILogoComponent.tsx b/invokeai/frontend/web/src/features/system/components/InvokeAILogoComponent.tsx index f6017d02f0..bec2c32b61 100644 --- a/invokeai/frontend/web/src/features/system/components/InvokeAILogoComponent.tsx +++ b/invokeai/frontend/web/src/features/system/components/InvokeAILogoComponent.tsx @@ -13,22 +13,16 @@ const InvokeAILogoComponent = () => { invoke-ai-logo - - + /> + + invoke ai { - const [menuOpened, setMenuOpened] = useState(false); - const resolution = useResolution(); const { t } = useTranslation(); + const isModelManagerEnabled = + useFeatureStatus('modelManager').isFeatureEnabled; + const isLocalizationEnabled = + useFeatureStatus('localization').isFeatureEnabled; + const isBugLinkEnabled = useFeatureStatus('bugLink').isFeatureEnabled; + const isDiscordLinkEnabled = useFeatureStatus('discordLink').isFeatureEnabled; + const isGithubLinkEnabled = useFeatureStatus('githubLink').isFeatureEnabled; + return ( - - - - - - + + + - {resolution === 'desktop' ? ( - - ) : ( + {isModelManagerEnabled && ( + } - aria-label={t('accessibility.menu')} - background={menuOpened ? 'base.800' : 'none'} - _hover={{ background: menuOpened ? 'base.800' : 'none' }} - onClick={() => setMenuOpened(!menuOpened)} - p={0} - > - )} - - - {resolution !== 'desktop' && menuOpened && ( - - - + aria-label={t('modelManager.modelManager')} + tooltip={t('modelManager.modelManager')} + size="sm" + variant="link" + data-variant="link" + fontSize={20} + icon={} + /> + )} - + + + } + /> + + + + + {isLocalizationEnabled && } + + {isBugLinkEnabled && ( + + } + /> + + )} + + {isGithubLinkEnabled && ( + + } + /> + + )} + + {isDiscordLinkEnabled && ( + + } + /> + + )} + + + } + /> + + ); }; diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index 23fc6bd192..c164b87515 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -152,16 +152,18 @@ const InvokeTabs = () => { onChange={(index: number) => { dispatch(setActiveTab(index)); }} - flexGrow={1} - flexDir={{ base: 'column', xl: 'row' }} - gap={{ base: 4 }} + sx={{ + flexGrow: 1, + gap: 4, + }} isLazy > {tabs} diff --git a/invokeai/frontend/web/src/features/ui/components/PinParametersPanelButton.tsx b/invokeai/frontend/web/src/features/ui/components/PinParametersPanelButton.tsx index 46d0fa3f93..a742e2a587 100644 --- a/invokeai/frontend/web/src/features/ui/components/PinParametersPanelButton.tsx +++ b/invokeai/frontend/web/src/features/ui/components/PinParametersPanelButton.tsx @@ -33,7 +33,6 @@ const PinParametersPanelButton = (props: PinParametersPanelButtonProps) => { icon={shouldPinParametersPanel ? : } variant="ghost" size="sm" - px={{ base: 10, xl: 0 }} sx={{ color: 'base.700', _hover: { From 229de2dbb8caf56a0a4acee3a0cdb864151488c0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Jun 2023 11:00:15 +1000 Subject: [PATCH 55/67] feat(ui): fix canvas saving - fix "bounding box region only" not being respected when saving - add toasts for each action - improve workflow `take()` predicates to use the requestId --- .../listeners/canvasCopiedToClipboard.ts | 7 ++++++ .../listeners/canvasDownloadedAsImage.ts | 3 ++- .../listeners/canvasMerged.ts | 24 +++++++++--------- .../listeners/canvasSavedToGallery.ts | 21 +++++++++------- .../listeners/imageUploaded.ts | 17 ++++++++++++- .../features/canvas/util/getBaseLayerBlob.ts | 25 +++++++++---------- .../canvas/util/getFullBaseLayerBlob.ts | 19 ++++++++++++++ 7 files changed, 80 insertions(+), 36 deletions(-) create mode 100644 invokeai/frontend/web/src/features/canvas/util/getFullBaseLayerBlob.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts index 16642f1f32..a7ddd8e917 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts @@ -28,6 +28,13 @@ export const addCanvasCopiedToClipboardListener = () => { } copyBlobToClipboard(blob); + + dispatch( + addToast({ + title: 'Canvas Copied to Clipboard', + status: 'success', + }) + ); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts index ef4c63b31c..c97df09cff 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts @@ -27,7 +27,8 @@ export const addCanvasDownloadedAsImageListener = () => { return; } - downloadBlob(blob, 'mergedCanvas.png'); + downloadBlob(blob, 'canvas.png'); + dispatch(addToast({ title: 'Canvas Downloaded', status: 'success' })); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts index 80865f3126..ed157066bb 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts @@ -1,22 +1,20 @@ import { canvasMerged } from 'features/canvas/store/actions'; import { startAppListening } from '..'; import { log } from 'app/logging/useLogger'; -import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; import { addToast } from 'features/system/store/systemSlice'; import { imageUploaded } from 'services/thunks/image'; -import { v4 as uuidv4 } from 'uuid'; import { setMergedCanvas } from 'features/canvas/store/canvasSlice'; import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider'; +import { getFullBaseLayerBlob } from 'features/canvas/util/getFullBaseLayerBlob'; const moduleLog = log.child({ namespace: 'canvasCopiedToClipboardListener' }); +export const MERGED_CANVAS_FILENAME = 'mergedCanvas.png'; export const addCanvasMergedListener = () => { startAppListening({ actionCreator: canvasMerged, effect: async (action, { dispatch, getState, take }) => { - const state = getState(); - - const blob = await getBaseLayerBlob(state, true); + const blob = await getFullBaseLayerBlob(); if (!blob) { moduleLog.error('Problem getting base layer blob'); @@ -48,12 +46,12 @@ export const addCanvasMergedListener = () => { relativeTo: canvasBaseLayer.getParent(), }); - const filename = `mergedCanvas_${uuidv4()}.png`; - - dispatch( + const imageUploadedRequest = dispatch( imageUploaded({ formData: { - file: new File([blob], filename, { type: 'image/png' }), + file: new File([blob], MERGED_CANVAS_FILENAME, { + type: 'image/png', + }), }, imageCategory: 'general', isIntermediate: true, @@ -61,9 +59,11 @@ export const addCanvasMergedListener = () => { ); const [{ payload }] = await take( - (action): action is ReturnType => - imageUploaded.fulfilled.match(action) && - action.meta.arg.formData.file.name === filename + ( + uploadedImageAction + ): uploadedImageAction is ReturnType => + imageUploaded.fulfilled.match(uploadedImageAction) && + uploadedImageAction.meta.requestId === imageUploadedRequest.requestId ); const mergedCanvasImage = payload; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts index b89620775b..2ea69df179 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts @@ -4,9 +4,10 @@ import { log } from 'app/logging/useLogger'; import { imageUploaded } from 'services/thunks/image'; import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; import { addToast } from 'features/system/store/systemSlice'; -import { v4 as uuidv4 } from 'uuid'; import { imageUpserted } from 'features/gallery/store/imagesSlice'; +export const SAVED_CANVAS_FILENAME = 'savedCanvas.png'; + const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' }); export const addCanvasSavedToGalleryListener = () => { @@ -15,7 +16,7 @@ export const addCanvasSavedToGalleryListener = () => { effect: async (action, { dispatch, getState, take }) => { const state = getState(); - const blob = await getBaseLayerBlob(state, true); + const blob = await getBaseLayerBlob(state); if (!blob) { moduleLog.error('Problem getting base layer blob'); @@ -29,12 +30,12 @@ export const addCanvasSavedToGalleryListener = () => { return; } - const filename = `mergedCanvas_${uuidv4()}.png`; - - dispatch( + const imageUploadedRequest = dispatch( imageUploaded({ formData: { - file: new File([blob], filename, { type: 'image/png' }), + file: new File([blob], SAVED_CANVAS_FILENAME, { + type: 'image/png', + }), }, imageCategory: 'general', isIntermediate: false, @@ -42,9 +43,11 @@ export const addCanvasSavedToGalleryListener = () => { ); const [{ payload: uploadedImageDTO }] = await take( - (action): action is ReturnType => - imageUploaded.fulfilled.match(action) && - action.meta.arg.formData.file.name === filename + ( + uploadedImageAction + ): uploadedImageAction is ReturnType => + imageUploaded.fulfilled.match(uploadedImageAction) && + uploadedImageAction.meta.requestId === imageUploadedRequest.requestId ); dispatch(imageUpserted(uploadedImageDTO)); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index 6d84431f80..bfc362e48d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -3,6 +3,8 @@ import { imageUploaded } from 'services/thunks/image'; import { addToast } from 'features/system/store/systemSlice'; import { log } from 'app/logging/useLogger'; import { imageUpserted } from 'features/gallery/store/imagesSlice'; +import { SAVED_CANVAS_FILENAME } from './canvasSavedToGallery'; +import { MERGED_CANVAS_FILENAME } from './canvasMerged'; const moduleLog = log.child({ namespace: 'image' }); @@ -19,9 +21,22 @@ export const addImageUploadedFulfilledListener = () => { return; } - const state = getState(); + const originalFileName = action.meta.arg.formData.file.name; dispatch(imageUpserted(image)); + + if (originalFileName === SAVED_CANVAS_FILENAME) { + dispatch( + addToast({ title: 'Canvas Saved to Gallery', status: 'success' }) + ); + return; + } + + if (originalFileName === MERGED_CANVAS_FILENAME) { + dispatch(addToast({ title: 'Canvas Merged', status: 'success' })); + return; + } + dispatch(addToast({ title: 'Image Uploaded', status: 'success' })); }, }); diff --git a/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts b/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts index a576551d72..20ac482710 100644 --- a/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts +++ b/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts @@ -2,10 +2,10 @@ import { getCanvasBaseLayer } from './konvaInstanceProvider'; import { RootState } from 'app/store/store'; import { konvaNodeToBlob } from './konvaNodeToBlob'; -export const getBaseLayerBlob = async ( - state: RootState, - withoutBoundingBox?: boolean -) => { +/** + * Get the canvas base layer blob, with or without bounding box according to `shouldCropToBoundingBoxOnSave` + */ +export const getBaseLayerBlob = async (state: RootState) => { const canvasBaseLayer = getCanvasBaseLayer(); if (!canvasBaseLayer) { @@ -24,15 +24,14 @@ export const getBaseLayerBlob = async ( const absPos = clonedBaseLayer.getAbsolutePosition(); - const boundingBox = - shouldCropToBoundingBoxOnSave && !withoutBoundingBox - ? { - x: boundingBoxCoordinates.x + absPos.x, - y: boundingBoxCoordinates.y + absPos.y, - width: boundingBoxDimensions.width, - height: boundingBoxDimensions.height, - } - : clonedBaseLayer.getClientRect(); + const boundingBox = shouldCropToBoundingBoxOnSave + ? { + x: boundingBoxCoordinates.x + absPos.x, + y: boundingBoxCoordinates.y + absPos.y, + width: boundingBoxDimensions.width, + height: boundingBoxDimensions.height, + } + : clonedBaseLayer.getClientRect(); return konvaNodeToBlob(clonedBaseLayer, boundingBox); }; diff --git a/invokeai/frontend/web/src/features/canvas/util/getFullBaseLayerBlob.ts b/invokeai/frontend/web/src/features/canvas/util/getFullBaseLayerBlob.ts new file mode 100644 index 0000000000..ba855723fb --- /dev/null +++ b/invokeai/frontend/web/src/features/canvas/util/getFullBaseLayerBlob.ts @@ -0,0 +1,19 @@ +import { getCanvasBaseLayer } from './konvaInstanceProvider'; +import { konvaNodeToBlob } from './konvaNodeToBlob'; + +/** + * Gets the canvas base layer blob, without bounding box + */ +export const getFullBaseLayerBlob = async () => { + const canvasBaseLayer = getCanvasBaseLayer(); + + if (!canvasBaseLayer) { + return; + } + + const clonedBaseLayer = canvasBaseLayer.clone(); + + clonedBaseLayer.scale({ x: 1, y: 1 }); + + return konvaNodeToBlob(clonedBaseLayer, clonedBaseLayer.getClientRect()); +}; From fc5f9c30a64e12777a2e02a6dd853a981cc3cda4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Jun 2023 12:28:18 +1000 Subject: [PATCH 56/67] fix(ui): fix metadata viewer not working for canvas images --- .../web/src/features/gallery/components/CurrentImagePreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx index 12d62ead70..5e210bf4b7 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx @@ -117,7 +117,7 @@ const CurrentImagePreview = () => { /> )} - {shouldShowImageDetails && image && image.metadata && ( + {shouldShowImageDetails && image && ( Date: Tue, 6 Jun 2023 12:31:10 +1000 Subject: [PATCH 57/67] fix(ui): fix canvas auto-save not working --- .../listeners/imageMetadataReceived.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts index 63aeecb95e..7d7e92ff61 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts @@ -1,6 +1,6 @@ import { log } from 'app/logging/useLogger'; import { startAppListening } from '..'; -import { imageMetadataReceived } from 'services/thunks/image'; +import { imageMetadataReceived, imageUpdated } from 'services/thunks/image'; import { imageUpserted } from 'features/gallery/store/imagesSlice'; const moduleLog = log.child({ namespace: 'image' }); @@ -10,10 +10,25 @@ export const addImageMetadataReceivedFulfilledListener = () => { actionCreator: imageMetadataReceived.fulfilled, effect: (action, { getState, dispatch }) => { const image = action.payload; - if (image.is_intermediate) { + + const state = getState(); + + if ( + image.session_id === state.canvas.layerState.stagingArea.sessionId && + state.canvas.shouldAutoSave + ) { + dispatch( + imageUpdated({ + imageName: image.image_name, + imageOrigin: image.image_origin, + requestBody: { is_intermediate: false }, + }) + ); + } else if (image.is_intermediate) { // No further actions needed for intermediate images return; } + moduleLog.debug({ data: { image } }, 'Image metadata received'); dispatch(imageUpserted(image)); }, From 840c632c0a708c1f1f22acd78b18221211cbb7b3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Jun 2023 12:33:00 +1000 Subject: [PATCH 58/67] feat(ui): sort images by `updated_at` instead of `created_at` fixes issue where saved staging area images are sorted as expected in gallery. --- invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts b/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts index cb6469aeb4..de2cdb48b2 100644 --- a/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts @@ -12,7 +12,7 @@ import { receivedPageOfImages } from 'services/thunks/image'; export const imagesAdapter = createEntityAdapter({ selectId: (image) => image.image_name, - sortComparer: (a, b) => dateComparator(b.created_at, a.created_at), + sortComparer: (a, b) => dateComparator(b.updated_at, a.updated_at), }); export const IMAGE_CATEGORIES: ImageCategory[] = ['general']; From 3ff732d58310456a2954f72f7d2e90bbc05a215f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Jun 2023 16:01:35 +1000 Subject: [PATCH 59/67] feat(ui): clear controlnet image when image deleted --- .../listeners/imageDeleted.ts | 8 +------- .../controlNet/store/controlNetSlice.ts | 16 +++++++++++++++ .../src/features/gallery/store/imagesSlice.ts | 20 ++++++++----------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts index bf7ca4020c..e8e7a78165 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts @@ -5,8 +5,6 @@ import { log } from 'app/logging/useLogger'; import { clamp } from 'lodash-es'; import { imageSelected } from 'features/gallery/store/gallerySlice'; import { - imageRemoved, - imagesAdapter, selectImagesEntities, selectImagesIds, } from 'features/gallery/store/imagesSlice'; @@ -58,8 +56,6 @@ export const addRequestedImageDeletionListener = () => { } } - dispatch(imageRemoved(image_name)); - dispatch( imageDeleted({ imageName: image_name, imageOrigin: image_origin }) ); @@ -74,9 +70,7 @@ export const addImageDeletedPendingListener = () => { startAppListening({ actionCreator: imageDeleted.pending, effect: (action, { dispatch, getState }) => { - const { imageName, imageOrigin } = action.meta.arg; - // Preemptively remove the image from the gallery - imagesAdapter.removeOne(getState().images, imageName); + // }, }); }; diff --git a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts index 1389457aba..40714d3ecb 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts @@ -13,6 +13,8 @@ import { ControlNetModel, } from './constants'; import { controlNetImageProcessed } from './actions'; +import { imageDeleted } from 'services/thunks/image'; +import { forEach } from 'lodash-es'; export const initialControlNet: Omit = { isEnabled: true, @@ -194,6 +196,20 @@ export const controlNetSlice = createSlice({ state.isProcessingControlImage = true; } }); + + builder.addCase(imageDeleted.pending, (state, action) => { + // Preemptively remove the image from the gallery + const { imageName } = action.meta.arg; + forEach(state.controlNets, (c) => { + if (c.controlImage?.image_name === imageName) { + c.controlImage = null; + c.processedControlImage = null; + } + if (c.processedControlImage?.image_name === imageName) { + c.processedControlImage = null; + } + }); + }); }, }); diff --git a/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts b/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts index de2cdb48b2..539690dcde 100644 --- a/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts @@ -7,8 +7,8 @@ import { import { RootState } from 'app/store/store'; import { ImageCategory, ImageDTO } from 'services/api'; import { dateComparator } from 'common/util/dateComparator'; -import { isString, keyBy } from 'lodash-es'; -import { receivedPageOfImages } from 'services/thunks/image'; +import { keyBy } from 'lodash-es'; +import { imageDeleted, receivedPageOfImages } from 'services/thunks/image'; export const imagesAdapter = createEntityAdapter({ selectId: (image) => image.image_name, @@ -49,14 +49,6 @@ const imagesSlice = createSlice({ imageUpserted: (state, action: PayloadAction) => { imagesAdapter.upsertOne(state, action.payload); }, - imageRemoved: (state, action: PayloadAction) => { - if (isString(action.payload)) { - imagesAdapter.removeOne(state, action.payload); - return; - } - - imagesAdapter.removeOne(state, action.payload.image_name); - }, imageCategoriesChanged: (state, action: PayloadAction) => { state.categories = action.payload; }, @@ -76,6 +68,11 @@ const imagesSlice = createSlice({ state.total = total; imagesAdapter.upsertMany(state, items); }); + builder.addCase(imageDeleted.pending, (state, action) => { + // Preemptively remove the image from the gallery + const { imageName } = action.meta.arg; + imagesAdapter.removeOne(state, imageName); + }); }, }); @@ -87,8 +84,7 @@ export const { selectTotal: selectImagesTotal, } = imagesAdapter.getSelectors((state) => state.images); -export const { imageUpserted, imageRemoved, imageCategoriesChanged } = - imagesSlice.actions; +export const { imageUpserted, imageCategoriesChanged } = imagesSlice.actions; export default imagesSlice.reducer; From 2fc0a4d53be0d762394ba5a5319982fb7400c502 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Jun 2023 20:16:43 +1000 Subject: [PATCH 60/67] feat(ui): improve handling for urls/metadata received Update images everywhere when urls or metadata is received: - control images - init images - canvas - nodes - init image Also renamed the variable. --- .../listeners/imageDeleted.ts | 5 +++ .../listeners/imageMetadataReceived.ts | 4 +++ .../listeners/imageUrlsReceived.ts | 15 ++++----- .../frontend/web/src/app/types/invokeai.ts | 1 + .../src/features/canvas/store/canvasSlice.ts | 21 ++++++++++++ .../controlNet/store/controlNetSlice.ts | 18 ++++++++++- .../features/gallery/store/gallerySlice.ts | 10 ++++++ .../src/features/gallery/store/imagesSlice.ts | 32 +++++++++++++++++-- .../src/features/nodes/store/nodesSlice.ts | 20 +++++++++--- .../parameters/store/generationSlice.ts | 11 +++++++ .../src/features/system/store/configSlice.ts | 1 + 11 files changed, 122 insertions(+), 16 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts index e8e7a78165..b527b5d00b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts @@ -5,6 +5,7 @@ import { log } from 'app/logging/useLogger'; import { clamp } from 'lodash-es'; import { imageSelected } from 'features/gallery/store/gallerySlice'; import { + imageRemoved, selectImagesEntities, selectImagesIds, } from 'features/gallery/store/imagesSlice'; @@ -56,6 +57,10 @@ export const addRequestedImageDeletionListener = () => { } } + // Preemptively remove from gallery + dispatch(imageRemoved(image_name)); + + // Delete from server dispatch( imageDeleted({ imageName: image_name, imageOrigin: image_origin }) ); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts index 7d7e92ff61..016e3ec8a8 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts @@ -26,6 +26,10 @@ export const addImageMetadataReceivedFulfilledListener = () => { ); } else if (image.is_intermediate) { // No further actions needed for intermediate images + moduleLog.trace( + { data: { image } }, + 'Image metadata received (intermediate), skipping' + ); return; } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUrlsReceived.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUrlsReceived.ts index fd0461f893..2e365a20ac 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUrlsReceived.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUrlsReceived.ts @@ -1,7 +1,7 @@ import { log } from 'app/logging/useLogger'; import { startAppListening } from '..'; import { imageUrlsReceived } from 'services/thunks/image'; -import { imagesAdapter } from 'features/gallery/store/imagesSlice'; +import { imageUpdatedOne } from 'features/gallery/store/imagesSlice'; const moduleLog = log.child({ namespace: 'image' }); @@ -14,13 +14,12 @@ export const addImageUrlsReceivedFulfilledListener = () => { const { image_name, image_url, thumbnail_url } = image; - imagesAdapter.updateOne(getState().images, { - id: image_name, - changes: { - image_url, - thumbnail_url, - }, - }); + dispatch( + imageUpdatedOne({ + id: image_name, + changes: { image_url, thumbnail_url }, + }) + ); }, }); }; diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index 304b094749..fa5c725a84 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -114,6 +114,7 @@ export type AppConfig = { /** * Whether or not we need to re-fetch images */ + shouldUpdateImageUrlsOnError: boolean; disabledTabs: InvokeTabName[]; disabledFeatures: AppFeature[]; disabledSDFeatures: SDFeature[]; diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index c0b73ed3ae..4742de0483 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -31,6 +31,7 @@ import { import { ImageDTO } from 'services/api'; import { sessionCanceled } from 'services/thunks/session'; import { setShouldUseCanvasBetaLayout } from 'features/ui/store/uiSlice'; +import { imageUrlsReceived } from 'services/thunks/image'; export const initialLayerState: CanvasLayerState = { objects: [], @@ -856,6 +857,26 @@ export const canvasSlice = createSlice({ builder.addCase(setShouldUseCanvasBetaLayout, (state, action) => { state.doesCanvasNeedScaling = true; }); + builder.addCase(imageUrlsReceived.fulfilled, (state, action) => { + const { image_name, image_origin, image_url, thumbnail_url } = + action.payload; + + state.layerState.objects.forEach((object) => { + if (object.kind === 'image') { + if (object.image.image_name === image_name) { + object.image.image_url = image_url; + object.image.thumbnail_url = thumbnail_url; + } + } + }); + + state.layerState.stagingArea.images.forEach((stagedImage) => { + if (stagedImage.image.image_name === image_name) { + stagedImage.image.image_url = image_url; + stagedImage.image.thumbnail_url = thumbnail_url; + } + }); + }); }, }); diff --git a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts index 40714d3ecb..da76ce4a8a 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts @@ -13,7 +13,7 @@ import { ControlNetModel, } from './constants'; import { controlNetImageProcessed } from './actions'; -import { imageDeleted } from 'services/thunks/image'; +import { imageDeleted, imageUrlsReceived } from 'services/thunks/image'; import { forEach } from 'lodash-es'; export const initialControlNet: Omit = { @@ -210,6 +210,22 @@ export const controlNetSlice = createSlice({ } }); }); + + builder.addCase(imageUrlsReceived.fulfilled, (state, action) => { + const { image_name, image_origin, image_url, thumbnail_url } = + action.payload; + + forEach(state.controlNets, (c) => { + if (c.controlImage?.image_name === image_name) { + c.controlImage.image_url = image_url; + c.controlImage.thumbnail_url = thumbnail_url; + } + if (c.processedControlImage?.image_name === image_name) { + c.processedControlImage.image_url = image_url; + c.processedControlImage.thumbnail_url = thumbnail_url; + } + }); + }); }, }); diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 8e5ecf64fa..b9d091305a 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -2,6 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import { ImageDTO } from 'services/api'; import { imageUpserted } from './imagesSlice'; +import { imageUrlsReceived } from 'services/thunks/image'; type GalleryImageObjectFitType = 'contain' | 'cover'; @@ -57,6 +58,15 @@ export const gallerySlice = createSlice({ state.selectedImage = action.payload; } }); + builder.addCase(imageUrlsReceived.fulfilled, (state, action) => { + const { image_name, image_origin, image_url, thumbnail_url } = + action.payload; + + if (state.selectedImage?.image_name === image_name) { + state.selectedImage.image_url = image_url; + state.selectedImage.thumbnail_url = thumbnail_url; + } + }); }, }); diff --git a/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts b/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts index 539690dcde..c9fc61d10d 100644 --- a/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts @@ -1,5 +1,6 @@ import { PayloadAction, + Update, createEntityAdapter, createSelector, createSlice, @@ -8,7 +9,12 @@ import { RootState } from 'app/store/store'; import { ImageCategory, ImageDTO } from 'services/api'; import { dateComparator } from 'common/util/dateComparator'; import { keyBy } from 'lodash-es'; -import { imageDeleted, receivedPageOfImages } from 'services/thunks/image'; +import { + imageDeleted, + imageMetadataReceived, + imageUrlsReceived, + receivedPageOfImages, +} from 'services/thunks/image'; export const imagesAdapter = createEntityAdapter({ selectId: (image) => image.image_name, @@ -49,6 +55,12 @@ const imagesSlice = createSlice({ imageUpserted: (state, action: PayloadAction) => { imagesAdapter.upsertOne(state, action.payload); }, + imageUpdatedOne: (state, action: PayloadAction>) => { + imagesAdapter.updateOne(state, action.payload); + }, + imageRemoved: (state, action: PayloadAction) => { + imagesAdapter.removeOne(state, action.payload); + }, imageCategoriesChanged: (state, action: PayloadAction) => { state.categories = action.payload; }, @@ -69,10 +81,19 @@ const imagesSlice = createSlice({ imagesAdapter.upsertMany(state, items); }); builder.addCase(imageDeleted.pending, (state, action) => { - // Preemptively remove the image from the gallery + // Image deleted const { imageName } = action.meta.arg; imagesAdapter.removeOne(state, imageName); }); + builder.addCase(imageUrlsReceived.fulfilled, (state, action) => { + const { image_name, image_origin, image_url, thumbnail_url } = + action.payload; + + imagesAdapter.updateOne(state, { + id: image_name, + changes: { image_url, thumbnail_url }, + }); + }); }, }); @@ -84,7 +105,12 @@ export const { selectTotal: selectImagesTotal, } = imagesAdapter.getSelectors((state) => state.images); -export const { imageUpserted, imageCategoriesChanged } = imagesSlice.actions; +export const { + imageUpserted, + imageUpdatedOne, + imageRemoved, + imageCategoriesChanged, +} = imagesSlice.actions; export default imagesSlice.reducer; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 3c93be7ac5..50c33e88b2 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -16,9 +16,10 @@ import { receivedOpenAPISchema } from 'services/thunks/schema'; import { InvocationTemplate, InvocationValue } from '../types/types'; import { parseSchema } from '../util/parseSchema'; import { log } from 'app/logging/useLogger'; -import { size } from 'lodash-es'; +import { forEach, size } from 'lodash-es'; import { isAnyGraphBuilt } from './actions'; import { RgbaColor } from 'react-colorful'; +import { imageUrlsReceived } from 'services/thunks/image'; export type NodesState = { nodes: Node[]; @@ -98,9 +99,20 @@ const nodesSlice = createSlice({ state.schema = action.payload; }); - builder.addMatcher(isAnyGraphBuilt, (state, action) => { - // TODO: Achtung! Side effect in a reducer! - log.info({ namespace: 'nodes', data: action.payload }, 'Graph built'); + builder.addCase(imageUrlsReceived.fulfilled, (state, action) => { + const { image_name, image_origin, image_url, thumbnail_url } = + action.payload; + + state.nodes.forEach((node) => { + forEach(node.data.inputs, (input) => { + if (input.type === 'image') { + if (input.value?.image_name === image_name) { + input.value.image_url = image_url; + input.value.thumbnail_url = thumbnail_url; + } + } + }); + }); }); }, }); diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index 6420950e4a..3512ded3ab 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -17,6 +17,7 @@ import { StrengthParam, WidthParam, } from './parameterZodSchemas'; +import { imageUrlsReceived } from 'services/thunks/image'; export interface GenerationState { cfgScale: CfgScaleParam; @@ -231,6 +232,16 @@ export const generationSlice = createSlice({ state.model = defaultModel; } }); + + builder.addCase(imageUrlsReceived.fulfilled, (state, action) => { + const { image_name, image_origin, image_url, thumbnail_url } = + action.payload; + + if (state.initialImage?.image_name === image_name) { + state.initialImage.image_url = image_url; + state.initialImage.thumbnail_url = thumbnail_url; + } + }); }, }); diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts index f8cb3a483c..5e0d2ca472 100644 --- a/invokeai/frontend/web/src/features/system/store/configSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts @@ -5,6 +5,7 @@ import { merge } from 'lodash-es'; export const initialConfigState: AppConfig = { shouldTransformUrls: false, + shouldUpdateImageUrlsOnError: false, disabledTabs: [], disabledFeatures: [], disabledSDFeatures: [], From 8283d23b74f8d0d997cb04d9e0e72ef5dc22d029 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Jun 2023 20:24:18 +1000 Subject: [PATCH 61/67] feat(ui): remove `shouldTransformUrls` This is no longer used. --- .../frontend/web/src/app/types/invokeai.ts | 6 +--- .../web/src/common/components/IAIDndImage.tsx | 4 +-- .../frontend/web/src/common/util/getUrl.ts | 34 ------------------- .../components/IAICanvasObjectRenderer.tsx | 4 +-- .../components/IAICanvasStagingArea.tsx | 4 +-- .../components/CurrentImageButtons.tsx | 15 ++------ .../gallery/components/HoverableImage.tsx | 8 ++--- .../ImageMetadataViewer.tsx | 21 +----------- .../lightbox/components/ReactPanZoomImage.tsx | 4 +-- .../ImageToImage/InitialImagePreview.tsx | 2 -- .../src/features/system/store/configSlice.ts | 1 - 11 files changed, 10 insertions(+), 93 deletions(-) delete mode 100644 invokeai/frontend/web/src/common/util/getUrl.ts diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index fa5c725a84..f202b66ca2 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -108,11 +108,7 @@ export type SDFeature = */ export type AppConfig = { /** - * Whether or not URLs should be transformed to use a different host - */ - shouldTransformUrls: boolean; - /** - * Whether or not we need to re-fetch images + * Whether or not we should update image urls when image loading errors */ shouldUpdateImageUrlsOnError: boolean; disabledTabs: InvokeTabName[]; diff --git a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx index 5a7f93747b..f31aebf596 100644 --- a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx +++ b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx @@ -4,7 +4,6 @@ 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 } from 'framer-motion'; import { ReactElement, SyntheticEvent } from 'react'; import { memo, useRef } from 'react'; @@ -45,7 +44,6 @@ const IAIDndImage = (props: IAIDndImageProps) => { minSize = 24, } = props; const dndId = useRef(uuidv4()); - const { getUrl } = useGetUrl(); const { isOver, setNodeRef: setDroppableRef, @@ -100,7 +98,7 @@ const IAIDndImage = (props: IAIDndImageProps) => { }} > { - if (OpenAPI.BASE && shouldTransformUrls) { - return [OpenAPI.BASE, url].join('/'); - } - - return url; -}; - -export const useGetUrl = () => { - const shouldTransformUrls = useAppSelector( - (state: RootState) => state.config.shouldTransformUrls - ); - - const getUrl = useCallback( - (url?: string) => { - if (OpenAPI.BASE && shouldTransformUrls) { - return [OpenAPI.BASE, url].join('/'); - } - - return url; - }, - [shouldTransformUrls] - ); - - return { - shouldTransformUrls, - getUrl, - }; -}; diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx index c99465cf40..ea04aa95c8 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx @@ -1,6 +1,5 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { useGetUrl } from 'common/util/getUrl'; import { canvasSelector } from 'features/canvas/store/canvasSelectors'; import { rgbaColorToString } from 'features/canvas/util/colorToString'; import { isEqual } from 'lodash-es'; @@ -33,7 +32,6 @@ const selector = createSelector( const IAICanvasObjectRenderer = () => { const { objects } = useAppSelector(selector); - const { getUrl } = useGetUrl(); if (!objects) return null; @@ -46,7 +44,7 @@ const IAICanvasObjectRenderer = () => { key={i} x={obj.x} y={obj.y} - url={getUrl(obj.image.image_url)} + url={obj.image.image_url} /> ); } else if (isCanvasBaseLine(obj)) { diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx index f03aeedb86..c33e0cacf5 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx @@ -1,6 +1,5 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { useGetUrl } from 'common/util/getUrl'; import { canvasSelector } from 'features/canvas/store/canvasSelectors'; import { GroupConfig } from 'konva/lib/Group'; import { isEqual } from 'lodash-es'; @@ -56,13 +55,12 @@ const IAICanvasStagingArea = (props: Props) => { width, height, } = useAppSelector(selector); - const { getUrl } = useGetUrl(); return ( {shouldShowStagingImage && currentStagingAreaImage && ( diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx index 91bd1a0425..6862b35fb8 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx @@ -47,7 +47,6 @@ import { import { gallerySelector } from '../store/gallerySelectors'; import { useCallback } from 'react'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; -import { useGetUrl } from 'common/util/getUrl'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { initialImageSelected } from 'features/parameters/store/actions'; @@ -153,8 +152,6 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled; const isFaceRestoreEnabled = useFeatureStatus('faceRestore').isFeatureEnabled; - const { getUrl, shouldTransformUrls } = useGetUrl(); - const { isOpen: isDeleteDialogOpen, onOpen: onDeleteDialogOpen, @@ -197,10 +194,6 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { return; } - if (shouldTransformUrls) { - return getUrl(image.image_url); - } - if (image.image_url.startsWith('http')) { return image.image_url; } @@ -229,7 +222,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { isClosable: true, }); }); - }, [toaster, shouldTransformUrls, getUrl, t, image]); + }, [toaster, t, image]); const handleClickUseAllParameters = useCallback(() => { recallAllParameters(image); @@ -461,11 +454,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { {t('parameters.copyImageToLink')} - + } size="sm" w="100%"> {t('parameters.downloadImage')} diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx index 4dad27d4e8..ef4ed5be1c 100644 --- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx @@ -21,7 +21,6 @@ import { gallerySelector } from 'features/gallery/store/gallerySelectors'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { useTranslation } from 'react-i18next'; import IAIIconButton from 'common/components/IAIIconButton'; -import { useGetUrl } from 'common/util/getUrl'; import { ExternalLinkIcon } from '@chakra-ui/icons'; import { IoArrowUndoCircleOutline } from 'react-icons/io5'; import { createSelector } from '@reduxjs/toolkit'; @@ -104,7 +103,6 @@ const HoverableImage = memo((props: HoverableImageProps) => { const { image, isSelected } = props; const { image_url, thumbnail_url, image_name } = image; - const { getUrl } = useGetUrl(); const [isHovered, setIsHovered] = useState(false); @@ -208,7 +206,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { }; const handleOpenInNewTab = () => { - window.open(getUrl(image.image_url), '_blank'); + window.open(image.image_url, '_blank'); }; return ( @@ -296,8 +294,6 @@ const HoverableImage = memo((props: HoverableImageProps) => { onMouseOver={handleMouseOver} onMouseOut={handleMouseOut} userSelect="none" - // draggable={true} - // onDragStart={handleDragStart} onClick={handleSelectImage} ref={ref} sx={{ @@ -317,7 +313,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { shouldUseSingleGalleryColumn ? 'contain' : galleryImageObjectFit } rounded="md" - src={getUrl(thumbnail_url || image_url)} + src={thumbnail_url || image_url} fallback={} sx={{ width: '100%', diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx index 1619680ec5..1a8801fa52 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx @@ -9,19 +9,6 @@ import { Tooltip, } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; -import { useGetUrl } from 'common/util/getUrl'; -import promptToString from 'common/util/promptToString'; -import { - setCfgScale, - setHeight, - setImg2imgStrength, - setNegativePrompt, - setPositivePrompt, - setScheduler, - setSeed, - setSteps, - setWidth, -} from 'features/parameters/store/generationSlice'; import { setShouldShowImageDetails } from 'features/ui/store/uiSlice'; import { memo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -30,7 +17,6 @@ import { FaCopy } from 'react-icons/fa'; import { IoArrowUndoCircleOutline } from 'react-icons/io5'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import { ImageDTO } from 'services/api'; -import { Scheduler } from 'app/constants'; import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; type MetadataItemProps = { @@ -146,7 +132,6 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { const metadata = image?.metadata; const { t } = useTranslation(); - const { getUrl } = useGetUrl(); const metadataJSON = JSON.stringify(image, null, 2); @@ -168,11 +153,7 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { > File: - + {image.image_name} diff --git a/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx b/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx index b1e822c309..7ec7d23371 100644 --- a/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx +++ b/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { TransformComponent, useTransformContext } from 'react-zoom-pan-pinch'; -import { useGetUrl } from 'common/util/getUrl'; import { ImageDTO } from 'services/api'; type ReactPanZoomProps = { @@ -23,7 +22,6 @@ export default function ReactPanZoomImage({ scaleY, }: ReactPanZoomProps) { const { centerView } = useTransformContext(); - const { getUrl } = useGetUrl(); return ( { const { initialImage } = useAppSelector(selector); const { shouldFetchImages } = useAppSelector(configSelector); - const { getUrl } = useGetUrl(); const dispatch = useAppDispatch(); const { t } = useTranslation(); const toaster = useAppToaster(); diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts index 5e0d2ca472..fb00a7a5d4 100644 --- a/invokeai/frontend/web/src/features/system/store/configSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts @@ -4,7 +4,6 @@ import { AppConfig, PartialAppConfig } from 'app/types/invokeai'; import { merge } from 'lodash-es'; export const initialConfigState: AppConfig = { - shouldTransformUrls: false, shouldUpdateImageUrlsOnError: false, disabledTabs: [], disabledFeatures: [], From b20045133025b5240416350296a50a0deab41cf0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Jun 2023 22:02:23 +1000 Subject: [PATCH 62/67] feat(ui): add nodesSelector --- invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 50c33e88b2..0f143b3a6a 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -17,9 +17,9 @@ import { InvocationTemplate, InvocationValue } from '../types/types'; import { parseSchema } from '../util/parseSchema'; import { log } from 'app/logging/useLogger'; import { forEach, size } from 'lodash-es'; -import { isAnyGraphBuilt } from './actions'; import { RgbaColor } from 'react-colorful'; import { imageUrlsReceived } from 'services/thunks/image'; +import { RootState } from 'app/store/store'; export type NodesState = { nodes: Node[]; @@ -130,3 +130,5 @@ export const { } = nodesSlice.actions; export default nodesSlice.reducer; + +export const nodesSelecter = (state: RootState) => state.nodes; From fa338ddb6a43f03dca6cd2de4e930ac30baa44fb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Jun 2023 22:10:21 +1000 Subject: [PATCH 63/67] feat(ui): add useGetIsImageInUse Checks if an image is currently being used eg in canvas, nodes, controlnet, init image. --- .../src/common/hooks/useGetIsImageInUse.ts | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 invokeai/frontend/web/src/common/hooks/useGetIsImageInUse.ts diff --git a/invokeai/frontend/web/src/common/hooks/useGetIsImageInUse.ts b/invokeai/frontend/web/src/common/hooks/useGetIsImageInUse.ts new file mode 100644 index 0000000000..ad14941d16 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useGetIsImageInUse.ts @@ -0,0 +1,54 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { canvasSelector } from 'features/canvas/store/canvasSelectors'; +import { controlNetSelector } from 'features/controlNet/store/controlNetSlice'; +import { nodesSelecter } from 'features/nodes/store/nodesSlice'; +import { generationSelector } from 'features/parameters/store/generationSelectors'; +import { some } from 'lodash-es'; + +const selectIsImageInUse = createSelector( + [ + generationSelector, + canvasSelector, + nodesSelecter, + controlNetSelector, + (state, image_name) => image_name, + ], + (generation, canvas, nodes, controlNet, image_name) => { + const isInitialImage = generation.initialImage?.image_name === image_name; + + const isCanvasImage = canvas.layerState.objects.some( + (obj) => obj.kind === 'image' && obj.image.image_name === image_name + ); + + const isNodesImage = nodes.nodes.some((node) => { + return some( + node.data.inputs, + (input) => + input.type === 'image' && input.value?.image_name === image_name + ); + }); + + const isControlNetImage = some( + controlNet.controlNets, + (c) => + c.controlImage?.image_name === image_name || + c.processedControlImage?.image_name === image_name + ); + + return { + isInitialImage, + isCanvasImage, + isNodesImage, + isControlNetImage, + }; + }, + defaultSelectorOptions +); + +export const useGetIsImageInUse = (image_name?: string) => { + const a = useAppSelector((state) => selectIsImageInUse(state, image_name)); + + return a; +}; From 3d249c4fa3bebafe744c4e9e52b3159bc3450d5a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Jun 2023 00:40:40 +1000 Subject: [PATCH 64/67] feat(ui): refactor image deletion Add `DeleteImageContext`: - provide a single function to delete an image - opens the modal or immediately deletes, if confirm is off --- .../frontend/web/src/app/components/App.tsx | 2 + .../web/src/app/components/InvokeAIUI.tsx | 16 ++- .../src/app/contexts/DeleteImageContext.tsx | 107 ++++++++++++++++++ .../components/CurrentImageButtons.tsx | 61 ++-------- .../components/CurrentImagePreview.tsx | 3 + .../{DeleteImageModal.tsx => DeleteModal.tsx} | 59 +++++++--- .../gallery/components/HoverableImage.tsx | 66 +++-------- .../ImageActionButtons/DeleteImageButton.tsx | 92 --------------- .../ImageToImage/InitialImagePreview.tsx | 1 + 9 files changed, 191 insertions(+), 216 deletions(-) create mode 100644 invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx rename invokeai/frontend/web/src/features/gallery/components/{DeleteImageModal.tsx => DeleteModal.tsx} (70%) delete mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageActionButtons/DeleteImageButton.tsx diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 21b3945490..67d0091261 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -21,6 +21,7 @@ import { ReactNode, memo, useCallback, useEffect, useState } from 'react'; import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants'; import GlobalHotkeys from './GlobalHotkeys'; import Toaster from './Toaster'; +import DeleteModal from 'features/gallery/components/DeleteModal'; const DEFAULT_CONFIG = {}; @@ -133,6 +134,7 @@ const App = ({ + diff --git a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx index c94f7624b2..0537d1de2a 100644 --- a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx +++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx @@ -17,6 +17,10 @@ import '../../i18n'; import { socketMiddleware } from 'services/events/middleware'; import { Middleware } from '@reduxjs/toolkit'; import ImageDndContext from './ImageDnd/ImageDndContext'; +import { + DeleteImageContext, + DeleteImageContextProvider, +} from 'app/contexts/DeleteImageContext'; const App = lazy(() => import('./App')); const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider')); @@ -71,11 +75,13 @@ const InvokeAIUI = ({ }> - + + + diff --git a/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx b/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx new file mode 100644 index 0000000000..1d129d4e00 --- /dev/null +++ b/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx @@ -0,0 +1,107 @@ +import { useDisclosure } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { requestedImageDeletion } from 'features/gallery/store/actions'; +import { systemSelector } from 'features/system/store/systemSelectors'; +import { PropsWithChildren, createContext, useCallback, useState } from 'react'; +import { ImageDTO } from 'services/api'; + +type DeleteImageContextValue = { + /** + * Whether the delete image dialog is open. + */ + isOpen: boolean; + /** + * Closes the delete image dialog. + */ + onClose: () => void; + /** + * Immediately deletes an image. + * + * You probably don't want to use this - use `onDelete` instead. + */ + onImmediatelyDelete: () => void; + /** + * Opens the delete image dialog and handles all deletion-related checks. + */ + onDelete: (image?: ImageDTO) => void; +}; + +export const DeleteImageContext = createContext({ + isOpen: false, + onClose: () => undefined, + onImmediatelyDelete: () => undefined, + onDelete: () => undefined, +}); + +const selector = createSelector( + [systemSelector], + (system) => { + const { isProcessing, isConnected, shouldConfirmOnDelete } = system; + + return { + canDeleteImage: isConnected && !isProcessing, + shouldConfirmOnDelete, + isProcessing, + isConnected, + }; + }, + defaultSelectorOptions +); + +type Props = PropsWithChildren; + +export const DeleteImageContextProvider = (props: Props) => { + const { canDeleteImage, shouldConfirmOnDelete } = useAppSelector(selector); + const [imageToDelete, setImageToDelete] = useState(); + const dispatch = useAppDispatch(); + const { isOpen, onOpen, onClose } = useDisclosure(); + + const closeAndClearImageToDelete = useCallback(() => { + setImageToDelete(undefined); + onClose(); + }, [onClose]); + + const onImmediatelyDelete = useCallback(() => { + if (canDeleteImage && imageToDelete) { + dispatch(requestedImageDeletion(imageToDelete)); + } + closeAndClearImageToDelete(); + }, [canDeleteImage, imageToDelete, closeAndClearImageToDelete, dispatch]); + + const handleDelete = useCallback( + (image: ImageDTO) => { + if (shouldConfirmOnDelete) { + onOpen(); + } else { + dispatch(requestedImageDeletion(image)); + } + }, + [shouldConfirmOnDelete, onOpen, dispatch] + ); + + const onDelete = useCallback( + (image?: ImageDTO) => { + if (!image) { + return; + } + setImageToDelete(image); + handleDelete(image); + }, + [handleDelete] + ); + + return ( + + {props.children} + + ); +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx index 6862b35fb8..333ad516ef 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx @@ -1,13 +1,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { isEqual } from 'lodash-es'; -import { - ButtonGroup, - Flex, - FlexProps, - Link, - useDisclosure, -} from '@chakra-ui/react'; +import { ButtonGroup, Flex, FlexProps, Link } from '@chakra-ui/react'; // import { runESRGAN, runFacetool } from 'app/socketio/actions'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIButton from 'common/components/IAIButton'; @@ -45,21 +39,18 @@ import { FaShareAlt, } from 'react-icons/fa'; import { gallerySelector } from '../store/gallerySelectors'; -import { useCallback } from 'react'; +import { useCallback, useContext } from 'react'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { initialImageSelected } from 'features/parameters/store/actions'; -import { - requestedImageDeletion, - sentImageToCanvas, - sentImageToImg2Img, -} from '../store/actions'; +import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions'; import FaceRestoreSettings from 'features/parameters/components/Parameters/FaceRestore/FaceRestoreSettings'; import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/UpscaleSettings'; -import DeleteImageButton from './ImageActionButtons/DeleteImageButton'; import { useAppToaster } from 'app/components/Toaster'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; +import { DeleteImageContext } from 'app/contexts/DeleteImageContext'; +import { DeleteImageButton } from './DeleteModal'; const currentImageButtonsSelector = createSelector( [ @@ -122,10 +113,6 @@ const currentImageButtonsSelector = createSelector( type CurrentImageButtonsProps = FlexProps; -/** - * Row of buttons for common actions: - * Use as init image, use all params, use seed, upscale, fix faces, details, delete. - */ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { const dispatch = useAppDispatch(); const { @@ -137,13 +124,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { facetoolStrength, shouldDisableToolbarButtons, shouldShowImageDetails, - // currentImage, isLightboxOpen, activeTabName, shouldHidePreview, image, - canDeleteImage, - shouldConfirmOnDelete, shouldShowProgressInViewer, } = useAppSelector(currentImageButtonsSelector); @@ -152,18 +136,14 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled; const isFaceRestoreEnabled = useFeatureStatus('faceRestore').isFeatureEnabled; - const { - isOpen: isDeleteDialogOpen, - onOpen: onDeleteDialogOpen, - onClose: onDeleteDialogClose, - } = useDisclosure(); - const toaster = useAppToaster(); const { t } = useTranslation(); const { recallBothPrompts, recallSeed, recallAllParameters } = useRecallParameters(); + const { onDelete } = useContext(DeleteImageContext); + // const handleCopyImage = useCallback(async () => { // if (!image?.url) { // return; @@ -262,6 +242,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { // selectedImage && dispatch(runESRGAN(selectedImage)); }, []); + const handleDelete = useCallback(() => { + onDelete(image); + }, [image, onDelete]); + useHotkeys( 'Shift+U', () => { @@ -363,31 +347,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { [image, shouldShowImageDetails, toaster] ); - const handleDelete = useCallback(() => { - if (canDeleteImage && image) { - dispatch(requestedImageDeletion(image)); - } - }, [image, canDeleteImage, dispatch]); - - const handleInitiateDelete = useCallback(() => { - if (shouldConfirmOnDelete) { - onDeleteDialogOpen(); - } else { - handleDelete(); - } - }, [shouldConfirmOnDelete, onDeleteDialogOpen, handleDelete]); - const handleClickProgressImagesToggle = useCallback(() => { dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer)); }, [dispatch, shouldShowProgressInViewer]); - useHotkeys('delete', handleInitiateDelete, [ - image, - shouldConfirmOnDelete, - isConnected, - isProcessing, - ]); - const handleLightBox = useCallback(() => { dispatch(setIsLightboxOpen(!isLightboxOpen)); }, [dispatch, isLightboxOpen]); @@ -596,7 +559,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { - + diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx index 5e210bf4b7..b8d9d6220a 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 { imageSelected } from '../store/gallerySlice'; import IAIDndImage from 'common/components/IAIDndImage'; import { ImageDTO } from 'services/api'; import { IAIImageFallback } from 'common/components/IAIImageFallback'; +import { useGetIsImageInUse } from 'common/hooks/useGetIsImageInUse'; export const imagesSelector = createSelector( [uiSelector, gallerySelector, systemSelector], @@ -54,6 +55,8 @@ const CurrentImagePreview = () => { const toaster = useAppToaster(); const dispatch = useAppDispatch(); + const isImageInUse = useGetIsImageInUse(image?.image_name); + console.log(isImageInUse); const handleError = useCallback(() => { dispatch(imageSelected()); if (shouldFetchImages) { diff --git a/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/gallery/components/DeleteModal.tsx similarity index 70% rename from invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx rename to invokeai/frontend/web/src/features/gallery/components/DeleteModal.tsx index 12038f4179..ca06aa7953 100644 --- a/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/DeleteModal.tsx @@ -9,16 +9,19 @@ import { Text, } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; +import { DeleteImageContext } from 'app/contexts/DeleteImageContext'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIButton from 'common/components/IAIButton'; +import IAIIconButton from 'common/components/IAIIconButton'; import IAISwitch from 'common/components/IAISwitch'; import { configSelector } from 'features/system/store/configSelectors'; import { systemSelector } from 'features/system/store/systemSelectors'; import { setShouldConfirmOnDelete } from 'features/system/store/systemSlice'; import { isEqual } from 'lodash-es'; -import { ChangeEvent, memo, useCallback, useRef } from 'react'; +import { ChangeEvent, memo, useCallback, useContext, useRef } from 'react'; import { useTranslation } from 'react-i18next'; +import { FaTrash } from 'react-icons/fa'; const selector = createSelector( [systemSelector, configSelector], @@ -34,22 +37,12 @@ const selector = createSelector( } ); -interface DeleteImageModalProps { - isOpen: boolean; - onClose: () => void; - handleDelete: () => void; -} - -const DeleteImageModal = ({ - isOpen, - onClose, - handleDelete, -}: DeleteImageModalProps) => { +const DeleteImageModal = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); + const { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin } = useAppSelector(selector); - const cancelRef = useRef(null); const handleChangeShouldConfirmOnDelete = useCallback( (e: ChangeEvent) => @@ -57,10 +50,10 @@ const DeleteImageModal = ({ [dispatch] ); - const handleClickDelete = useCallback(() => { - handleDelete(); - onClose(); - }, [handleDelete, onClose]); + const { isOpen, onClose, onImmediatelyDelete } = + useContext(DeleteImageContext); + + const cancelRef = useRef(null); return ( Cancel - + Delete @@ -107,3 +100,33 @@ const DeleteImageModal = ({ }; export default memo(DeleteImageModal); + +const deleteImageButtonsSelector = createSelector( + [systemSelector], + (system) => { + const { isProcessing, isConnected } = system; + + return isConnected && !isProcessing; + } +); + +type DeleteImageButtonProps = { + onClick: () => void; +}; + +export const DeleteImageButton = (props: DeleteImageButtonProps) => { + const { onClick } = props; + const { t } = useTranslation(); + const canDeleteImage = useAppSelector(deleteImageButtonsSelector); + + return ( + } + tooltip={`${t('gallery.deleteImage')} (Del)`} + aria-label={`${t('gallery.deleteImage')} (Del)`} + isDisabled={!canDeleteImage} + colorScheme="error" + /> + ); +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx index ef4ed5be1c..2b8f72101d 100644 --- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx @@ -1,17 +1,8 @@ -import { - Box, - Flex, - Icon, - Image, - MenuItem, - MenuList, - useDisclosure, -} from '@chakra-ui/react'; +import { Box, Flex, Icon, Image, MenuItem, MenuList } from '@chakra-ui/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { imageSelected } from 'features/gallery/store/gallerySlice'; -import { DragEvent, MouseEvent, memo, useCallback, useState } from 'react'; +import { memo, useCallback, useContext, useState } from 'react'; import { FaCheck, FaExpand, FaImage, FaShare, FaTrash } from 'react-icons/fa'; -import DeleteImageModal from './DeleteImageModal'; import { ContextMenu } from 'chakra-ui-contextmenu'; import { resizeAndScaleCanvas, @@ -31,14 +22,11 @@ import { isEqual } from 'lodash-es'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { initialImageSelected } from 'features/parameters/store/actions'; -import { - requestedImageDeletion, - sentImageToCanvas, - sentImageToImg2Img, -} from '../store/actions'; +import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions'; import { useAppToaster } from 'app/components/Toaster'; import { ImageDTO } from 'services/api'; import { useDraggable } from '@dnd-kit/core'; +import { DeleteImageContext } from 'app/contexts/DeleteImageContext'; export const selector = createSelector( [gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector], @@ -92,27 +80,22 @@ const HoverableImage = memo((props: HoverableImageProps) => { galleryImageMinimumWidth, canDeleteImage, shouldUseSingleGalleryColumn, - shouldConfirmOnDelete, } = useAppSelector(selector); - const { - isOpen: isDeleteDialogOpen, - onOpen: onDeleteDialogOpen, - onClose: onDeleteDialogClose, - } = useDisclosure(); - const { image, isSelected } = props; const { image_url, thumbnail_url, image_name } = image; const [isHovered, setIsHovered] = useState(false); - const toaster = useAppToaster(); const { t } = useTranslation(); - const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled; const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled; + const { onDelete } = useContext(DeleteImageContext); + const handleDelete = useCallback(() => { + onDelete(image); + }, [image, onDelete]); const { recallBothPrompts, recallSeed, recallAllParameters } = useRecallParameters(); @@ -126,26 +109,6 @@ const HoverableImage = memo((props: HoverableImageProps) => { const handleMouseOver = () => setIsHovered(true); const handleMouseOut = () => setIsHovered(false); - // Immediately deletes an image - const handleDelete = useCallback(() => { - if (canDeleteImage && image) { - dispatch(requestedImageDeletion(image)); - } - }, [dispatch, image, canDeleteImage]); - - // Opens the alert dialog to check if user is sure they want to delete - const handleInitiateDelete = useCallback( - (e: MouseEvent) => { - e.stopPropagation(); - if (shouldConfirmOnDelete) { - onDeleteDialogOpen(); - } else { - handleDelete(); - } - }, - [handleDelete, onDeleteDialogOpen, shouldConfirmOnDelete] - ); - const handleSelectImage = useCallback(() => { dispatch(imageSelected(image)); }, [image, dispatch]); @@ -281,7 +244,11 @@ const HoverableImage = memo((props: HoverableImageProps) => { {t('parameters.sendToUnifiedCanvas')} )} - } onClickCapture={onDeleteDialogOpen}> + } + onClickCapture={handleDelete} + > {t('gallery.deleteImage')} @@ -357,7 +324,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { }} > } size="xs" @@ -369,11 +336,6 @@ const HoverableImage = memo((props: HoverableImageProps) => { )} - ); }, memoEqualityCheck); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageActionButtons/DeleteImageButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageActionButtons/DeleteImageButton.tsx deleted file mode 100644 index 4b0f6e60dd..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/ImageActionButtons/DeleteImageButton.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; - -import { useDisclosure } from '@chakra-ui/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIIconButton from 'common/components/IAIIconButton'; -import { systemSelector } from 'features/system/store/systemSelectors'; - -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { FaTrash } from 'react-icons/fa'; -import { memo, useCallback } from 'react'; -import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import DeleteImageModal from '../DeleteImageModal'; -import { requestedImageDeletion } from 'features/gallery/store/actions'; -import { ImageDTO } from 'services/api'; - -const selector = createSelector( - [systemSelector], - (system) => { - const { isProcessing, isConnected, shouldConfirmOnDelete } = system; - - return { - canDeleteImage: isConnected && !isProcessing, - shouldConfirmOnDelete, - isProcessing, - isConnected, - }; - }, - defaultSelectorOptions -); - -type DeleteImageButtonProps = { - image: ImageDTO | undefined; -}; - -const DeleteImageButton = (props: DeleteImageButtonProps) => { - const { image } = props; - const dispatch = useAppDispatch(); - const { isProcessing, isConnected, canDeleteImage, shouldConfirmOnDelete } = - useAppSelector(selector); - - const { - isOpen: isDeleteDialogOpen, - onOpen: onDeleteDialogOpen, - onClose: onDeleteDialogClose, - } = useDisclosure(); - - const { t } = useTranslation(); - - const handleDelete = useCallback(() => { - if (canDeleteImage && image) { - dispatch(requestedImageDeletion(image)); - } - }, [image, canDeleteImage, dispatch]); - - const handleInitiateDelete = useCallback(() => { - if (shouldConfirmOnDelete) { - onDeleteDialogOpen(); - } else { - handleDelete(); - } - }, [shouldConfirmOnDelete, onDeleteDialogOpen, handleDelete]); - - useHotkeys('delete', handleInitiateDelete, [ - image, - shouldConfirmOnDelete, - isConnected, - isProcessing, - ]); - - return ( - <> - } - tooltip={`${t('gallery.deleteImage')} (Del)`} - aria-label={`${t('gallery.deleteImage')} (Del)`} - isDisabled={!image || !isConnected} - colorScheme="error" - /> - {image && ( - - )} - - ); -}; - -export default memo(DeleteImageButton); 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 c006215256..73efb69728 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 @@ -14,6 +14,7 @@ import { useAppToaster } from 'app/components/Toaster'; import IAIDndImage from 'common/components/IAIDndImage'; import { ImageDTO } from 'services/api'; import { IAIImageFallback } from 'common/components/IAIImageFallback'; +import { useGetIsImageInUse } from 'common/hooks/useGetIsImageInUse'; const selector = createSelector( [generationSelector], From bf116927e10a3dd9432e3c273027cc0126b2b518 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Jun 2023 01:30:06 +1000 Subject: [PATCH 65/67] feat(ui): clear features if image used by them is deleted This handles the case when an image is deleted but is still in use in as eg an init image on canvas, or a control image. If we just delete the image, canvas/controlnet/etc may break (the image would just fail to load). When an image is deleted, the app checks to see if it is in use in: - Image to Image - ControlNet - Unified Canvas - Node Editor The delete dialog will always open if the image is in use anywhere, and the user is advised that deleting the image will reset the feature(s). Even if the user has ticked the box to not confirm on delete, the dialog will still show if the image is in use somewhere. --- .../frontend/web/src/app/components/App.tsx | 4 +- .../src/app/contexts/DeleteImageContext.tsx | 71 +++++++++++++++---- ...useGetIsImageInUse.ts => useImageUsage.ts} | 24 +++++-- .../controlNet/store/controlNetSlice.ts | 4 ++ .../components/CurrentImageButtons.tsx | 2 +- .../components/CurrentImagePreview.tsx | 3 - .../{DeleteModal.tsx => DeleteImageModal.tsx} | 68 +++++++++++++----- .../src/features/nodes/store/nodesSlice.ts | 4 ++ .../ImageToImage/InitialImagePreview.tsx | 1 - 9 files changed, 135 insertions(+), 46 deletions(-) rename invokeai/frontend/web/src/common/hooks/{useGetIsImageInUse.ts => useImageUsage.ts} (73%) rename invokeai/frontend/web/src/features/gallery/components/{DeleteModal.tsx => DeleteImageModal.tsx} (68%) diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 67d0091261..bb2f140716 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -21,7 +21,7 @@ import { ReactNode, memo, useCallback, useEffect, useState } from 'react'; import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants'; import GlobalHotkeys from './GlobalHotkeys'; import Toaster from './Toaster'; -import DeleteModal from 'features/gallery/components/DeleteModal'; +import DeleteImageModal from 'features/gallery/components/DeleteImageModal'; const DEFAULT_CONFIG = {}; @@ -134,7 +134,7 @@ const App = ({ - + diff --git a/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx b/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx index 1d129d4e00..2f2bc4625b 100644 --- a/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx +++ b/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx @@ -7,6 +7,12 @@ import { systemSelector } from 'features/system/store/systemSelectors'; import { PropsWithChildren, createContext, useCallback, useState } from 'react'; import { ImageDTO } from 'services/api'; +import { useImageUsage } from 'common/hooks/useImageUsage'; +import { resetCanvas } from 'features/canvas/store/canvasSlice'; +import { controlNetReset } from 'features/controlNet/store/controlNetSlice'; +import { clearInitialImage } from 'features/parameters/store/generationSlice'; +import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; + type DeleteImageContextValue = { /** * Whether the delete image dialog is open. @@ -16,16 +22,20 @@ type DeleteImageContextValue = { * Closes the delete image dialog. */ onClose: () => void; + /** + * Opens the delete image dialog and handles all deletion-related checks. + */ + onDelete: (image?: ImageDTO) => void; + /** + * The image pending deletion + */ + image?: ImageDTO; /** * Immediately deletes an image. * * You probably don't want to use this - use `onDelete` instead. */ onImmediatelyDelete: () => void; - /** - * Opens the delete image dialog and handles all deletion-related checks. - */ - onDelete: (image?: ImageDTO) => void; }; export const DeleteImageContext = createContext({ @@ -43,8 +53,6 @@ const selector = createSelector( return { canDeleteImage: isConnected && !isProcessing, shouldConfirmOnDelete, - isProcessing, - isConnected, }; }, defaultSelectorOptions @@ -57,6 +65,35 @@ export const DeleteImageContextProvider = (props: Props) => { const [imageToDelete, setImageToDelete] = useState(); const dispatch = useAppDispatch(); const { isOpen, onOpen, onClose } = useDisclosure(); + const imageUsage = useImageUsage(imageToDelete?.image_name); + + const handleActualDeletion = useCallback( + (image: ImageDTO) => { + dispatch(requestedImageDeletion(image)); + + if (imageUsage.isCanvasImage) { + dispatch(resetCanvas()); + } + + if (imageUsage.isControlNetImage) { + dispatch(controlNetReset()); + } + + if (imageUsage.isInitialImage) { + dispatch(clearInitialImage()); + } + + if (imageUsage.isControlNetImage) { + dispatch(nodeEditorReset()); + } + }, + [ + dispatch, + imageUsage.isCanvasImage, + imageUsage.isControlNetImage, + imageUsage.isInitialImage, + ] + ); const closeAndClearImageToDelete = useCallback(() => { setImageToDelete(undefined); @@ -65,20 +102,25 @@ export const DeleteImageContextProvider = (props: Props) => { const onImmediatelyDelete = useCallback(() => { if (canDeleteImage && imageToDelete) { - dispatch(requestedImageDeletion(imageToDelete)); + handleActualDeletion(imageToDelete); } closeAndClearImageToDelete(); - }, [canDeleteImage, imageToDelete, closeAndClearImageToDelete, dispatch]); + }, [ + canDeleteImage, + imageToDelete, + closeAndClearImageToDelete, + handleActualDeletion, + ]); - const handleDelete = useCallback( + const handleGatedDeletion = useCallback( (image: ImageDTO) => { - if (shouldConfirmOnDelete) { + if (shouldConfirmOnDelete || imageUsage) { onOpen(); } else { - dispatch(requestedImageDeletion(image)); + handleActualDeletion(image); } }, - [shouldConfirmOnDelete, onOpen, dispatch] + [shouldConfirmOnDelete, imageUsage, onOpen, handleActualDeletion] ); const onDelete = useCallback( @@ -87,15 +129,16 @@ export const DeleteImageContextProvider = (props: Props) => { return; } setImageToDelete(image); - handleDelete(image); + handleGatedDeletion(image); }, - [handleDelete] + [handleGatedDeletion] ); return ( image_name, + (state: RootState, image_name?: string) => image_name, ], (generation, canvas, nodes, controlNet, image_name) => { const isInitialImage = generation.initialImage?.image_name === image_name; @@ -37,18 +45,22 @@ const selectIsImageInUse = createSelector( c.processedControlImage?.image_name === image_name ); - return { + const imageUsage: ImageUsage = { isInitialImage, isCanvasImage, isNodesImage, isControlNetImage, }; + + return imageUsage; }, defaultSelectorOptions ); -export const useGetIsImageInUse = (image_name?: string) => { - const a = useAppSelector((state) => selectIsImageInUse(state, image_name)); +export const useImageUsage = (image_name?: string) => { + const imageUsage = useAppSelector((state) => + selectImageUsage(state, image_name) + ); - return a; + return imageUsage; }; diff --git a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts index da76ce4a8a..92d6c302e9 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts @@ -187,6 +187,9 @@ export const controlNetSlice = createSlice({ processorType ].default as RequiredControlNetProcessorNode; }, + controlNetReset: () => { + return { ...initialControlNetState }; + }, }, extraReducers: (builder) => { builder.addCase(controlNetImageProcessed, (state, action) => { @@ -243,6 +246,7 @@ export const { controlNetEndStepPctChanged, controlNetProcessorParamsChanged, controlNetProcessorTypeChanged, + controlNetReset, } = controlNetSlice.actions; export default controlNetSlice.reducer; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx index 333ad516ef..a5eaeb4c71 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx @@ -50,7 +50,7 @@ import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/U import { useAppToaster } from 'app/components/Toaster'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { DeleteImageContext } from 'app/contexts/DeleteImageContext'; -import { DeleteImageButton } from './DeleteModal'; +import { DeleteImageButton } from './DeleteImageModal'; const currentImageButtonsSelector = createSelector( [ diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx index b8d9d6220a..5e210bf4b7 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx @@ -15,7 +15,6 @@ import { imageSelected } from '../store/gallerySlice'; import IAIDndImage from 'common/components/IAIDndImage'; import { ImageDTO } from 'services/api'; import { IAIImageFallback } from 'common/components/IAIImageFallback'; -import { useGetIsImageInUse } from 'common/hooks/useGetIsImageInUse'; export const imagesSelector = createSelector( [uiSelector, gallerySelector, systemSelector], @@ -55,8 +54,6 @@ const CurrentImagePreview = () => { const toaster = useAppToaster(); const dispatch = useAppDispatch(); - const isImageInUse = useGetIsImageInUse(image?.image_name); - console.log(isImageInUse); const handleError = useCallback(() => { dispatch(imageSelected()); if (shouldFetchImages) { diff --git a/invokeai/frontend/web/src/features/gallery/components/DeleteModal.tsx b/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx similarity index 68% rename from invokeai/frontend/web/src/features/gallery/components/DeleteModal.tsx rename to invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx index ca06aa7953..335944df43 100644 --- a/invokeai/frontend/web/src/features/gallery/components/DeleteModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx @@ -5,19 +5,24 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, + Divider, Flex, + ListItem, Text, + UnorderedList, } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { DeleteImageContext } from 'app/contexts/DeleteImageContext'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAIButton from 'common/components/IAIButton'; import IAIIconButton from 'common/components/IAIIconButton'; import IAISwitch from 'common/components/IAISwitch'; +import { ImageUsage, useImageUsage } from 'common/hooks/useImageUsage'; import { configSelector } from 'features/system/store/configSelectors'; import { systemSelector } from 'features/system/store/systemSelectors'; import { setShouldConfirmOnDelete } from 'features/system/store/systemSlice'; -import { isEqual } from 'lodash-es'; +import { some } from 'lodash-es'; import { ChangeEvent, memo, useCallback, useContext, useRef } from 'react'; import { useTranslation } from 'react-i18next'; @@ -28,31 +33,56 @@ const selector = createSelector( (system, config) => { const { shouldConfirmOnDelete } = system; const { canRestoreDeletedImagesFromBin } = config; - return { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin }; + + return { + shouldConfirmOnDelete, + canRestoreDeletedImagesFromBin, + }; }, - { - memoizeOptions: { - resultEqualityCheck: isEqual, - }, - } + defaultSelectorOptions ); +const ImageInUseMessage = (props: { imageUsage: ImageUsage }) => { + const { imageUsage } = props; + + if (!some(imageUsage)) { + return null; + } + + return ( + <> + This image is currently in use in the following features: + + {imageUsage.isInitialImage && Image to Image} + {imageUsage.isCanvasImage && Unified Canvas} + {imageUsage.isControlNetImage && ControlNet} + {imageUsage.isNodesImage && Node Editor} + + + If you delete this image, those features will immediately be reset. + + + ); +}; + const DeleteImageModal = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); + const { isOpen, onClose, onImmediatelyDelete, image } = + useContext(DeleteImageContext); + const { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin } = useAppSelector(selector); + const imageUsage = useImageUsage(image?.image_name); + const handleChangeShouldConfirmOnDelete = useCallback( (e: ChangeEvent) => dispatch(setShouldConfirmOnDelete(!e.target.checked)), [dispatch] ); - const { isOpen, onClose, onImmediatelyDelete } = - useContext(DeleteImageContext); - const cancelRef = useRef(null); return ( @@ -69,15 +99,15 @@ const DeleteImageModal = () => { - - - {t('common.areYouSure')} - - {canRestoreDeletedImagesFromBin - ? t('gallery.deleteImageBin') - : t('gallery.deleteImagePermanent')} - - + + + + + {canRestoreDeletedImagesFromBin + ? t('gallery.deleteImageBin') + : t('gallery.deleteImagePermanent')} + + {t('common.areYouSure')} { + return { ...initialNodesState }; + }, }, extraReducers(builder) { builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => { @@ -127,6 +130,7 @@ export const { connectionEnded, shouldShowGraphOverlayChanged, parsedOpenAPISchema, + nodeEditorReset, } = nodesSlice.actions; export default nodesSlice.reducer; 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 73efb69728..c006215256 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 @@ -14,7 +14,6 @@ import { useAppToaster } from 'app/components/Toaster'; import IAIDndImage from 'common/components/IAIDndImage'; import { ImageDTO } from 'services/api'; import { IAIImageFallback } from 'common/components/IAIImageFallback'; -import { useGetIsImageInUse } from 'common/hooks/useGetIsImageInUse'; const selector = createSelector( [generationSelector], From bbb2a08e8f81a49a90475b5f96ae6658518fe0eb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Jun 2023 20:01:27 +1000 Subject: [PATCH 66/67] feat(ui): fix bugs with image deletion - `imageUsage` object was always stale due to react component lifecycle, fixed this - cleaned up the deletion listener and context --- .../src/app/contexts/DeleteImageContext.tsx | 143 ++++++++++++------ .../listeners/imageDeleted.ts | 28 +++- .../web/src/common/hooks/useImageUsage.ts | 66 -------- .../gallery/components/DeleteImageModal.tsx | 16 +- .../web/src/features/gallery/store/actions.ts | 13 +- 5 files changed, 140 insertions(+), 126 deletions(-) delete mode 100644 invokeai/frontend/web/src/common/hooks/useImageUsage.ts diff --git a/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx b/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx index 2f2bc4625b..8263b48114 100644 --- a/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx +++ b/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx @@ -4,14 +4,69 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { requestedImageDeletion } from 'features/gallery/store/actions'; import { systemSelector } from 'features/system/store/systemSelectors'; -import { PropsWithChildren, createContext, useCallback, useState } from 'react'; +import { + PropsWithChildren, + createContext, + useCallback, + useEffect, + useState, +} from 'react'; import { ImageDTO } from 'services/api'; +import { RootState } from 'app/store/store'; +import { canvasSelector } from 'features/canvas/store/canvasSelectors'; +import { controlNetSelector } from 'features/controlNet/store/controlNetSlice'; +import { nodesSelecter } from 'features/nodes/store/nodesSlice'; +import { generationSelector } from 'features/parameters/store/generationSelectors'; +import { some } from 'lodash-es'; -import { useImageUsage } from 'common/hooks/useImageUsage'; -import { resetCanvas } from 'features/canvas/store/canvasSlice'; -import { controlNetReset } from 'features/controlNet/store/controlNetSlice'; -import { clearInitialImage } from 'features/parameters/store/generationSlice'; -import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; +export type ImageUsage = { + isInitialImage: boolean; + isCanvasImage: boolean; + isNodesImage: boolean; + isControlNetImage: boolean; +}; + +export const selectImageUsage = createSelector( + [ + generationSelector, + canvasSelector, + nodesSelecter, + controlNetSelector, + (state: RootState, image_name?: string) => image_name, + ], + (generation, canvas, nodes, controlNet, image_name) => { + const isInitialImage = generation.initialImage?.image_name === image_name; + + const isCanvasImage = canvas.layerState.objects.some( + (obj) => obj.kind === 'image' && obj.image.image_name === image_name + ); + + const isNodesImage = nodes.nodes.some((node) => { + return some( + node.data.inputs, + (input) => + input.type === 'image' && input.value?.image_name === image_name + ); + }); + + const isControlNetImage = some( + controlNet.controlNets, + (c) => + c.controlImage?.image_name === image_name || + c.processedControlImage?.image_name === image_name + ); + + const imageUsage: ImageUsage = { + isInitialImage, + isCanvasImage, + isNodesImage, + isControlNetImage, + }; + + return imageUsage; + }, + defaultSelectorOptions +); type DeleteImageContextValue = { /** @@ -30,6 +85,10 @@ type DeleteImageContextValue = { * The image pending deletion */ image?: ImageDTO; + /** + * The features in which this image is used + */ + imageUsage?: ImageUsage; /** * Immediately deletes an image. * @@ -65,41 +124,28 @@ export const DeleteImageContextProvider = (props: Props) => { const [imageToDelete, setImageToDelete] = useState(); const dispatch = useAppDispatch(); const { isOpen, onOpen, onClose } = useDisclosure(); - const imageUsage = useImageUsage(imageToDelete?.image_name); - const handleActualDeletion = useCallback( - (image: ImageDTO) => { - dispatch(requestedImageDeletion(image)); - - if (imageUsage.isCanvasImage) { - dispatch(resetCanvas()); - } - - if (imageUsage.isControlNetImage) { - dispatch(controlNetReset()); - } - - if (imageUsage.isInitialImage) { - dispatch(clearInitialImage()); - } - - if (imageUsage.isControlNetImage) { - dispatch(nodeEditorReset()); - } - }, - [ - dispatch, - imageUsage.isCanvasImage, - imageUsage.isControlNetImage, - imageUsage.isInitialImage, - ] + // Check where the image to be deleted is used (eg init image, controlnet, etc.) + const imageUsage = useAppSelector((state) => + selectImageUsage(state, imageToDelete?.image_name) ); + // Clean up after deleting or dismissing the modal const closeAndClearImageToDelete = useCallback(() => { setImageToDelete(undefined); onClose(); }, [onClose]); + // Dispatch the actual deletion action, to be handled by listener middleware + const handleActualDeletion = useCallback( + (image: ImageDTO) => { + dispatch(requestedImageDeletion({ image, imageUsage })); + closeAndClearImageToDelete(); + }, + [closeAndClearImageToDelete, dispatch, imageUsage] + ); + + // This is intended to be called by the delete button in the dialog const onImmediatelyDelete = useCallback(() => { if (canDeleteImage && imageToDelete) { handleActualDeletion(imageToDelete); @@ -114,25 +160,31 @@ export const DeleteImageContextProvider = (props: Props) => { const handleGatedDeletion = useCallback( (image: ImageDTO) => { - if (shouldConfirmOnDelete || imageUsage) { + if (shouldConfirmOnDelete || some(imageUsage)) { + // If we should confirm on delete, or if the image is in use, open the dialog onOpen(); } else { handleActualDeletion(image); } }, - [shouldConfirmOnDelete, imageUsage, onOpen, handleActualDeletion] + [imageUsage, shouldConfirmOnDelete, onOpen, handleActualDeletion] ); - const onDelete = useCallback( - (image?: ImageDTO) => { - if (!image) { - return; - } - setImageToDelete(image); - handleGatedDeletion(image); - }, - [handleGatedDeletion] - ); + // Consumers of the context call this to delete an image + const onDelete = useCallback((image?: ImageDTO) => { + if (!image) { + return; + } + // Set the image to delete, then let the effect call the actual deletion + setImageToDelete(image); + }, []); + + useEffect(() => { + // We need to use an effect here to trigger the image usage selector, else we get a stale value + if (imageToDelete) { + handleGatedDeletion(imageToDelete); + } + }, [handleGatedDeletion, imageToDelete]); return ( { onClose: closeAndClearImageToDelete, onDelete, onImmediatelyDelete, + imageUsage, }} > {props.children} diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts index b527b5d00b..f4376a4959 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts @@ -9,6 +9,10 @@ import { selectImagesEntities, selectImagesIds, } from 'features/gallery/store/imagesSlice'; +import { resetCanvas } from 'features/canvas/store/canvasSlice'; +import { controlNetReset } from 'features/controlNet/store/controlNetSlice'; +import { clearInitialImage } from 'features/parameters/store/generationSlice'; +import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' }); @@ -19,11 +23,7 @@ export const addRequestedImageDeletionListener = () => { startAppListening({ actionCreator: requestedImageDeletion, effect: (action, { dispatch, getState }) => { - const image = action.payload; - if (!image) { - moduleLog.warn('No image provided'); - return; - } + const { image, imageUsage } = action.payload; const { image_name, image_origin } = image; @@ -57,6 +57,24 @@ export const addRequestedImageDeletionListener = () => { } } + // We need to reset the features where the image is in use - none of these work if their image(s) don't exist + + if (imageUsage.isCanvasImage) { + dispatch(resetCanvas()); + } + + if (imageUsage.isControlNetImage) { + dispatch(controlNetReset()); + } + + if (imageUsage.isInitialImage) { + dispatch(clearInitialImage()); + } + + if (imageUsage.isNodesImage) { + dispatch(nodeEditorReset()); + } + // Preemptively remove from gallery dispatch(imageRemoved(image_name)); diff --git a/invokeai/frontend/web/src/common/hooks/useImageUsage.ts b/invokeai/frontend/web/src/common/hooks/useImageUsage.ts deleted file mode 100644 index cf762f7880..0000000000 --- a/invokeai/frontend/web/src/common/hooks/useImageUsage.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { RootState } from 'app/store/store'; -import { useAppSelector } from 'app/store/storeHooks'; -import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import { canvasSelector } from 'features/canvas/store/canvasSelectors'; -import { controlNetSelector } from 'features/controlNet/store/controlNetSlice'; -import { nodesSelecter } from 'features/nodes/store/nodesSlice'; -import { generationSelector } from 'features/parameters/store/generationSelectors'; -import { some } from 'lodash-es'; - -export type ImageUsage = { - isInitialImage: boolean; - isCanvasImage: boolean; - isNodesImage: boolean; - isControlNetImage: boolean; -}; - -const selectImageUsage = createSelector( - [ - generationSelector, - canvasSelector, - nodesSelecter, - controlNetSelector, - (state: RootState, image_name?: string) => image_name, - ], - (generation, canvas, nodes, controlNet, image_name) => { - const isInitialImage = generation.initialImage?.image_name === image_name; - - const isCanvasImage = canvas.layerState.objects.some( - (obj) => obj.kind === 'image' && obj.image.image_name === image_name - ); - - const isNodesImage = nodes.nodes.some((node) => { - return some( - node.data.inputs, - (input) => - input.type === 'image' && input.value?.image_name === image_name - ); - }); - - const isControlNetImage = some( - controlNet.controlNets, - (c) => - c.controlImage?.image_name === image_name || - c.processedControlImage?.image_name === image_name - ); - - const imageUsage: ImageUsage = { - isInitialImage, - isCanvasImage, - isNodesImage, - isControlNetImage, - }; - - return imageUsage; - }, - defaultSelectorOptions -); - -export const useImageUsage = (image_name?: string) => { - const imageUsage = useAppSelector((state) => - selectImageUsage(state, image_name) - ); - - return imageUsage; -}; diff --git a/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx index 335944df43..0ce7bb3666 100644 --- a/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx @@ -12,13 +12,15 @@ import { UnorderedList, } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; -import { DeleteImageContext } from 'app/contexts/DeleteImageContext'; +import { + DeleteImageContext, + ImageUsage, +} from 'app/contexts/DeleteImageContext'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAIButton from 'common/components/IAIButton'; import IAIIconButton from 'common/components/IAIIconButton'; import IAISwitch from 'common/components/IAISwitch'; -import { ImageUsage, useImageUsage } from 'common/hooks/useImageUsage'; import { configSelector } from 'features/system/store/configSelectors'; import { systemSelector } from 'features/system/store/systemSelectors'; import { setShouldConfirmOnDelete } from 'features/system/store/systemSlice'; @@ -42,9 +44,13 @@ const selector = createSelector( defaultSelectorOptions ); -const ImageInUseMessage = (props: { imageUsage: ImageUsage }) => { +const ImageInUseMessage = (props: { imageUsage?: ImageUsage }) => { const { imageUsage } = props; + if (!imageUsage) { + return null; + } + if (!some(imageUsage)) { return null; } @@ -69,14 +75,12 @@ const DeleteImageModal = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const { isOpen, onClose, onImmediatelyDelete, image } = + const { isOpen, onClose, onImmediatelyDelete, image, imageUsage } = useContext(DeleteImageContext); const { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin } = useAppSelector(selector); - const imageUsage = useImageUsage(image?.image_name); - const handleChangeShouldConfirmOnDelete = useCallback( (e: ChangeEvent) => dispatch(setShouldConfirmOnDelete(!e.target.checked)), diff --git a/invokeai/frontend/web/src/features/gallery/store/actions.ts b/invokeai/frontend/web/src/features/gallery/store/actions.ts index 7c00201da9..8b2beb9c13 100644 --- a/invokeai/frontend/web/src/features/gallery/store/actions.ts +++ b/invokeai/frontend/web/src/features/gallery/store/actions.ts @@ -1,10 +1,15 @@ import { createAction } from '@reduxjs/toolkit'; -import { ImageNameAndOrigin } from 'features/parameters/store/actions'; +import { ImageUsage } from 'app/contexts/DeleteImageContext'; import { ImageDTO } from 'services/api'; -export const requestedImageDeletion = createAction< - ImageDTO | ImageNameAndOrigin | undefined ->('gallery/requestedImageDeletion'); +export type RequestedImageDeletionArg = { + image: ImageDTO; + imageUsage: ImageUsage; +}; + +export const requestedImageDeletion = createAction( + 'gallery/requestedImageDeletion' +); export const sentImageToCanvas = createAction('gallery/sentImageToCanvas'); From 454683e6eb058b4f83ee2045bb86464a6558645f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Jun 2023 00:23:51 +1000 Subject: [PATCH 67/67] feat(ui): update image urls on connect (#3507) * feat(ui): update image urls on connect Add `updateImageUrlsOnConnect` RTK listener: - requests URLs for *every* image the app knows about, on connect: gallery, selectedImage, initialImage, canvas images, nodes images, controlnet images - only fires when `shouldUpdateImagesOnConnect` config is enabled * remove prop --------- Co-authored-by: Mary Hipp --- .../middleware/listenerMiddleware/index.ts | 4 + .../listeners/updateImageUrlsOnConnect.ts | 93 +++++++++++++++++++ .../frontend/web/src/app/types/invokeai.ts | 2 +- .../src/features/system/store/configSlice.ts | 2 +- 4 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateImageUrlsOnConnect.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 a9349dc863..8c073e81d6 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -72,6 +72,7 @@ import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingA import { addImageCategoriesChangedListener } from './listeners/imageCategoriesChanged'; import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed'; import { addControlNetAutoProcessListener } from './listeners/controlNetAutoProcess'; +import { addUpdateImageUrlsOnConnectListener } from './listeners/updateImageUrlsOnConnect'; export const listenerMiddleware = createListenerMiddleware(); @@ -179,3 +180,6 @@ addImageCategoriesChangedListener(); // ControlNet addControlNetImageProcessedListener(); addControlNetAutoProcessListener(); + +// Update image URLs on connect +addUpdateImageUrlsOnConnectListener(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateImageUrlsOnConnect.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateImageUrlsOnConnect.ts new file mode 100644 index 0000000000..d02ffbe931 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateImageUrlsOnConnect.ts @@ -0,0 +1,93 @@ +import { socketConnected } from 'services/events/actions'; +import { startAppListening } from '..'; +import { createSelector } from '@reduxjs/toolkit'; +import { generationSelector } from 'features/parameters/store/generationSelectors'; +import { canvasSelector } from 'features/canvas/store/canvasSelectors'; +import { nodesSelecter } from 'features/nodes/store/nodesSlice'; +import { controlNetSelector } from 'features/controlNet/store/controlNetSlice'; +import { ImageDTO } from 'services/api'; +import { forEach, uniqBy } from 'lodash-es'; +import { imageUrlsReceived } from 'services/thunks/image'; +import { log } from 'app/logging/useLogger'; +import { selectImagesEntities } from 'features/gallery/store/imagesSlice'; + +const moduleLog = log.child({ namespace: 'images' }); + +const selectAllUsedImages = createSelector( + [ + generationSelector, + canvasSelector, + nodesSelecter, + controlNetSelector, + selectImagesEntities, + ], + (generation, canvas, nodes, controlNet, imageEntities) => { + const allUsedImages: ImageDTO[] = []; + + if (generation.initialImage) { + allUsedImages.push(generation.initialImage); + } + + canvas.layerState.objects.forEach((obj) => { + if (obj.kind === 'image') { + allUsedImages.push(obj.image); + } + }); + + nodes.nodes.forEach((node) => { + forEach(node.data.inputs, (input) => { + if (input.type === 'image' && input.value) { + allUsedImages.push(input.value); + } + }); + }); + + forEach(controlNet.controlNets, (c) => { + if (c.controlImage) { + allUsedImages.push(c.controlImage); + } + if (c.processedControlImage) { + allUsedImages.push(c.processedControlImage); + } + }); + + forEach(imageEntities, (image) => { + if (image) { + allUsedImages.push(image); + } + }); + + const uniqueImages = uniqBy(allUsedImages, 'image_name'); + + return uniqueImages; + } +); + +export const addUpdateImageUrlsOnConnectListener = () => { + startAppListening({ + actionCreator: socketConnected, + effect: async (action, { dispatch, getState, take }) => { + const state = getState(); + + if (!state.config.shouldUpdateImagesOnConnect) { + return; + } + + const allUsedImages = selectAllUsedImages(state); + + moduleLog.trace( + { data: allUsedImages }, + `Fetching new image URLs for ${allUsedImages.length} images` + ); + + allUsedImages.forEach(({ image_name, image_origin }) => { + dispatch( + imageUrlsReceived({ + imageName: image_name, + imageOrigin: image_origin, + }) + ); + }); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index f202b66ca2..4931c498bf 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -110,7 +110,7 @@ export type AppConfig = { /** * Whether or not we should update image urls when image loading errors */ - shouldUpdateImageUrlsOnError: boolean; + shouldUpdateImagesOnConnect: boolean; disabledTabs: InvokeTabName[]; disabledFeatures: AppFeature[]; disabledSDFeatures: SDFeature[]; diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts index fb00a7a5d4..5f4dd68959 100644 --- a/invokeai/frontend/web/src/features/system/store/configSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts @@ -4,7 +4,7 @@ import { AppConfig, PartialAppConfig } from 'app/types/invokeai'; import { merge } from 'lodash-es'; export const initialConfigState: AppConfig = { - shouldUpdateImageUrlsOnError: false, + shouldUpdateImagesOnConnect: false, disabledTabs: [], disabledFeatures: [], disabledSDFeatures: [],