Merge remote-tracking branch 'origin/main' into feat/preview_predicted_x0

# Conflicts:
#	invokeai/app/invocations/generate.py
This commit is contained in:
Kevin Turner 2023-03-12 20:56:02 -07:00
commit 288cee9611
157 changed files with 5699 additions and 2633 deletions

View File

@ -24,3 +24,4 @@ jobs:
days-before-pr-stale: -1 days-before-pr-stale: -1
days-before-pr-close: -1 days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
operations-per-run: 500

View File

@ -1,12 +1,12 @@
name: Test invoke.py pip name: Test invoke.py pip
on: on:
pull_request: pull_request:
paths-ignore: paths:
- 'pyproject.toml' - '**'
- 'invokeai/**' - '!pyproject.toml'
- 'invokeai/backend/**' - '!invokeai/**'
- 'invokeai/configs/**' - 'invokeai/frontend/web/**'
- 'invokeai/frontend/web/dist/**' - '!invokeai/frontend/web/dist/**'
merge_group: merge_group:
workflow_dispatch: workflow_dispatch:

View File

@ -6,15 +6,13 @@ on:
paths: paths:
- 'pyproject.toml' - 'pyproject.toml'
- 'invokeai/**' - 'invokeai/**'
- 'invokeai/backend/**' - '!invokeai/frontend/web/**'
- 'invokeai/configs/**'
- 'invokeai/frontend/web/dist/**' - 'invokeai/frontend/web/dist/**'
pull_request: pull_request:
paths: paths:
- 'pyproject.toml' - 'pyproject.toml'
- 'invokeai/**' - 'invokeai/**'
- 'invokeai/backend/**' - '!invokeai/frontend/web/**'
- 'invokeai/configs/**'
- 'invokeai/frontend/web/dist/**' - 'invokeai/frontend/web/dist/**'
types: types:
- 'ready_for_review' - 'ready_for_review'

View File

@ -4,7 +4,8 @@ import os
from argparse import Namespace from argparse import Namespace
from ...backend import Globals from ...backend import Globals
from ..services.generate_initializer import get_generate from ..services.model_manager_initializer import get_model_manager
from ..services.restoration_services import RestorationServices
from ..services.graph import GraphExecutionState from ..services.graph import GraphExecutionState
from ..services.image_storage import DiskImageStorage from ..services.image_storage import DiskImageStorage
from ..services.invocation_queue import MemoryInvocationQueue from ..services.invocation_queue import MemoryInvocationQueue
@ -37,18 +38,16 @@ class ApiDependencies:
invoker: Invoker = None invoker: Invoker = None
@staticmethod @staticmethod
def initialize(args, config, event_handler_id: int): def initialize(config, event_handler_id: int):
Globals.try_patchmatch = args.patchmatch Globals.try_patchmatch = config.patchmatch
Globals.always_use_cpu = args.always_use_cpu Globals.always_use_cpu = config.always_use_cpu
Globals.internet_available = args.internet_available and check_internet() Globals.internet_available = config.internet_available and check_internet()
Globals.disable_xformers = not args.xformers Globals.disable_xformers = not config.xformers
Globals.ckpt_convert = args.ckpt_convert Globals.ckpt_convert = config.ckpt_convert
# TODO: Use a logger # TODO: Use a logger
print(f">> Internet connectivity is {Globals.internet_available}") print(f">> Internet connectivity is {Globals.internet_available}")
generate = get_generate(args, config)
events = FastAPIEventService(event_handler_id) events = FastAPIEventService(event_handler_id)
output_folder = os.path.abspath( output_folder = os.path.abspath(
@ -61,7 +60,7 @@ class ApiDependencies:
db_location = os.path.join(output_folder, "invokeai.db") db_location = os.path.join(output_folder, "invokeai.db")
services = InvocationServices( services = InvocationServices(
generate=generate, model_manager=get_model_manager(config),
events=events, events=events,
images=images, images=images,
queue=MemoryInvocationQueue(), queue=MemoryInvocationQueue(),
@ -69,6 +68,7 @@ class ApiDependencies:
filename=db_location, table_name="graph_executions" filename=db_location, table_name="graph_executions"
), ),
processor=DefaultInvocationProcessor(), processor=DefaultInvocationProcessor(),
restoration=RestorationServices(config),
) )
ApiDependencies.invoker = Invoker(services) ApiDependencies.invoker = Invoker(services)

View File

@ -1,5 +1,4 @@
# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654)
import asyncio import asyncio
from inspect import signature from inspect import signature
@ -53,11 +52,11 @@ config = {}
# Add startup event to load dependencies # Add startup event to load dependencies
@app.on_event("startup") @app.on_event("startup")
async def startup_event(): async def startup_event():
args = Args() config = Args()
config = args.parse_args() config.parse_args()
ApiDependencies.initialize( ApiDependencies.initialize(
args=args, config=config, event_handler_id=event_handler_id config=config, event_handler_id=event_handler_id
) )

View File

@ -17,7 +17,8 @@ from .cli.commands import BaseCommand, CliContext, ExitCli, add_parsers, get_gra
from .invocations import * from .invocations import *
from .invocations.baseinvocation import BaseInvocation from .invocations.baseinvocation import BaseInvocation
from .services.events import EventServiceBase from .services.events import EventServiceBase
from .services.generate_initializer import get_generate from .services.model_manager_initializer import get_model_manager
from .services.restoration_services import RestorationServices
from .services.graph import EdgeConnection, GraphExecutionState from .services.graph import EdgeConnection, GraphExecutionState
from .services.image_storage import DiskImageStorage from .services.image_storage import DiskImageStorage
from .services.invocation_queue import MemoryInvocationQueue from .services.invocation_queue import MemoryInvocationQueue
@ -126,14 +127,9 @@ def invoke_all(context: CliContext):
def invoke_cli(): def invoke_cli():
args = Args() config = Args()
config = args.parse_args() config.parse_args()
model_manager = get_model_manager(config)
generate = get_generate(args, config)
# NOTE: load model on first use, uncomment to load at startup
# TODO: Make this a config option?
# generate.load_model()
events = EventServiceBase() events = EventServiceBase()
@ -145,7 +141,7 @@ def invoke_cli():
db_location = os.path.join(output_folder, "invokeai.db") db_location = os.path.join(output_folder, "invokeai.db")
services = InvocationServices( services = InvocationServices(
generate=generate, model_manager=model_manager,
events=events, events=events,
images=DiskImageStorage(output_folder), images=DiskImageStorage(output_folder),
queue=MemoryInvocationQueue(), queue=MemoryInvocationQueue(),
@ -153,6 +149,7 @@ def invoke_cli():
filename=db_location, table_name="graph_executions" filename=db_location, table_name="graph_executions"
), ),
processor=DefaultInvocationProcessor(), processor=DefaultInvocationProcessor(),
restoration=RestorationServices(config),
) )
invoker = Invoker(services) invoker = Invoker(services)

View File

@ -13,13 +13,13 @@ from ..services.image_storage import ImageType
from ..services.invocation_services import InvocationServices from ..services.invocation_services import InvocationServices
from .baseinvocation import BaseInvocation, InvocationContext from .baseinvocation import BaseInvocation, InvocationContext
from .image import ImageField, ImageOutput from .image import ImageField, ImageOutput
from ...backend.generator import Txt2Img, Img2Img, Inpaint, InvokeAIGenerator
from ...backend.stable_diffusion import PipelineIntermediateState from ...backend.stable_diffusion import PipelineIntermediateState
SAMPLER_NAME_VALUES = Literal[ SAMPLER_NAME_VALUES = Literal[
"ddim", "plms", "k_lms", "k_dpm_2", "k_dpm_2_a", "k_euler", "k_euler_a", "k_heun" tuple(InvokeAIGenerator.schedulers())
] ]
# Text to image # Text to image
class TextToImageInvocation(BaseInvocation): class TextToImageInvocation(BaseInvocation):
"""Generates an image using text2img.""" """Generates an image using text2img."""
@ -63,19 +63,18 @@ class TextToImageInvocation(BaseInvocation):
# Handle invalid model parameter # Handle invalid model parameter
# TODO: figure out if this can be done via a validator that uses the model_cache # TODO: figure out if this can be done via a validator that uses the model_cache
# TODO: How to get the default model name now? # TODO: How to get the default model name now?
if self.model is None or self.model == "": # (right now uses whatever current model is set in model manager)
self.model = context.services.generate.model_name model= context.services.model_manager.get_model()
outputs = Txt2Img(model).generate(
# Set the model (if already cached, this does nothing)
context.services.generate.set_model(self.model)
results = context.services.generate.prompt2image(
prompt=self.prompt, prompt=self.prompt,
step_callback=partial(self.dispatch_progress, context), step_callback=partial(self.dispatch_progress, context),
**self.dict( **self.dict(
exclude={"prompt"} exclude={"prompt"}
), # Shorthand for passing all of the parameters above manually ), # Shorthand for passing all of the parameters above manually
) )
# Outputs is an infinite iterator that will return a new InvokeAIGeneratorOutput object
# each time it is called. We only need the first one.
generate_output = next(outputs)
# Results are image and seed, unwrap for now and ignore the seed # Results are image and seed, unwrap for now and ignore the seed
# TODO: pre-seed? # TODO: pre-seed?
@ -84,7 +83,7 @@ class TextToImageInvocation(BaseInvocation):
image_name = context.services.images.create_name( image_name = context.services.images.create_name(
context.graph_execution_state_id, self.id context.graph_execution_state_id, self.id
) )
context.services.images.save(image_type, image_name, results[0][0]) context.services.images.save(image_type, image_name, generate_output.image)
return ImageOutput( return ImageOutput(
image=ImageField(image_type=image_type, image_name=image_name) image=ImageField(image_type=image_type, image_name=image_name)
) )
@ -118,23 +117,20 @@ class ImageToImageInvocation(TextToImageInvocation):
# Handle invalid model parameter # Handle invalid model parameter
# TODO: figure out if this can be done via a validator that uses the model_cache # TODO: figure out if this can be done via a validator that uses the model_cache
# TODO: How to get the default model name now? # TODO: How to get the default model name now?
if self.model is None or self.model == "": model = context.services.model_manager.get_model()
self.model = context.services.generate.model_name generator_output = next(
Img2Img(model).generate(
# Set the model (if already cached, this does nothing) prompt=self.prompt,
context.services.generate.set_model(self.model) init_img=image,
init_mask=mask,
results = context.services.generate.prompt2image(
prompt=self.prompt,
init_img=image,
init_mask=mask,
step_callback=partial(self.dispatch_progress, context), step_callback=partial(self.dispatch_progress, context),
**self.dict( **self.dict(
exclude={"prompt", "image", "mask"} exclude={"prompt", "image", "mask"}
), # Shorthand for passing all of the parameters above manually ), # Shorthand for passing all of the parameters above manually
)
) )
result_image = results[0][0] result_image = generator_output.image
# Results are image and seed, unwrap for now and ignore the seed # Results are image and seed, unwrap for now and ignore the seed
# TODO: pre-seed? # TODO: pre-seed?
@ -148,7 +144,6 @@ class ImageToImageInvocation(TextToImageInvocation):
image=ImageField(image_type=image_type, image_name=image_name) image=ImageField(image_type=image_type, image_name=image_name)
) )
class InpaintInvocation(ImageToImageInvocation): class InpaintInvocation(ImageToImageInvocation):
"""Generates an image using inpaint.""" """Generates an image using inpaint."""
@ -180,23 +175,20 @@ class InpaintInvocation(ImageToImageInvocation):
# Handle invalid model parameter # Handle invalid model parameter
# TODO: figure out if this can be done via a validator that uses the model_cache # TODO: figure out if this can be done via a validator that uses the model_cache
# TODO: How to get the default model name now? # TODO: How to get the default model name now?
if self.model is None or self.model == "": manager = context.services.model_manager.get_model()
self.model = context.services.generate.model_name generator_output = next(
Inpaint(model).generate(
# Set the model (if already cached, this does nothing) prompt=self.prompt,
context.services.generate.set_model(self.model) init_img=image,
init_mask=mask,
results = context.services.generate.prompt2image(
prompt=self.prompt,
init_img=image,
init_mask=mask,
step_callback=partial(self.dispatch_progress, context), step_callback=partial(self.dispatch_progress, context),
**self.dict( **self.dict(
exclude={"prompt", "image", "mask"} exclude={"prompt", "image", "mask"}
), # Shorthand for passing all of the parameters above manually ), # Shorthand for passing all of the parameters above manually
)
) )
result_image = results[0][0] result_image = generator_output.image
# Results are image and seed, unwrap for now and ignore the seed # Results are image and seed, unwrap for now and ignore the seed
# TODO: pre-seed? # TODO: pre-seed?

View File

@ -8,7 +8,6 @@ from ..services.invocation_services import InvocationServices
from .baseinvocation import BaseInvocation, InvocationContext from .baseinvocation import BaseInvocation, InvocationContext
from .image import ImageField, ImageOutput from .image import ImageField, ImageOutput
class RestoreFaceInvocation(BaseInvocation): class RestoreFaceInvocation(BaseInvocation):
"""Restores faces in an image.""" """Restores faces in an image."""
#fmt: off #fmt: off
@ -23,7 +22,7 @@ class RestoreFaceInvocation(BaseInvocation):
image = context.services.images.get( image = context.services.images.get(
self.image.image_type, self.image.image_name self.image.image_type, self.image.image_name
) )
results = context.services.generate.upscale_and_reconstruct( results = context.services.restoration.upscale_and_reconstruct(
image_list=[[image, 0]], image_list=[[image, 0]],
upscale=None, upscale=None,
strength=self.strength, # GFPGAN strength strength=self.strength, # GFPGAN strength

View File

@ -26,7 +26,7 @@ class UpscaleInvocation(BaseInvocation):
image = context.services.images.get( image = context.services.images.get(
self.image.image_type, self.image.image_name self.image.image_type, self.image.image_name
) )
results = context.services.generate.upscale_and_reconstruct( results = context.services.restoration.upscale_and_reconstruct(
image_list=[[image, 0]], image_list=[[image, 0]],
upscale=(self.level, self.strength), upscale=(self.level, self.strength),
strength=0.0, # GFPGAN strength strength=0.0, # GFPGAN strength

View File

@ -1,255 +0,0 @@
import os
import sys
import traceback
from argparse import Namespace
import invokeai.version
from invokeai.backend import Generate, ModelManager
from ...backend import Globals
# TODO: most of this code should be split into individual services as the Generate.py code is deprecated
def get_generate(args, config) -> Generate:
if not args.conf:
config_file = os.path.join(Globals.root, "configs", "models.yaml")
if not os.path.exists(config_file):
report_model_error(
args, FileNotFoundError(f"The file {config_file} could not be found.")
)
print(f">> {invokeai.version.__app_name__}, version {invokeai.version.__version__}")
print(f'>> InvokeAI runtime directory is "{Globals.root}"')
# these two lines prevent a horrible warning message from appearing
# when the frozen CLIP tokenizer is imported
import transformers # type: ignore
transformers.logging.set_verbosity_error()
import diffusers
diffusers.logging.set_verbosity_error()
# Loading Face Restoration and ESRGAN Modules
gfpgan, codeformer, esrgan = load_face_restoration(args)
# normalize the config directory relative to root
if not os.path.isabs(args.conf):
args.conf = os.path.normpath(os.path.join(Globals.root, args.conf))
if args.embeddings:
if not os.path.isabs(args.embedding_path):
embedding_path = os.path.normpath(
os.path.join(Globals.root, args.embedding_path)
)
else:
embedding_path = args.embedding_path
else:
embedding_path = None
# migrate legacy models
ModelManager.migrate_models()
# load the infile as a list of lines
if args.infile:
try:
if os.path.isfile(args.infile):
infile = open(args.infile, "r", encoding="utf-8")
elif args.infile == "-": # stdin
infile = sys.stdin
else:
raise FileNotFoundError(f"{args.infile} not found.")
except (FileNotFoundError, IOError) as e:
print(f"{e}. Aborting.")
sys.exit(-1)
# creating a Generate object:
try:
gen = Generate(
conf=args.conf,
model=args.model,
sampler_name=args.sampler_name,
embedding_path=embedding_path,
full_precision=args.full_precision,
precision=args.precision,
gfpgan=gfpgan,
codeformer=codeformer,
esrgan=esrgan,
free_gpu_mem=args.free_gpu_mem,
safety_checker=args.safety_checker,
max_loaded_models=args.max_loaded_models,
)
except (FileNotFoundError, TypeError, AssertionError) as e:
report_model_error(opt, e)
except (IOError, KeyError) as e:
print(f"{e}. Aborting.")
sys.exit(-1)
if args.seamless:
print(">> changed to seamless tiling mode")
# preload the model
try:
gen.load_model()
except KeyError:
pass
except Exception as e:
report_model_error(args, e)
# try to autoconvert new models
# autoimport new .ckpt files
if path := args.autoconvert:
gen.model_manager.autoconvert_weights(
conf_path=args.conf,
weights_directory=path,
)
return gen
def load_face_restoration(opt):
try:
gfpgan, codeformer, esrgan = None, None, None
if opt.restore or opt.esrgan:
from invokeai.backend.restoration import Restoration
restoration = Restoration()
if opt.restore:
gfpgan, codeformer = restoration.load_face_restore_models(
opt.gfpgan_model_path
)
else:
print(">> Face restoration disabled")
if opt.esrgan:
esrgan = restoration.load_esrgan(opt.esrgan_bg_tile)
else:
print(">> Upscaling disabled")
else:
print(">> Face restoration and upscaling disabled")
except (ModuleNotFoundError, ImportError):
print(traceback.format_exc(), file=sys.stderr)
print(">> You may need to install the ESRGAN and/or GFPGAN modules")
return gfpgan, codeformer, esrgan
def report_model_error(opt: Namespace, e: Exception):
print(f'** An error occurred while attempting to initialize the model: "{str(e)}"')
print(
"** This can be caused by a missing or corrupted models file, and can sometimes be fixed by (re)installing the models."
)
yes_to_all = os.environ.get("INVOKE_MODEL_RECONFIGURE")
if yes_to_all:
print(
"** Reconfiguration is being forced by environment variable INVOKE_MODEL_RECONFIGURE"
)
else:
response = input(
"Do you want to run invokeai-configure script to select and/or reinstall models? [y] "
)
if response.startswith(("n", "N")):
return
print("invokeai-configure is launching....\n")
# Match arguments that were set on the CLI
# only the arguments accepted by the configuration script are parsed
root_dir = ["--root", opt.root_dir] if opt.root_dir is not None else []
config = ["--config", opt.conf] if opt.conf is not None else []
previous_args = sys.argv
sys.argv = ["invokeai-configure"]
sys.argv.extend(root_dir)
sys.argv.extend(config)
if yes_to_all is not None:
for arg in yes_to_all.split():
sys.argv.append(arg)
from invokeai.frontend.install import invokeai_configure
invokeai_configure()
# TODO: Figure out how to restart
# print('** InvokeAI will now restart')
# sys.argv = previous_args
# main() # would rather do a os.exec(), but doesn't exist?
# sys.exit(0)
# Temporary initializer for Generate until we migrate off of it
def old_get_generate(args, config) -> Generate:
# TODO: Remove the need for globals
from invokeai.backend.globals import Globals
# alert - setting globals here
Globals.root = os.path.expanduser(
args.root_dir or os.environ.get("INVOKEAI_ROOT") or os.path.abspath(".")
)
Globals.try_patchmatch = args.patchmatch
print(f'>> InvokeAI runtime directory is "{Globals.root}"')
# these two lines prevent a horrible warning message from appearing
# when the frozen CLIP tokenizer is imported
import transformers
transformers.logging.set_verbosity_error()
# Loading Face Restoration and ESRGAN Modules
gfpgan, codeformer, esrgan = None, None, None
try:
if config.restore or config.esrgan:
from ldm.invoke.restoration import Restoration
restoration = Restoration()
if config.restore:
gfpgan, codeformer = restoration.load_face_restore_models(
config.gfpgan_model_path
)
else:
print(">> Face restoration disabled")
if config.esrgan:
esrgan = restoration.load_esrgan(config.esrgan_bg_tile)
else:
print(">> Upscaling disabled")
else:
print(">> Face restoration and upscaling disabled")
except (ModuleNotFoundError, ImportError):
print(traceback.format_exc(), file=sys.stderr)
print(">> You may need to install the ESRGAN and/or GFPGAN modules")
# normalize the config directory relative to root
if not os.path.isabs(config.conf):
config.conf = os.path.normpath(os.path.join(Globals.root, config.conf))
if config.embeddings:
if not os.path.isabs(config.embedding_path):
embedding_path = os.path.normpath(
os.path.join(Globals.root, config.embedding_path)
)
else:
embedding_path = None
# TODO: lazy-initialize this by wrapping it
try:
generate = Generate(
conf=config.conf,
model=config.model,
sampler_name=config.sampler_name,
embedding_path=embedding_path,
full_precision=config.full_precision,
precision=config.precision,
gfpgan=gfpgan,
codeformer=codeformer,
esrgan=esrgan,
free_gpu_mem=config.free_gpu_mem,
safety_checker=config.safety_checker,
max_loaded_models=config.max_loaded_models,
)
except (FileNotFoundError, TypeError, AssertionError):
# emergency_model_reconfigure() # TODO?
sys.exit(-1)
except (IOError, KeyError) as e:
print(f"{e}. Aborting.")
sys.exit(-1)
generate.free_gpu_mem = config.free_gpu_mem
return generate

View File

@ -1,36 +1,39 @@
# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654)
from invokeai.backend import Generate from invokeai.backend import ModelManager
from .events import EventServiceBase from .events import EventServiceBase
from .image_storage import ImageStorageBase from .image_storage import ImageStorageBase
from .restoration_services import RestorationServices
from .invocation_queue import InvocationQueueABC from .invocation_queue import InvocationQueueABC
from .item_storage import ItemStorageABC from .item_storage import ItemStorageABC
class InvocationServices: class InvocationServices:
"""Services that can be used by invocations""" """Services that can be used by invocations"""
generate: Generate # TODO: wrap Generate, or split it up from model?
events: EventServiceBase events: EventServiceBase
images: ImageStorageBase images: ImageStorageBase
queue: InvocationQueueABC queue: InvocationQueueABC
model_manager: ModelManager
restoration: RestorationServices
# NOTE: we must forward-declare any types that include invocations, since invocations can use services # NOTE: we must forward-declare any types that include invocations, since invocations can use services
graph_execution_manager: ItemStorageABC["GraphExecutionState"] graph_execution_manager: ItemStorageABC["GraphExecutionState"]
processor: "InvocationProcessorABC" processor: "InvocationProcessorABC"
def __init__( def __init__(
self, self,
generate: Generate, model_manager: ModelManager,
events: EventServiceBase, events: EventServiceBase,
images: ImageStorageBase, images: ImageStorageBase,
queue: InvocationQueueABC, queue: InvocationQueueABC,
graph_execution_manager: ItemStorageABC["GraphExecutionState"], graph_execution_manager: ItemStorageABC["GraphExecutionState"],
processor: "InvocationProcessorABC", processor: "InvocationProcessorABC",
restoration: RestorationServices,
): ):
self.generate = generate self.model_manager = model_manager
self.events = events self.events = events
self.images = images self.images = images
self.queue = queue self.queue = queue
self.graph_execution_manager = graph_execution_manager self.graph_execution_manager = graph_execution_manager
self.processor = processor self.processor = processor
self.restoration = restoration

View File

@ -0,0 +1,120 @@
import os
import sys
import torch
from argparse import Namespace
from invokeai.backend import Args
from omegaconf import OmegaConf
from pathlib import Path
import invokeai.version
from ...backend import ModelManager
from ...backend.util import choose_precision, choose_torch_device
from ...backend import Globals
# TODO: Replace with an abstract class base ModelManagerBase
def get_model_manager(config: Args) -> ModelManager:
if not config.conf:
config_file = os.path.join(Globals.root, "configs", "models.yaml")
if not os.path.exists(config_file):
report_model_error(
config, FileNotFoundError(f"The file {config_file} could not be found.")
)
print(f">> {invokeai.version.__app_name__}, version {invokeai.version.__version__}")
print(f'>> InvokeAI runtime directory is "{Globals.root}"')
# these two lines prevent a horrible warning message from appearing
# when the frozen CLIP tokenizer is imported
import transformers # type: ignore
transformers.logging.set_verbosity_error()
import diffusers
diffusers.logging.set_verbosity_error()
# normalize the config directory relative to root
if not os.path.isabs(config.conf):
config.conf = os.path.normpath(os.path.join(Globals.root, config.conf))
if config.embeddings:
if not os.path.isabs(config.embedding_path):
embedding_path = os.path.normpath(
os.path.join(Globals.root, config.embedding_path)
)
else:
embedding_path = config.embedding_path
else:
embedding_path = None
# migrate legacy models
ModelManager.migrate_models()
# creating the model manager
try:
device = torch.device(choose_torch_device())
precision = 'float16' if config.precision=='float16' \
else 'float32' if config.precision=='float32' \
else choose_precision(device)
model_manager = ModelManager(
OmegaConf.load(config.conf),
precision=precision,
device_type=device,
max_loaded_models=config.max_loaded_models,
embedding_path = Path(embedding_path),
)
except (FileNotFoundError, TypeError, AssertionError) as e:
report_model_error(config, e)
except (IOError, KeyError) as e:
print(f"{e}. Aborting.")
sys.exit(-1)
# try to autoconvert new models
# autoimport new .ckpt files
if path := config.autoconvert:
model_manager.autoconvert_weights(
conf_path=config.conf,
weights_directory=path,
)
return model_manager
def report_model_error(opt: Namespace, e: Exception):
print(f'** An error occurred while attempting to initialize the model: "{str(e)}"')
print(
"** This can be caused by a missing or corrupted models file, and can sometimes be fixed by (re)installing the models."
)
yes_to_all = os.environ.get("INVOKE_MODEL_RECONFIGURE")
if yes_to_all:
print(
"** Reconfiguration is being forced by environment variable INVOKE_MODEL_RECONFIGURE"
)
else:
response = input(
"Do you want to run invokeai-configure script to select and/or reinstall models? [y] "
)
if response.startswith(("n", "N")):
return
print("invokeai-configure is launching....\n")
# Match arguments that were set on the CLI
# only the arguments accepted by the configuration script are parsed
root_dir = ["--root", opt.root_dir] if opt.root_dir is not None else []
config = ["--config", opt.conf] if opt.conf is not None else []
previous_config = sys.argv
sys.argv = ["invokeai-configure"]
sys.argv.extend(root_dir)
sys.argv.extend(config.to_dict())
if yes_to_all is not None:
for arg in yes_to_all.split():
sys.argv.append(arg)
from invokeai.frontend.install import invokeai_configure
invokeai_configure()
# TODO: Figure out how to restart
# print('** InvokeAI will now restart')
# sys.argv = previous_args
# main() # would rather do a os.exec(), but doesn't exist?
# sys.exit(0)

View File

@ -0,0 +1,109 @@
import sys
import traceback
import torch
from ...backend.restoration import Restoration
from ...backend.util import choose_torch_device, CPU_DEVICE, MPS_DEVICE
# This should be a real base class for postprocessing functions,
# but right now we just instantiate the existing gfpgan, esrgan
# and codeformer functions.
class RestorationServices:
'''Face restoration and upscaling'''
def __init__(self,args):
try:
gfpgan, codeformer, esrgan = None, None, None
if args.restore or args.esrgan:
restoration = Restoration()
if args.restore:
gfpgan, codeformer = restoration.load_face_restore_models(
args.gfpgan_model_path
)
else:
print(">> Face restoration disabled")
if args.esrgan:
esrgan = restoration.load_esrgan(args.esrgan_bg_tile)
else:
print(">> Upscaling disabled")
else:
print(">> Face restoration and upscaling disabled")
except (ModuleNotFoundError, ImportError):
print(traceback.format_exc(), file=sys.stderr)
print(">> You may need to install the ESRGAN and/or GFPGAN modules")
self.device = torch.device(choose_torch_device())
self.gfpgan = gfpgan
self.codeformer = codeformer
self.esrgan = esrgan
# note that this one method does gfpgan and codepath reconstruction, as well as
# esrgan upscaling
# TO DO: refactor into separate methods
def upscale_and_reconstruct(
self,
image_list,
facetool="gfpgan",
upscale=None,
upscale_denoise_str=0.75,
strength=0.0,
codeformer_fidelity=0.75,
save_original=False,
image_callback=None,
prefix=None,
):
results = []
for r in image_list:
image, seed = r
try:
if strength > 0:
if self.gfpgan is not None or self.codeformer is not None:
if facetool == "gfpgan":
if self.gfpgan is None:
print(
">> GFPGAN not found. Face restoration is disabled."
)
else:
image = self.gfpgan.process(image, strength, seed)
if facetool == "codeformer":
if self.codeformer is None:
print(
">> CodeFormer not found. Face restoration is disabled."
)
else:
cf_device = (
CPU_DEVICE if self.device == MPS_DEVICE else self.device
)
image = self.codeformer.process(
image=image,
strength=strength,
device=cf_device,
seed=seed,
fidelity=codeformer_fidelity,
)
else:
print(">> Face Restoration is disabled.")
if upscale is not None:
if self.esrgan is not None:
if len(upscale) < 2:
upscale.append(0.75)
image = self.esrgan.process(
image,
upscale[1],
seed,
int(upscale[0]),
denoise_str=upscale_denoise_str,
)
else:
print(">> ESRGAN is disabled. Image not upscaled.")
except Exception as e:
print(
f">> Error running RealESRGAN or GFPGAN. Your image was not upscaled.\n{e}"
)
if image_callback is not None:
image_callback(image, seed, upscaled=True, use_prefix=prefix)
else:
r[0] = image
results.append([image, seed])
return results

View File

@ -2,6 +2,15 @@
Initialization file for invokeai.backend Initialization file for invokeai.backend
""" """
from .generate import Generate from .generate import Generate
from .generator import (
InvokeAIGeneratorBasicParams,
InvokeAIGenerator,
InvokeAIGeneratorOutput,
Txt2Img,
Img2Img,
Inpaint
)
from .model_management import ModelManager from .model_management import ModelManager
from .safety_checker import SafetyChecker
from .args import Args from .args import Args
from .globals import Globals from .globals import Globals

View File

@ -25,18 +25,19 @@ from accelerate.utils import set_seed
from diffusers.pipeline_utils import DiffusionPipeline from diffusers.pipeline_utils import DiffusionPipeline
from diffusers.utils.import_utils import is_xformers_available from diffusers.utils.import_utils import is_xformers_available
from omegaconf import OmegaConf from omegaconf import OmegaConf
from pathlib import Path
from .args import metadata_from_png from .args import metadata_from_png
from .generator import infill_methods from .generator import infill_methods
from .globals import Globals, global_cache_dir from .globals import Globals, global_cache_dir
from .image_util import InitImageResizer, PngWriter, Txt2Mask, configure_model_padding from .image_util import InitImageResizer, PngWriter, Txt2Mask, configure_model_padding
from .model_management import ModelManager from .model_management import ModelManager
from .safety_checker import SafetyChecker
from .prompting import get_uc_and_c_and_ec from .prompting import get_uc_and_c_and_ec
from .prompting.conditioning import log_tokenization from .prompting.conditioning import log_tokenization
from .stable_diffusion import HuggingFaceConceptsLibrary from .stable_diffusion import HuggingFaceConceptsLibrary
from .util import choose_precision, choose_torch_device from .util import choose_precision, choose_torch_device
def fix_func(orig): def fix_func(orig):
if hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
@ -222,6 +223,7 @@ class Generate:
self.precision, self.precision,
max_loaded_models=max_loaded_models, max_loaded_models=max_loaded_models,
sequential_offload=self.free_gpu_mem, sequential_offload=self.free_gpu_mem,
embedding_path=Path(self.embedding_path),
) )
# don't accept invalid models # don't accept invalid models
fallback = self.model_manager.default_model() or FALLBACK_MODEL_NAME fallback = self.model_manager.default_model() or FALLBACK_MODEL_NAME
@ -244,31 +246,8 @@ class Generate:
# load safety checker if requested # load safety checker if requested
if safety_checker: if safety_checker:
try: print(">> Initializing NSFW checker")
print(">> Initializing NSFW checker") self.safety_checker = SafetyChecker(self.device)
from diffusers.pipelines.stable_diffusion.safety_checker import (
StableDiffusionSafetyChecker,
)
from transformers import AutoFeatureExtractor
safety_model_id = "CompVis/stable-diffusion-safety-checker"
safety_model_path = global_cache_dir("hub")
self.safety_checker = StableDiffusionSafetyChecker.from_pretrained(
safety_model_id,
local_files_only=True,
cache_dir=safety_model_path,
)
self.safety_feature_extractor = AutoFeatureExtractor.from_pretrained(
safety_model_id,
local_files_only=True,
cache_dir=safety_model_path,
)
self.safety_checker.to(self.device)
except Exception:
print(
"** An error was encountered while installing the safety checker:"
)
print(traceback.format_exc())
else: else:
print(">> NSFW checker is disabled") print(">> NSFW checker is disabled")
@ -523,15 +502,6 @@ class Generate:
generator.set_variation(self.seed, variation_amount, with_variations) generator.set_variation(self.seed, variation_amount, with_variations)
generator.use_mps_noise = use_mps_noise generator.use_mps_noise = use_mps_noise
checker = (
{
"checker": self.safety_checker,
"extractor": self.safety_feature_extractor,
}
if self.safety_checker
else None
)
results = generator.generate( results = generator.generate(
prompt, prompt,
iterations=iterations, iterations=iterations,
@ -558,7 +528,7 @@ class Generate:
embiggen_strength=embiggen_strength, embiggen_strength=embiggen_strength,
inpaint_replace=inpaint_replace, inpaint_replace=inpaint_replace,
mask_blur_radius=mask_blur_radius, mask_blur_radius=mask_blur_radius,
safety_checker=checker, safety_checker=self.safety_checker,
seam_size=seam_size, seam_size=seam_size,
seam_blur=seam_blur, seam_blur=seam_blur,
seam_strength=seam_strength, seam_strength=seam_strength,
@ -940,18 +910,6 @@ class Generate:
self.generators = {} self.generators = {}
set_seed(random.randrange(0, np.iinfo(np.uint32).max)) set_seed(random.randrange(0, np.iinfo(np.uint32).max))
if self.embedding_path is not None:
print(f">> Loading embeddings from {self.embedding_path}")
for root, _, files in os.walk(self.embedding_path):
for name in files:
ti_path = os.path.join(root, name)
self.model.textual_inversion_manager.load_textual_inversion(
ti_path, defer_injecting_tokens=True
)
print(
f'>> Textual inversion triggers: {", ".join(sorted(self.model.textual_inversion_manager.get_all_trigger_strings()))}'
)
self.model_name = model_name self.model_name = model_name
self._set_scheduler() # requires self.model_name to be set first self._set_scheduler() # requires self.model_name to be set first
return self.model return self.model
@ -998,7 +956,7 @@ class Generate:
): ):
results = [] results = []
for r in image_list: for r in image_list:
image, seed = r image, seed, _ = r
try: try:
if strength > 0: if strength > 0:
if self.gfpgan is not None or self.codeformer is not None: if self.gfpgan is not None or self.codeformer is not None:

View File

@ -1,5 +1,13 @@
""" """
Initialization file for the invokeai.generator package Initialization file for the invokeai.generator package
""" """
from .base import Generator from .base import (
InvokeAIGenerator,
InvokeAIGeneratorBasicParams,
InvokeAIGeneratorOutput,
Txt2Img,
Img2Img,
Inpaint,
Generator,
)
from .inpaint import infill_methods from .inpaint import infill_methods

View File

@ -4,11 +4,15 @@ including img2img, txt2img, and inpaint
""" """
from __future__ import annotations from __future__ import annotations
import itertools
import dataclasses
import diffusers
import os import os
import random import random
import traceback import traceback
from abc import ABCMeta
from argparse import Namespace
from contextlib import nullcontext from contextlib import nullcontext
from pathlib import Path
import cv2 import cv2
import numpy as np import numpy as np
@ -17,13 +21,258 @@ from PIL import Image, ImageChops, ImageFilter
from accelerate.utils import set_seed from accelerate.utils import set_seed
from diffusers import DiffusionPipeline from diffusers import DiffusionPipeline
from tqdm import trange from tqdm import trange
from typing import List, Iterator, Type
from dataclasses import dataclass, field
from diffusers.schedulers import SchedulerMixin as Scheduler
import invokeai.assets.web as web_assets from ..image_util import configure_model_padding
from ..util.util import rand_perlin_2d from ..util.util import rand_perlin_2d
from ..safety_checker import SafetyChecker
from ..prompting.conditioning import get_uc_and_c_and_ec
from ..stable_diffusion.diffusers_pipeline import StableDiffusionGeneratorPipeline
downsampling = 8 downsampling = 8
CAUTION_IMG = "caution.png"
@dataclass
class InvokeAIGeneratorBasicParams:
seed: int=None
width: int=512
height: int=512
cfg_scale: int=7.5
steps: int=20
ddim_eta: float=0.0
scheduler: int='ddim'
precision: str='float16'
perlin: float=0.0
threshold: int=0.0
seamless: bool=False
seamless_axes: List[str]=field(default_factory=lambda: ['x', 'y'])
h_symmetry_time_pct: float=None
v_symmetry_time_pct: float=None
variation_amount: float = 0.0
with_variations: list=field(default_factory=list)
safety_checker: SafetyChecker=None
@dataclass
class InvokeAIGeneratorOutput:
'''
InvokeAIGeneratorOutput is a dataclass that contains the outputs of a generation
operation, including the image, its seed, the model name used to generate the image
and the model hash, as well as all the generate() parameters that went into
generating the image (in .params, also available as attributes)
'''
image: Image
seed: int
model_hash: str
attention_maps_images: List[Image]
params: Namespace
# we are interposing a wrapper around the original Generator classes so that
# old code that calls Generate will continue to work.
class InvokeAIGenerator(metaclass=ABCMeta):
scheduler_map = dict(
ddim=diffusers.DDIMScheduler,
dpmpp_2=diffusers.DPMSolverMultistepScheduler,
k_dpm_2=diffusers.KDPM2DiscreteScheduler,
k_dpm_2_a=diffusers.KDPM2AncestralDiscreteScheduler,
k_dpmpp_2=diffusers.DPMSolverMultistepScheduler,
k_euler=diffusers.EulerDiscreteScheduler,
k_euler_a=diffusers.EulerAncestralDiscreteScheduler,
k_heun=diffusers.HeunDiscreteScheduler,
k_lms=diffusers.LMSDiscreteScheduler,
plms=diffusers.PNDMScheduler,
)
def __init__(self,
model_info: dict,
params: InvokeAIGeneratorBasicParams=InvokeAIGeneratorBasicParams(),
):
self.model_info=model_info
self.params=params
def generate(self,
prompt: str='',
callback: callable=None,
step_callback: callable=None,
iterations: int=1,
**keyword_args,
)->Iterator[InvokeAIGeneratorOutput]:
'''
Return an iterator across the indicated number of generations.
Each time the iterator is called it will return an InvokeAIGeneratorOutput
object. Use like this:
outputs = txt2img.generate(prompt='banana sushi', iterations=5)
for result in outputs:
print(result.image, result.seed)
In the typical case of wanting to get just a single image, iterations
defaults to 1 and do:
output = next(txt2img.generate(prompt='banana sushi')
Pass None to get an infinite iterator.
outputs = txt2img.generate(prompt='banana sushi', iterations=None)
for o in outputs:
print(o.image, o.seed)
'''
generator_args = dataclasses.asdict(self.params)
generator_args.update(keyword_args)
model_info = self.model_info
model_name = model_info['model_name']
model:StableDiffusionGeneratorPipeline = model_info['model']
model_hash = model_info['hash']
scheduler: Scheduler = self.get_scheduler(
model=model,
scheduler_name=generator_args.get('scheduler')
)
uc, c, extra_conditioning_info = get_uc_and_c_and_ec(prompt,model=model)
gen_class = self._generator_class()
generator = gen_class(model, self.params.precision)
if self.params.variation_amount > 0:
generator.set_variation(generator_args.get('seed'),
generator_args.get('variation_amount'),
generator_args.get('with_variations')
)
if isinstance(model, DiffusionPipeline):
for component in [model.unet, model.vae]:
configure_model_padding(component,
generator_args.get('seamless',False),
generator_args.get('seamless_axes')
)
else:
configure_model_padding(model,
generator_args.get('seamless',False),
generator_args.get('seamless_axes')
)
iteration_count = range(iterations) if iterations else itertools.count(start=0, step=1)
for i in iteration_count:
results = generator.generate(prompt,
conditioning=(uc, c, extra_conditioning_info),
sampler=scheduler,
**generator_args,
)
output = InvokeAIGeneratorOutput(
image=results[0][0],
seed=results[0][1],
attention_maps_images=results[0][2],
model_hash = model_hash,
params=Namespace(model_name=model_name,**generator_args),
)
if callback:
callback(output)
yield output
@classmethod
def schedulers(self)->List[str]:
'''
Return list of all the schedulers that we currently handle.
'''
return list(self.scheduler_map.keys())
def load_generator(self, model: StableDiffusionGeneratorPipeline, generator_class: Type[Generator]):
return generator_class(model, self.params.precision)
def get_scheduler(self, scheduler_name:str, model: StableDiffusionGeneratorPipeline)->Scheduler:
scheduler_class = self.scheduler_map.get(scheduler_name,'ddim')
scheduler = scheduler_class.from_config(model.scheduler.config)
# hack copied over from generate.py
if not hasattr(scheduler, 'uses_inpainting_model'):
scheduler.uses_inpainting_model = lambda: False
return scheduler
@classmethod
def _generator_class(cls)->Type[Generator]:
'''
In derived classes return the name of the generator to apply.
If you don't override will return the name of the derived
class, which nicely parallels the generator class names.
'''
return Generator
# ------------------------------------
class Txt2Img(InvokeAIGenerator):
@classmethod
def _generator_class(cls):
from .txt2img import Txt2Img
return Txt2Img
# ------------------------------------
class Img2Img(InvokeAIGenerator):
def generate(self,
init_image: Image | torch.FloatTensor,
strength: float=0.75,
**keyword_args
)->List[InvokeAIGeneratorOutput]:
return super().generate(init_image=init_image,
strength=strength,
**keyword_args
)
@classmethod
def _generator_class(cls):
from .img2img import Img2Img
return Img2Img
# ------------------------------------
# Takes all the arguments of Img2Img and adds the mask image and the seam/infill stuff
class Inpaint(Img2Img):
def generate(self,
mask_image: Image | torch.FloatTensor,
# Seam settings - when 0, doesn't fill seam
seam_size: int = 0,
seam_blur: int = 0,
seam_strength: float = 0.7,
seam_steps: int = 10,
tile_size: int = 32,
inpaint_replace=False,
infill_method=None,
inpaint_width=None,
inpaint_height=None,
inpaint_fill: tuple(int) = (0x7F, 0x7F, 0x7F, 0xFF),
**keyword_args
)->List[InvokeAIGeneratorOutput]:
return super().generate(
mask_image=mask_image,
seam_size=seam_size,
seam_blur=seam_blur,
seam_strength=seam_strength,
seam_steps=seam_steps,
tile_size=tile_size,
inpaint_replace=inpaint_replace,
infill_method=infill_method,
inpaint_width=inpaint_width,
inpaint_height=inpaint_height,
inpaint_fill=inpaint_fill,
**keyword_args
)
@classmethod
def _generator_class(cls):
from .inpaint import Inpaint
return Inpaint
# ------------------------------------
class Embiggen(Txt2Img):
def generate(
self,
embiggen: list=None,
embiggen_tiles: list = None,
strength: float=0.75,
**kwargs)->List[InvokeAIGeneratorOutput]:
return super().generate(embiggen=embiggen,
embiggen_tiles=embiggen_tiles,
strength=strength,
**kwargs)
@classmethod
def _generator_class(cls):
from .embiggen import Embiggen
return Embiggen
class Generator: class Generator:
downsampling_factor: int downsampling_factor: int
@ -44,7 +293,6 @@ class Generator:
self.with_variations = [] self.with_variations = []
self.use_mps_noise = False self.use_mps_noise = False
self.free_gpu_mem = None self.free_gpu_mem = None
self.caution_img = None
# this is going to be overridden in img2img.py, txt2img.py and inpaint.py # this is going to be overridden in img2img.py, txt2img.py and inpaint.py
def get_make_image(self, prompt, **kwargs): def get_make_image(self, prompt, **kwargs):
@ -64,10 +312,10 @@ class Generator:
def generate( def generate(
self, self,
prompt, prompt,
init_image,
width, width,
height, height,
sampler, sampler,
init_image=None,
iterations=1, iterations=1,
seed=None, seed=None,
image_callback=None, image_callback=None,
@ -76,7 +324,7 @@ class Generator:
perlin=0.0, perlin=0.0,
h_symmetry_time_pct=None, h_symmetry_time_pct=None,
v_symmetry_time_pct=None, v_symmetry_time_pct=None,
safety_checker: dict = None, safety_checker: SafetyChecker=None,
free_gpu_mem: bool = False, free_gpu_mem: bool = False,
**kwargs, **kwargs,
): ):
@ -130,9 +378,9 @@ class Generator:
image = make_image(x_T) image = make_image(x_T)
if self.safety_checker is not None: if self.safety_checker is not None:
image = self.safety_check(image) image = self.safety_checker.check(image)
results.append([image, seed]) results.append([image, seed, attention_maps_images])
if image_callback is not None: if image_callback is not None:
attention_maps_image = ( attention_maps_image = (
@ -292,16 +540,6 @@ class Generator:
seed = random.randrange(0, np.iinfo(np.uint32).max) seed = random.randrange(0, np.iinfo(np.uint32).max)
return (seed, initial_noise) return (seed, initial_noise)
# returns a tensor filled with random numbers from a normal distribution
def get_noise(self, width, height):
"""
Returns a tensor filled with random numbers, either form a normal distribution
(txt2img) or from the latent image (img2img, inpaint)
"""
raise NotImplementedError(
"get_noise() must be implemented in a descendent class"
)
def get_perlin_noise(self, width, height): def get_perlin_noise(self, width, height):
fixdevice = "cpu" if (self.model.device.type == "mps") else self.model.device fixdevice = "cpu" if (self.model.device.type == "mps") else self.model.device
# limit noise to only the diffusion image channels, not the mask channels # limit noise to only the diffusion image channels, not the mask channels
@ -361,53 +599,6 @@ class Generator:
return v2 return v2
def safety_check(self, image: Image.Image):
"""
If the CompViz safety checker flags an NSFW image, we
blur it out.
"""
import diffusers
checker = self.safety_checker["checker"]
extractor = self.safety_checker["extractor"]
features = extractor([image], return_tensors="pt")
features.to(self.model.device)
# unfortunately checker requires the numpy version, so we have to convert back
x_image = np.array(image).astype(np.float32) / 255.0
x_image = x_image[None].transpose(0, 3, 1, 2)
diffusers.logging.set_verbosity_error()
checked_image, has_nsfw_concept = checker(
images=x_image, clip_input=features.pixel_values
)
if has_nsfw_concept[0]:
print(
"** An image with potential non-safe content has been detected. A blurred image will be returned. **"
)
return self.blur(image)
else:
return image
def blur(self, input):
blurry = input.filter(filter=ImageFilter.GaussianBlur(radius=32))
try:
caution = self.get_caution_img()
if caution:
blurry.paste(caution, (0, 0), caution)
except FileNotFoundError:
pass
return blurry
def get_caution_img(self):
path = None
if self.caution_img:
return self.caution_img
path = Path(web_assets.__path__[0]) / CAUTION_IMG
caution = Image.open(path)
self.caution_img = caution.resize((caution.width // 2, caution.height // 2))
return self.caution_img
# this is a handy routine for debugging use. Given a generated sample, # this is a handy routine for debugging use. Given a generated sample,
# convert it into a PNG image and store it at the indicated path # convert it into a PNG image and store it at the indicated path
def save_sample(self, sample, filepath): def save_sample(self, sample, filepath):

View File

@ -34,8 +34,7 @@ from picklescan.scanner import scan_file_path
from invokeai.backend.globals import Globals, global_cache_dir from invokeai.backend.globals import Globals, global_cache_dir
from ..stable_diffusion import StableDiffusionGeneratorPipeline from ..stable_diffusion import StableDiffusionGeneratorPipeline
from ..util import CPU_DEVICE, ask_user, download_with_resume from ..util import CUDA_DEVICE, CPU_DEVICE, ask_user, download_with_resume
class SDLegacyType(Enum): class SDLegacyType(Enum):
V1 = 1 V1 = 1
@ -51,23 +50,29 @@ VAE_TO_REPO_ID = { # hack, see note in convert_and_import()
} }
class ModelManager(object): class ModelManager(object):
'''
Model manager handles loading, caching, importing, deleting, converting, and editing models.
'''
def __init__( def __init__(
self, self,
config: OmegaConf, config: OmegaConf|Path,
device_type: torch.device = CPU_DEVICE, device_type: torch.device = CUDA_DEVICE,
precision: str = "float16", precision: str = "float16",
max_loaded_models=DEFAULT_MAX_MODELS, max_loaded_models=DEFAULT_MAX_MODELS,
sequential_offload=False, sequential_offload=False,
embedding_path: Path=None,
): ):
""" """
Initialize with the path to the models.yaml config file, Initialize with the path to the models.yaml config file or
the torch device type, and precision. The optional an initialized OmegaConf dictionary. Optional parameters
min_avail_mem argument specifies how much unused system are the torch device type, precision, max_loaded_models,
(CPU) memory to preserve. The cache of models in RAM will and sequential_offload boolean. Note that the default device
grow until this value is approached. Default is 2G. type and precision are set up for a CUDA system running at half precision.
""" """
# prevent nasty-looking CLIP log message # prevent nasty-looking CLIP log message
transformers.logging.set_verbosity_error() transformers.logging.set_verbosity_error()
if not isinstance(config, DictConfig):
config = OmegaConf.load(config)
self.config = config self.config = config
self.precision = precision self.precision = precision
self.device = torch.device(device_type) self.device = torch.device(device_type)
@ -76,6 +81,7 @@ class ModelManager(object):
self.stack = [] # this is an LRU FIFO self.stack = [] # this is an LRU FIFO
self.current_model = None self.current_model = None
self.sequential_offload = sequential_offload self.sequential_offload = sequential_offload
self.embedding_path = embedding_path
def valid_model(self, model_name: str) -> bool: def valid_model(self, model_name: str) -> bool:
""" """
@ -84,12 +90,15 @@ class ModelManager(object):
""" """
return model_name in self.config return model_name in self.config
def get_model(self, model_name: str): def get_model(self, model_name: str=None)->dict:
""" """
Given a model named identified in models.yaml, return Given a model named identified in models.yaml, return
the model object. If in RAM will load into GPU VRAM. the model object. If in RAM will load into GPU VRAM.
If on disk, will load from there. If on disk, will load from there.
""" """
if not model_name:
return self.get_model(self.current_model) if self.current_model else self.get_model(self.default_model())
if not self.valid_model(model_name): if not self.valid_model(model_name):
print( print(
f'** "{model_name}" is not a known model name. Please check your models.yaml file' f'** "{model_name}" is not a known model name. Please check your models.yaml file'
@ -112,6 +121,7 @@ class ModelManager(object):
else: # we're about to load a new model, so potentially offload the least recently used one else: # we're about to load a new model, so potentially offload the least recently used one
requested_model, width, height, hash = self._load_model(model_name) requested_model, width, height, hash = self._load_model(model_name)
self.models[model_name] = { self.models[model_name] = {
"model_name": model_name,
"model": requested_model, "model": requested_model,
"width": width, "width": width,
"height": height, "height": height,
@ -121,6 +131,7 @@ class ModelManager(object):
self.current_model = model_name self.current_model = model_name
self._push_newest_model(model_name) self._push_newest_model(model_name)
return { return {
"model_name": model_name,
"model": requested_model, "model": requested_model,
"width": width, "width": width,
"height": height, "height": height,
@ -425,6 +436,7 @@ class ModelManager(object):
height = width height = width
print(f" | Default image dimensions = {width} x {height}") print(f" | Default image dimensions = {width} x {height}")
self._add_embeddings_to_model(pipeline)
return pipeline, width, height, model_hash return pipeline, width, height, model_hash
@ -1061,6 +1073,19 @@ class ModelManager(object):
self.stack.remove(model_name) self.stack.remove(model_name)
self.stack.append(model_name) self.stack.append(model_name)
def _add_embeddings_to_model(self, model: StableDiffusionGeneratorPipeline):
if self.embedding_path is not None:
print(f">> Loading embeddings from {self.embedding_path}")
for root, _, files in os.walk(self.embedding_path):
for name in files:
ti_path = os.path.join(root, name)
model.textual_inversion_manager.load_textual_inversion(
ti_path, defer_injecting_tokens=True
)
print(
f'>> Textual inversion triggers: {", ".join(sorted(model.textual_inversion_manager.get_all_trigger_strings()))}'
)
def _has_cuda(self) -> bool: def _has_cuda(self) -> bool:
return self.device.type == "cuda" return self.device.type == "cuda"

View File

@ -0,0 +1,82 @@
'''
SafetyChecker class - checks images against the StabilityAI NSFW filter
and blurs images that contain potential NSFW content.
'''
import diffusers
import numpy as np
import torch
import traceback
from diffusers.pipelines.stable_diffusion.safety_checker import (
StableDiffusionSafetyChecker,
)
from pathlib import Path
from PIL import Image, ImageFilter
from transformers import AutoFeatureExtractor
import invokeai.assets.web as web_assets
from .globals import global_cache_dir
from .util import CPU_DEVICE
class SafetyChecker(object):
CAUTION_IMG = "caution.png"
def __init__(self, device: torch.device):
path = Path(web_assets.__path__[0]) / self.CAUTION_IMG
caution = Image.open(path)
self.caution_img = caution.resize((caution.width // 2, caution.height // 2))
self.device = device
try:
safety_model_id = "CompVis/stable-diffusion-safety-checker"
safety_model_path = global_cache_dir("hub")
self.safety_checker = StableDiffusionSafetyChecker.from_pretrained(
safety_model_id,
local_files_only=True,
cache_dir=safety_model_path,
)
self.safety_feature_extractor = AutoFeatureExtractor.from_pretrained(
safety_model_id,
local_files_only=True,
cache_dir=safety_model_path,
)
except Exception:
print(
"** An error was encountered while installing the safety checker:"
)
print(traceback.format_exc())
def check(self, image: Image.Image):
"""
Check provided image against the StabilityAI safety checker and return
"""
self.safety_checker.to(self.device)
features = self.safety_feature_extractor([image], return_tensors="pt")
features.to(self.device)
# unfortunately checker requires the numpy version, so we have to convert back
x_image = np.array(image).astype(np.float32) / 255.0
x_image = x_image[None].transpose(0, 3, 1, 2)
diffusers.logging.set_verbosity_error()
checked_image, has_nsfw_concept = self.safety_checker(
images=x_image, clip_input=features.pixel_values
)
self.safety_checker.to(CPU_DEVICE) # offload
if has_nsfw_concept[0]:
print(
"** An image with potential non-safe content has been detected. A blurred image will be returned. **"
)
return self.blur(image)
else:
return image
def blur(self, input):
blurry = input.filter(filter=ImageFilter.GaussianBlur(radius=32))
try:
if caution := self.caution_img:
blurry.paste(caution, (0, 0), caution)
except FileNotFoundError:
pass
return blurry

View File

@ -35,6 +35,7 @@ module.exports = {
{ varsIgnorePattern: '^_', argsIgnorePattern: '^_' }, { varsIgnorePattern: '^_', argsIgnorePattern: '^_' },
], ],
'prettier/prettier': ['error', { endOfLine: 'auto' }], 'prettier/prettier': ['error', { endOfLine: 'auto' }],
'@typescript-eslint/ban-ts-comment': 'warn',
}, },
settings: { settings: {
react: { react: {

View File

@ -1 +0,0 @@
.ltr-image-gallery-css-transition-enter{transform:translate(150%)}.ltr-image-gallery-css-transition-enter-active{transform:translate(0);transition:all .12s ease-out}.ltr-image-gallery-css-transition-exit{transform:translate(0)}.ltr-image-gallery-css-transition-exit-active{transform:translate(150%);transition:all .12s ease-out}.rtl-image-gallery-css-transition-enter{transform:translate(-150%)}.rtl-image-gallery-css-transition-enter-active{transform:translate(0);transition:all .12s ease-out}.rtl-image-gallery-css-transition-exit{transform:translate(0)}.rtl-image-gallery-css-transition-exit-active{transform:translate(-150%);transition:all .12s ease-out}.ltr-parameters-panel-transition-enter{transform:translate(-150%)}.ltr-parameters-panel-transition-enter-active{transform:translate(0);transition:all .12s ease-out}.ltr-parameters-panel-transition-exit{transform:translate(0)}.ltr-parameters-panel-transition-exit-active{transform:translate(-150%);transition:all .12s ease-out}.rtl-parameters-panel-transition-enter{transform:translate(150%)}.rtl-parameters-panel-transition-enter-active{transform:translate(0);transition:all .12s ease-out}.rtl-parameters-panel-transition-exit{transform:translate(0)}.rtl-parameters-panel-transition-exit-active{transform:translate(150%);transition:all .12s ease-out}

View File

@ -0,0 +1 @@
.ltr-image-gallery-css-transition-enter{transform:translate(150%)}.ltr-image-gallery-css-transition-enter-active{transform:translate(0);transition:all .12s ease-out}.ltr-image-gallery-css-transition-exit{transform:translate(0)}.ltr-image-gallery-css-transition-exit-active{transform:translate(150%);transition:all .12s ease-out}.rtl-image-gallery-css-transition-enter{transform:translate(-150%)}.rtl-image-gallery-css-transition-enter-active{transform:translate(0);transition:all .12s ease-out}.rtl-image-gallery-css-transition-exit{transform:translate(0)}.rtl-image-gallery-css-transition-exit-active{transform:translate(-150%);transition:all .12s ease-out}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -12,7 +12,7 @@
margin: 0; margin: 0;
} }
</style> </style>
<script type="module" crossorigin src="./assets/index-c5a5b67c.js"></script> <script type="module" crossorigin src="./assets/index-e1f916bd.js"></script>
<link rel="stylesheet" href="./assets/index-5483945c.css"> <link rel="stylesheet" href="./assets/index-5483945c.css">
</head> </head>

View File

@ -49,10 +49,11 @@
"langSimplifiedChinese": "简体中文", "langSimplifiedChinese": "简体中文",
"langUkranian": "Украї́нська", "langUkranian": "Украї́нська",
"langSpanish": "Español", "langSpanish": "Español",
"text2img": "Text To Image", "txt2img": "Text To Image",
"img2img": "Image To Image", "img2img": "Image To Image",
"unifiedCanvas": "Unified Canvas", "unifiedCanvas": "Unified Canvas",
"nodes": "Nodes", "nodes": "Nodes",
"postprocessing": "Post Processing",
"nodesDesc": "A node based system for the generation of images is under development currently. Stay tuned for updates about this amazing feature.", "nodesDesc": "A node based system for the generation of images is under development currently. Stay tuned for updates about this amazing feature.",
"postProcessing": "Post Processing", "postProcessing": "Post Processing",
"postProcessDesc1": "Invoke AI offers a wide variety of post processing features. Image Upscaling and Face Restoration are already available in the WebUI. You can access them from the Advanced Options menu of the Text To Image and Image To Image tabs. You can also process images directly, using the image action buttons above the current image display or in the viewer.", "postProcessDesc1": "Invoke AI offers a wide variety of post processing features. Image Upscaling and Face Restoration are already available in the WebUI. You can access them from the Advanced Options menu of the Text To Image and Image To Image tabs. You can also process images directly, using the image action buttons above the current image display or in the viewer.",
@ -596,7 +597,7 @@
"autoSaveToGallery": "Auto Save to Gallery", "autoSaveToGallery": "Auto Save to Gallery",
"saveBoxRegionOnly": "Save Box Region Only", "saveBoxRegionOnly": "Save Box Region Only",
"limitStrokesToBox": "Limit Strokes to Box", "limitStrokesToBox": "Limit Strokes to Box",
"showCanvasDebugInfo": "Show Canvas Debug Info", "showCanvasDebugInfo": "Show Additional Canvas Info",
"clearCanvasHistory": "Clear Canvas History", "clearCanvasHistory": "Clear Canvas History",
"clearHistory": "Clear History", "clearHistory": "Clear History",
"clearCanvasHistoryMessage": "Clearing the canvas history leaves your current canvas intact, but irreversibly clears the undo and redo history.", "clearCanvasHistoryMessage": "Clearing the canvas history leaves your current canvas intact, but irreversibly clears the undo and redo history.",

View File

@ -63,7 +63,14 @@
"back": "Atrás", "back": "Atrás",
"statusConvertingModel": "Convertir el modelo", "statusConvertingModel": "Convertir el modelo",
"statusModelConverted": "Modelo adaptado", "statusModelConverted": "Modelo adaptado",
"statusMergingModels": "Fusionar modelos" "statusMergingModels": "Fusionar modelos",
"oceanTheme": "Océano",
"langPortuguese": "Portugués",
"langKorean": "Coreano",
"langHebrew": "Hebreo",
"pinOptionsPanel": "Pin del panel de opciones",
"loading": "Cargando",
"loadingInvokeAI": "Cargando invocar a la IA"
}, },
"gallery": { "gallery": {
"generations": "Generaciones", "generations": "Generaciones",
@ -385,14 +392,19 @@
"modelMergeAlphaHelp": "Alfa controla la fuerza de mezcla de los modelos. Los valores alfa más bajos reducen la influencia del segundo modelo.", "modelMergeAlphaHelp": "Alfa controla la fuerza de mezcla de los modelos. Los valores alfa más bajos reducen la influencia del segundo modelo.",
"modelMergeInterpAddDifferenceHelp": "En este modo, el Modelo 3 se sustrae primero del Modelo 2. La versión resultante se mezcla con el Modelo 1 con la tasa alfa establecida anteriormente. La versión resultante se mezcla con el Modelo 1 con la tasa alfa establecida anteriormente.", "modelMergeInterpAddDifferenceHelp": "En este modo, el Modelo 3 se sustrae primero del Modelo 2. La versión resultante se mezcla con el Modelo 1 con la tasa alfa establecida anteriormente. La versión resultante se mezcla con el Modelo 1 con la tasa alfa establecida anteriormente.",
"ignoreMismatch": "Ignorar discrepancias entre modelos seleccionados", "ignoreMismatch": "Ignorar discrepancias entre modelos seleccionados",
"modelMergeHeaderHelp1": "Puede combinar hasta tres modelos diferentes para crear una mezcla que se adapte a sus necesidades.", "modelMergeHeaderHelp1": "Puede unir hasta tres modelos diferentes para crear una combinación que se adapte a sus necesidades.",
"inverseSigmoid": "Sigmoideo inverso", "inverseSigmoid": "Sigmoideo inverso",
"weightedSum": "Modelo de suma ponderada", "weightedSum": "Modelo de suma ponderada",
"sigmoid": "Función sigmoide", "sigmoid": "Función sigmoide",
"allModels": "Todos los modelos", "allModels": "Todos los modelos",
"repo_id": "Identificador del repositorio", "repo_id": "Identificador del repositorio",
"pathToCustomConfig": "Ruta a la configuración personalizada", "pathToCustomConfig": "Ruta a la configuración personalizada",
"customConfig": "Configuración personalizada" "customConfig": "Configuración personalizada",
"v2_base": "v2 (512px)",
"none": "ninguno",
"pickModelType": "Elige el tipo de modelo",
"v2_768": "v2 (768px)",
"addDifference": "Añadir una diferencia"
}, },
"parameters": { "parameters": {
"images": "Imágenes", "images": "Imágenes",
@ -588,5 +600,27 @@
"betaDarkenOutside": "Oscurecer fuera", "betaDarkenOutside": "Oscurecer fuera",
"betaLimitToBox": "Limitar a caja", "betaLimitToBox": "Limitar a caja",
"betaPreserveMasked": "Preservar área enmascarada" "betaPreserveMasked": "Preservar área enmascarada"
},
"accessibility": {
"invokeProgressBar": "Activar la barra de progreso",
"modelSelect": "Seleccionar modelo",
"reset": "Reiniciar",
"uploadImage": "Cargar imagen",
"previousImage": "Imagen anterior",
"nextImage": "Siguiente imagen",
"useThisParameter": "Utiliza este parámetro",
"copyMetadataJson": "Copiar los metadatos JSON",
"exitViewer": "Salir del visor",
"zoomIn": "Acercar",
"zoomOut": "Alejar",
"rotateCounterClockwise": "Girar en sentido antihorario",
"rotateClockwise": "Girar en sentido horario",
"flipHorizontally": "Voltear horizontalmente",
"flipVertically": "Voltear verticalmente",
"modifyConfig": "Modificar la configuración",
"toggleAutoscroll": "Activar el autodesplazamiento",
"toggleLogViewer": "Alternar el visor de registros",
"showGallery": "Mostrar galería",
"showOptionsPanel": "Mostrar el panel de opciones"
} }
} }

View File

@ -63,7 +63,14 @@
"langSimplifiedChinese": "Cinese semplificato", "langSimplifiedChinese": "Cinese semplificato",
"langDutch": "Olandese", "langDutch": "Olandese",
"statusModelConverted": "Modello Convertito", "statusModelConverted": "Modello Convertito",
"statusConvertingModel": "Conversione Modello" "statusConvertingModel": "Conversione Modello",
"langKorean": "Coreano",
"langPortuguese": "Portoghese",
"pinOptionsPanel": "Blocca il pannello Opzioni",
"loading": "Caricamento in corso",
"oceanTheme": "Oceano",
"langHebrew": "Ebraico",
"loadingInvokeAI": "Caricamento Invoke AI"
}, },
"gallery": { "gallery": {
"generations": "Generazioni", "generations": "Generazioni",
@ -392,7 +399,12 @@
"customSaveLocation": "Ubicazione salvataggio personalizzata", "customSaveLocation": "Ubicazione salvataggio personalizzata",
"weightedSum": "Somma pesata", "weightedSum": "Somma pesata",
"sigmoid": "Sigmoide", "sigmoid": "Sigmoide",
"inverseSigmoid": "Sigmoide inverso" "inverseSigmoid": "Sigmoide inverso",
"v2_base": "v2 (512px)",
"v2_768": "v2 (768px)",
"none": "niente",
"addDifference": "Aggiungi differenza",
"pickModelType": "Scegli il tipo di modello"
}, },
"parameters": { "parameters": {
"images": "Immagini", "images": "Immagini",
@ -588,5 +600,27 @@
"betaDarkenOutside": "Oscura all'esterno", "betaDarkenOutside": "Oscura all'esterno",
"betaLimitToBox": "Limita al rettangolo", "betaLimitToBox": "Limita al rettangolo",
"betaPreserveMasked": "Conserva quanto mascherato" "betaPreserveMasked": "Conserva quanto mascherato"
},
"accessibility": {
"modelSelect": "Seleziona modello",
"invokeProgressBar": "Barra di avanzamento generazione",
"uploadImage": "Carica immagine",
"previousImage": "Immagine precedente",
"nextImage": "Immagine successiva",
"useThisParameter": "Usa questo parametro",
"reset": "Reimposta",
"copyMetadataJson": "Copia i metadati JSON",
"exitViewer": "Esci dal visualizzatore",
"zoomIn": "Zoom avanti",
"zoomOut": "Zoom Indietro",
"rotateCounterClockwise": "Ruotare in senso antiorario",
"rotateClockwise": "Ruotare in senso orario",
"flipHorizontally": "Capovolgi orizzontalmente",
"toggleLogViewer": "Attiva/disattiva visualizzatore registro",
"showGallery": "Mostra la galleria immagini",
"showOptionsPanel": "Mostra il pannello opzioni",
"flipVertically": "Capovolgi verticalmente",
"toggleAutoscroll": "Attiva/disattiva lo scorrimento automatico",
"modifyConfig": "Modifica configurazione"
} }
} }

View File

@ -63,6 +63,560 @@
"statusGeneratingOutpainting": "Geração de Ampliação", "statusGeneratingOutpainting": "Geração de Ampliação",
"statusGenerationComplete": "Geração Completa", "statusGenerationComplete": "Geração Completa",
"statusMergingModels": "Mesclando Modelos", "statusMergingModels": "Mesclando Modelos",
"statusMergedModels": "Modelos Mesclados" "statusMergedModels": "Modelos Mesclados",
"oceanTheme": "Oceano",
"pinOptionsPanel": "Fixar painel de opções",
"loading": "A carregar",
"loadingInvokeAI": "A carregar Invoke AI",
"langPortuguese": "Português"
},
"gallery": {
"galleryImageResetSize": "Resetar Imagem",
"gallerySettings": "Configurações de Galeria",
"maintainAspectRatio": "Mater Proporções",
"autoSwitchNewImages": "Trocar para Novas Imagens Automaticamente",
"pinGallery": "Fixar Galeria",
"singleColumnLayout": "Disposição em Coluna Única",
"allImagesLoaded": "Todas as Imagens Carregadas",
"loadMore": "Carregar Mais",
"noImagesInGallery": "Sem Imagens na Galeria",
"generations": "Gerações",
"showGenerations": "Mostrar Gerações",
"uploads": "Enviados",
"showUploads": "Mostrar Enviados",
"galleryImageSize": "Tamanho da Imagem"
},
"hotkeys": {
"generalHotkeys": "Atalhos Gerais",
"galleryHotkeys": "Atalhos da Galeria",
"toggleViewer": {
"title": "Ativar Visualizador",
"desc": "Abrir e fechar o Visualizador de Imagens"
},
"maximizeWorkSpace": {
"desc": "Fechar painéis e maximixar área de trabalho",
"title": "Maximizar a Área de Trabalho"
},
"changeTabs": {
"title": "Mudar Guias",
"desc": "Trocar para outra área de trabalho"
},
"consoleToggle": {
"desc": "Abrir e fechar console",
"title": "Ativar Console"
},
"setPrompt": {
"title": "Definir Prompt",
"desc": "Usar o prompt da imagem atual"
},
"sendToImageToImage": {
"desc": "Manda a imagem atual para Imagem Para Imagem",
"title": "Mandar para Imagem Para Imagem"
},
"previousImage": {
"desc": "Mostra a imagem anterior na galeria",
"title": "Imagem Anterior"
},
"nextImage": {
"title": "Próxima Imagem",
"desc": "Mostra a próxima imagem na galeria"
},
"decreaseGalleryThumbSize": {
"desc": "Diminui o tamanho das thumbs na galeria",
"title": "Diminuir Tamanho da Galeria de Imagem"
},
"selectBrush": {
"title": "Selecionar Pincel",
"desc": "Seleciona o pincel"
},
"selectEraser": {
"title": "Selecionar Apagador",
"desc": "Seleciona o apagador"
},
"decreaseBrushSize": {
"title": "Diminuir Tamanho do Pincel",
"desc": "Diminui o tamanho do pincel/apagador"
},
"increaseBrushOpacity": {
"desc": "Aumenta a opacidade do pincel",
"title": "Aumentar Opacidade do Pincel"
},
"moveTool": {
"title": "Ferramenta Mover",
"desc": "Permite navegar pela tela"
},
"decreaseBrushOpacity": {
"desc": "Diminui a opacidade do pincel",
"title": "Diminuir Opacidade do Pincel"
},
"toggleSnap": {
"title": "Ativar Encaixe",
"desc": "Ativa Encaixar na Grade"
},
"quickToggleMove": {
"title": "Ativar Mover Rapidamente",
"desc": "Temporariamente ativa o modo Mover"
},
"toggleLayer": {
"title": "Ativar Camada",
"desc": "Ativa a seleção de camada de máscara/base"
},
"clearMask": {
"title": "Limpar Máscara",
"desc": "Limpa toda a máscara"
},
"hideMask": {
"title": "Esconder Máscara",
"desc": "Esconde e Revela a máscara"
},
"mergeVisible": {
"title": "Fundir Visível",
"desc": "Fundir todas as camadas visíveis das telas"
},
"downloadImage": {
"desc": "Descarregar a tela atual",
"title": "Descarregar Imagem"
},
"undoStroke": {
"title": "Desfazer Traço",
"desc": "Desfaz um traço de pincel"
},
"redoStroke": {
"title": "Refazer Traço",
"desc": "Refaz o traço de pincel"
},
"keyboardShortcuts": "Atalhos de Teclado",
"appHotkeys": "Atalhos do app",
"invoke": {
"title": "Invocar",
"desc": "Gerar uma imagem"
},
"cancel": {
"title": "Cancelar",
"desc": "Cancelar geração de imagem"
},
"focusPrompt": {
"title": "Foco do Prompt",
"desc": "Foco da área de texto do prompt"
},
"toggleOptions": {
"title": "Ativar Opções",
"desc": "Abrir e fechar o painel de opções"
},
"pinOptions": {
"title": "Fixar Opções",
"desc": "Fixar o painel de opções"
},
"closePanels": {
"title": "Fechar Painéis",
"desc": "Fecha os painéis abertos"
},
"unifiedCanvasHotkeys": "Atalhos da Tela Unificada",
"toggleGallery": {
"title": "Ativar Galeria",
"desc": "Abrir e fechar a gaveta da galeria"
},
"setSeed": {
"title": "Definir Seed",
"desc": "Usar seed da imagem atual"
},
"setParameters": {
"title": "Definir Parâmetros",
"desc": "Usar todos os parâmetros da imagem atual"
},
"restoreFaces": {
"title": "Restaurar Rostos",
"desc": "Restaurar a imagem atual"
},
"upscale": {
"title": "Redimensionar",
"desc": "Redimensionar a imagem atual"
},
"showInfo": {
"title": "Mostrar Informações",
"desc": "Mostrar metadados de informações da imagem atual"
},
"deleteImage": {
"title": "Apagar Imagem",
"desc": "Apaga a imagem atual"
},
"toggleGalleryPin": {
"title": "Ativar Fixar Galeria",
"desc": "Fixa e desafixa a galeria na interface"
},
"increaseGalleryThumbSize": {
"title": "Aumentar Tamanho da Galeria de Imagem",
"desc": "Aumenta o tamanho das thumbs na galeria"
},
"increaseBrushSize": {
"title": "Aumentar Tamanho do Pincel",
"desc": "Aumenta o tamanho do pincel/apagador"
},
"fillBoundingBox": {
"title": "Preencher Caixa Delimitadora",
"desc": "Preenche a caixa delimitadora com a cor do pincel"
},
"eraseBoundingBox": {
"title": "Apagar Caixa Delimitadora",
"desc": "Apaga a área da caixa delimitadora"
},
"colorPicker": {
"title": "Selecionar Seletor de Cor",
"desc": "Seleciona o seletor de cores"
},
"showHideBoundingBox": {
"title": "Mostrar/Esconder Caixa Delimitadora",
"desc": "Ativa a visibilidade da caixa delimitadora"
},
"saveToGallery": {
"title": "Gravara Na Galeria",
"desc": "Grava a tela atual na galeria"
},
"copyToClipboard": {
"title": "Copiar para a Área de Transferência",
"desc": "Copia a tela atual para a área de transferência"
},
"resetView": {
"title": "Resetar Visualização",
"desc": "Reseta Visualização da Tela"
},
"previousStagingImage": {
"title": "Imagem de Preparação Anterior",
"desc": "Área de Imagem de Preparação Anterior"
},
"nextStagingImage": {
"title": "Próxima Imagem de Preparação Anterior",
"desc": "Próxima Área de Imagem de Preparação Anterior"
},
"acceptStagingImage": {
"title": "Aceitar Imagem de Preparação Anterior",
"desc": "Aceitar Área de Imagem de Preparação Anterior"
}
},
"modelManager": {
"modelAdded": "Modelo Adicionado",
"modelUpdated": "Modelo Atualizado",
"modelEntryDeleted": "Entrada de modelo excluída",
"description": "Descrição",
"modelLocationValidationMsg": "Caminho para onde o seu modelo está localizado.",
"repo_id": "Repo ID",
"vaeRepoIDValidationMsg": "Repositório Online do seu VAE",
"width": "Largura",
"widthValidationMsg": "Largura padrão do seu modelo.",
"height": "Altura",
"heightValidationMsg": "Altura padrão do seu modelo.",
"findModels": "Encontrar Modelos",
"scanAgain": "Digitalize Novamente",
"deselectAll": "Deselecionar Tudo",
"showExisting": "Mostrar Existente",
"deleteConfig": "Apagar Config",
"convertToDiffusersHelpText6": "Deseja converter este modelo?",
"mergedModelName": "Nome do modelo mesclado",
"alpha": "Alpha",
"interpolationType": "Tipo de Interpolação",
"modelMergeHeaderHelp1": "Pode mesclar até três modelos diferentes para criar uma mistura que atenda às suas necessidades.",
"modelMergeHeaderHelp2": "Apenas Diffusers estão disponíveis para mesclagem. Se deseja mesclar um modelo de checkpoint, por favor, converta-o para Diffusers primeiro.",
"modelMergeInterpAddDifferenceHelp": "Neste modo, o Modelo 3 é primeiro subtraído do Modelo 2. A versão resultante é mesclada com o Modelo 1 com a taxa alpha definida acima.",
"nameValidationMsg": "Insira um nome para o seu modelo",
"descriptionValidationMsg": "Adicione uma descrição para o seu modelo",
"config": "Configuração",
"modelExists": "Modelo Existe",
"selectAndAdd": "Selecione e Adicione Modelos Listados Abaixo",
"noModelsFound": "Nenhum Modelo Encontrado",
"v2_768": "v2 (768px)",
"inpainting": "v1 Inpainting",
"customConfig": "Configuração personalizada",
"pathToCustomConfig": "Caminho para configuração personalizada",
"statusConverting": "A converter",
"modelConverted": "Modelo Convertido",
"ignoreMismatch": "Ignorar Divergências entre Modelos Selecionados",
"addDifference": "Adicionar diferença",
"pickModelType": "Escolha o tipo de modelo",
"safetensorModels": "SafeTensors",
"cannotUseSpaces": "Não pode usar espaços",
"addNew": "Adicionar Novo",
"addManually": "Adicionar Manualmente",
"manual": "Manual",
"name": "Nome",
"configValidationMsg": "Caminho para o ficheiro de configuração do seu modelo.",
"modelLocation": "Localização do modelo",
"repoIDValidationMsg": "Repositório Online do seu Modelo",
"updateModel": "Atualizar Modelo",
"availableModels": "Modelos Disponíveis",
"load": "Carregar",
"active": "Ativado",
"notLoaded": "Não carregado",
"deleteModel": "Apagar modelo",
"deleteMsg1": "Tem certeza de que deseja apagar esta entrada do modelo de InvokeAI?",
"deleteMsg2": "Isso não vai apagar o ficheiro de modelo checkpoint do seu disco. Pode lê-los, se desejar.",
"convertToDiffusers": "Converter para Diffusers",
"convertToDiffusersHelpText1": "Este modelo será convertido ao formato 🧨 Diffusers.",
"convertToDiffusersHelpText2": "Este processo irá substituir a sua entrada de Gestor de Modelos por uma versão Diffusers do mesmo modelo.",
"convertToDiffusersHelpText3": "O seu ficheiro de ponto de verificação no disco NÃO será excluído ou modificado de forma alguma. Pode adicionar o seu ponto de verificação ao Gestor de modelos novamente, se desejar.",
"convertToDiffusersSaveLocation": "Local para Gravar",
"v2_base": "v2 (512px)",
"mergeModels": "Mesclar modelos",
"modelOne": "Modelo 1",
"modelTwo": "Modelo 2",
"modelThree": "Modelo 3",
"mergedModelSaveLocation": "Local de Salvamento",
"merge": "Mesclar",
"modelsMerged": "Modelos mesclados",
"mergedModelCustomSaveLocation": "Caminho Personalizado",
"invokeAIFolder": "Pasta Invoke AI",
"inverseSigmoid": "Sigmóide Inversa",
"none": "nenhum",
"modelManager": "Gerente de Modelo",
"model": "Modelo",
"allModels": "Todos os Modelos",
"checkpointModels": "Checkpoints",
"diffusersModels": "Diffusers",
"addNewModel": "Adicionar Novo modelo",
"addCheckpointModel": "Adicionar Modelo de Checkpoint/Safetensor",
"addDiffuserModel": "Adicionar Diffusers",
"vaeLocation": "Localização VAE",
"vaeLocationValidationMsg": "Caminho para onde o seu VAE está localizado.",
"vaeRepoID": "VAE Repo ID",
"addModel": "Adicionar Modelo",
"search": "Procurar",
"cached": "Em cache",
"checkpointFolder": "Pasta de Checkpoint",
"clearCheckpointFolder": "Apagar Pasta de Checkpoint",
"modelsFound": "Modelos Encontrados",
"selectFolder": "Selecione a Pasta",
"selected": "Selecionada",
"selectAll": "Selecionar Tudo",
"addSelected": "Adicione Selecionado",
"delete": "Apagar",
"formMessageDiffusersModelLocation": "Localização dos Modelos Diffusers",
"formMessageDiffusersModelLocationDesc": "Por favor entre com ao menos um.",
"formMessageDiffusersVAELocation": "Localização do VAE",
"formMessageDiffusersVAELocationDesc": "Se não provido, InvokeAI irá procurar pelo ficheiro VAE dentro do local do modelo.",
"convert": "Converter",
"convertToDiffusersHelpText4": "Este é um processo único. Pode levar cerca de 30 a 60s, a depender das especificações do seu computador.",
"convertToDiffusersHelpText5": "Por favor, certifique-se de que tenha espaço suficiente no disco. Os modelos geralmente variam entre 4GB e 7GB de tamanho.",
"v1": "v1",
"sameFolder": "Mesma pasta",
"invokeRoot": "Pasta do InvokeAI",
"custom": "Personalizado",
"customSaveLocation": "Local de salvamento personalizado",
"modelMergeAlphaHelp": "Alpha controla a força da mistura dos modelos. Valores de alpha mais baixos resultam numa influência menor do segundo modelo.",
"sigmoid": "Sigmóide",
"weightedSum": "Soma Ponderada"
},
"parameters": {
"width": "Largura",
"seed": "Seed",
"hiresStrength": "Força da Alta Resolução",
"negativePrompts": "Indicações negativas",
"general": "Geral",
"randomizeSeed": "Seed Aleatório",
"shuffle": "Embaralhar",
"noiseThreshold": "Limite de Ruído",
"perlinNoise": "Ruído de Perlin",
"variations": "Variatções",
"seedWeights": "Pesos da Seed",
"restoreFaces": "Restaurar Rostos",
"faceRestoration": "Restauração de Rosto",
"type": "Tipo",
"denoisingStrength": "A força de remoção de ruído",
"scale": "Escala",
"otherOptions": "Outras Opções",
"seamlessTiling": "Ladrilho Sem Fronteira",
"hiresOptim": "Otimização de Alta Res",
"imageFit": "Caber Imagem Inicial No Tamanho de Saída",
"codeformerFidelity": "Fidelidade",
"seamSize": "Tamanho da Fronteira",
"seamBlur": "Desfoque da Fronteira",
"seamStrength": "Força da Fronteira",
"seamSteps": "Passos da Fronteira",
"tileSize": "Tamanho do Ladrilho",
"boundingBoxHeader": "Caixa Delimitadora",
"seamCorrectionHeader": "Correção de Fronteira",
"infillScalingHeader": "Preencimento e Escala",
"img2imgStrength": "Força de Imagem Para Imagem",
"toggleLoopback": "Ativar Loopback",
"symmetry": "Simetria",
"promptPlaceholder": "Digite o prompt aqui. [tokens negativos], (upweight)++, (downweight)--, trocar e misturar estão disponíveis (veja docs)",
"sendTo": "Mandar para",
"openInViewer": "Abrir No Visualizador",
"closeViewer": "Fechar Visualizador",
"usePrompt": "Usar Prompt",
"deleteImage": "Apagar Imagem",
"initialImage": "Imagem inicial",
"showOptionsPanel": "Mostrar Painel de Opções",
"strength": "Força",
"upscaling": "Redimensionando",
"upscale": "Redimensionar",
"upscaleImage": "Redimensionar Imagem",
"scaleBeforeProcessing": "Escala Antes do Processamento",
"invoke": "Invocar",
"images": "Imagems",
"steps": "Passos",
"cfgScale": "Escala CFG",
"height": "Altura",
"sampler": "Amostrador",
"imageToImage": "Imagem para Imagem",
"variationAmount": "Quntidade de Variatções",
"scaledWidth": "L Escalada",
"scaledHeight": "A Escalada",
"infillMethod": "Método de Preenchimento",
"hSymmetryStep": "H Passo de Simetria",
"vSymmetryStep": "V Passo de Simetria",
"cancel": {
"immediate": "Cancelar imediatamente",
"schedule": "Cancelar após a iteração atual",
"isScheduled": "A cancelar",
"setType": "Definir tipo de cancelamento"
},
"sendToImg2Img": "Mandar para Imagem Para Imagem",
"sendToUnifiedCanvas": "Mandar para Tela Unificada",
"copyImage": "Copiar imagem",
"copyImageToLink": "Copiar Imagem Para a Ligação",
"downloadImage": "Descarregar Imagem",
"useSeed": "Usar Seed",
"useAll": "Usar Todos",
"useInitImg": "Usar Imagem Inicial",
"info": "Informações"
},
"settings": {
"confirmOnDelete": "Confirmar Antes de Apagar",
"displayHelpIcons": "Mostrar Ícones de Ajuda",
"useCanvasBeta": "Usar Layout de Telas Beta",
"enableImageDebugging": "Ativar Depuração de Imagem",
"useSlidersForAll": "Usar deslizadores para todas as opções",
"resetWebUIDesc1": "Reiniciar a interface apenas reinicia o cache local do broswer para imagens e configurações lembradas. Não apaga nenhuma imagem do disco.",
"models": "Modelos",
"displayInProgress": "Mostrar Progresso de Imagens Em Andamento",
"saveSteps": "Gravar imagens a cada n passos",
"resetWebUI": "Reiniciar Interface",
"resetWebUIDesc2": "Se as imagens não estão a aparecer na galeria ou algo mais não está a funcionar, favor tentar reiniciar antes de postar um problema no GitHub.",
"resetComplete": "A interface foi reiniciada. Atualize a página para carregar."
},
"toast": {
"uploadFailed": "Envio Falhou",
"uploadFailedMultipleImagesDesc": "Várias imagens copiadas, só é permitido uma imagem de cada vez",
"uploadFailedUnableToLoadDesc": "Não foj possível carregar o ficheiro",
"downloadImageStarted": "Download de Imagem Começou",
"imageNotLoadedDesc": "Nenhuma imagem encontrada a enviar para o módulo de imagem para imagem",
"imageLinkCopied": "Ligação de Imagem Copiada",
"imageNotLoaded": "Nenhuma Imagem Carregada",
"parametersFailed": "Problema ao carregar parâmetros",
"parametersFailedDesc": "Não foi possível carregar imagem incial.",
"seedSet": "Seed Definida",
"upscalingFailed": "Redimensionamento Falhou",
"promptNotSet": "Prompt Não Definido",
"tempFoldersEmptied": "Pasta de Ficheiros Temporários Esvaziada",
"imageCopied": "Imagem Copiada",
"imageSavedToGallery": "Imagem Salva na Galeria",
"canvasMerged": "Tela Fundida",
"sentToImageToImage": "Mandar Para Imagem Para Imagem",
"sentToUnifiedCanvas": "Enviada para a Tela Unificada",
"parametersSet": "Parâmetros Definidos",
"parametersNotSet": "Parâmetros Não Definidos",
"parametersNotSetDesc": "Nenhum metadado foi encontrado para essa imagem.",
"seedNotSet": "Seed Não Definida",
"seedNotSetDesc": "Não foi possível achar a seed para a imagem.",
"promptSet": "Prompt Definido",
"promptNotSetDesc": "Não foi possível achar prompt para essa imagem.",
"faceRestoreFailed": "Restauração de Rosto Falhou",
"metadataLoadFailed": "Falha ao tentar carregar metadados",
"initialImageSet": "Imagem Inicial Definida",
"initialImageNotSet": "Imagem Inicial Não Definida",
"initialImageNotSetDesc": "Não foi possível carregar imagem incial"
},
"tooltip": {
"feature": {
"prompt": "Este é o campo de prompt. O prompt inclui objetos de geração e termos estilísticos. Também pode adicionar peso (importância do token) no prompt, mas comandos e parâmetros de CLI não funcionarão.",
"other": "Essas opções ativam modos alternativos de processamento para o Invoke. 'Seamless tiling' criará padrões repetidos na saída. 'High resolution' é uma geração em duas etapas com img2img: use essa configuração quando desejar uma imagem maior e mais coerente sem artefatos. Levará mais tempo do que o txt2img usual.",
"seed": "O valor da semente afeta o ruído inicial a partir do qual a imagem é formada. Pode usar as sementes já existentes de imagens anteriores. 'Limiar de ruído' é usado para mitigar artefatos em valores CFG altos (experimente a faixa de 0-10) e o Perlin para adicionar ruído Perlin durante a geração: ambos servem para adicionar variação às suas saídas.",
"imageToImage": "Image to Image carrega qualquer imagem como inicial, que é então usada para gerar uma nova junto com o prompt. Quanto maior o valor, mais a imagem resultante mudará. Valores de 0.0 a 1.0 são possíveis, a faixa recomendada é de 0.25 a 0.75",
"faceCorrection": "Correção de rosto com GFPGAN ou Codeformer: o algoritmo detecta rostos na imagem e corrige quaisquer defeitos. Um valor alto mudará mais a imagem, a resultar em rostos mais atraentes. Codeformer com uma fidelidade maior preserva a imagem original às custas de uma correção de rosto mais forte.",
"seamCorrection": "Controla o tratamento das emendas visíveis que ocorrem entre as imagens geradas no canvas.",
"gallery": "A galeria exibe as gerações da pasta de saída conforme elas são criadas. As configurações são armazenadas em ficheiros e acessadas pelo menu de contexto.",
"variations": "Experimente uma variação com um valor entre 0,1 e 1,0 para mudar o resultado para uma determinada semente. Variações interessantes da semente estão entre 0,1 e 0,3.",
"upscale": "Use o ESRGAN para ampliar a imagem imediatamente após a geração.",
"boundingBox": "A caixa delimitadora é a mesma que as configurações de largura e altura para Texto para Imagem ou Imagem para Imagem. Apenas a área na caixa será processada.",
"infillAndScaling": "Gira os métodos de preenchimento (usados em áreas mascaradas ou apagadas do canvas) e a escala (útil para tamanhos de caixa delimitadora pequenos)."
}
},
"unifiedCanvas": {
"emptyTempImagesFolderMessage": "Esvaziar a pasta de ficheiros de imagem temporários também reseta completamente a Tela Unificada. Isso inclui todo o histórico de desfazer/refazer, imagens na área de preparação e a camada base da tela.",
"scaledBoundingBox": "Caixa Delimitadora Escalada",
"boundingBoxPosition": "Posição da Caixa Delimitadora",
"next": "Próximo",
"accept": "Aceitar",
"showHide": "Mostrar/Esconder",
"discardAll": "Descartar Todos",
"betaClear": "Limpar",
"betaDarkenOutside": "Escurecer Externamente",
"base": "Base",
"brush": "Pincel",
"showIntermediates": "Mostrar Intermediários",
"showGrid": "Mostrar Grade",
"clearCanvasHistoryConfirm": "Tem certeza que quer limpar o histórico de tela?",
"boundingBox": "Caixa Delimitadora",
"canvasDimensions": "Dimensões da Tela",
"canvasPosition": "Posição da Tela",
"cursorPosition": "Posição do cursor",
"previous": "Anterior",
"betaLimitToBox": "Limitar á Caixa",
"layer": "Camada",
"mask": "Máscara",
"maskingOptions": "Opções de Mascaramento",
"enableMask": "Ativar Máscara",
"preserveMaskedArea": "Preservar Área da Máscara",
"clearMask": "Limpar Máscara",
"eraser": "Apagador",
"fillBoundingBox": "Preencher Caixa Delimitadora",
"eraseBoundingBox": "Apagar Caixa Delimitadora",
"colorPicker": "Seletor de Cor",
"brushOptions": "Opções de Pincel",
"brushSize": "Tamanho",
"move": "Mover",
"resetView": "Resetar Visualização",
"mergeVisible": "Fundir Visível",
"saveToGallery": "Gravar na Galeria",
"copyToClipboard": "Copiar para a Área de Transferência",
"downloadAsImage": "Descarregar Como Imagem",
"undo": "Desfazer",
"redo": "Refazer",
"clearCanvas": "Limpar Tela",
"canvasSettings": "Configurações de Tela",
"snapToGrid": "Encaixar na Grade",
"darkenOutsideSelection": "Escurecer Seleção Externa",
"autoSaveToGallery": "Gravar Automaticamente na Galeria",
"saveBoxRegionOnly": "Gravar Apenas a Região da Caixa",
"limitStrokesToBox": "Limitar Traços à Caixa",
"showCanvasDebugInfo": "Mostrar Informações de Depuração daTela",
"clearCanvasHistory": "Limpar o Histórico da Tela",
"clearHistory": "Limpar Históprico",
"clearCanvasHistoryMessage": "Limpar o histórico de tela deixa a sua tela atual intacta, mas limpa de forma irreversível o histórico de desfazer e refazer.",
"emptyTempImageFolder": "Esvaziar a Pasta de Ficheiros de Imagem Temporários",
"emptyFolder": "Esvaziar Pasta",
"emptyTempImagesFolderConfirm": "Tem certeza que quer esvaziar a pasta de ficheiros de imagem temporários?",
"activeLayer": "Camada Ativa",
"canvasScale": "Escala da Tela",
"betaPreserveMasked": "Preservar Máscarado"
},
"accessibility": {
"invokeProgressBar": "Invocar barra de progresso",
"reset": "Repôr",
"nextImage": "Próxima imagem",
"useThisParameter": "Usar este parâmetro",
"copyMetadataJson": "Copiar metadados JSON",
"zoomIn": "Ampliar",
"zoomOut": "Reduzir",
"rotateCounterClockwise": "Girar no sentido anti-horário",
"rotateClockwise": "Girar no sentido horário",
"flipVertically": "Espelhar verticalmente",
"modifyConfig": "Modificar config",
"toggleAutoscroll": "Alternar rolagem automática",
"showGallery": "Mostrar galeria",
"showOptionsPanel": "Mostrar painel de opções",
"uploadImage": "Enviar imagem",
"previousImage": "Imagem anterior",
"flipHorizontally": "Espelhar horizontalmente",
"toggleLogViewer": "Alternar visualizador de registo"
} }
} }

View File

@ -63,7 +63,10 @@
"statusMergingModels": "Mesclando Modelos", "statusMergingModels": "Mesclando Modelos",
"statusMergedModels": "Modelos Mesclados", "statusMergedModels": "Modelos Mesclados",
"langRussian": "Russo", "langRussian": "Russo",
"langSpanish": "Espanhol" "langSpanish": "Espanhol",
"pinOptionsPanel": "Fixar painel de opções",
"loadingInvokeAI": "Carregando Invoke AI",
"loading": "Carregando"
}, },
"gallery": { "gallery": {
"generations": "Gerações", "generations": "Gerações",

View File

@ -46,7 +46,15 @@
"statusLoadingModel": "Загрузка модели", "statusLoadingModel": "Загрузка модели",
"statusModelChanged": "Модель изменена", "statusModelChanged": "Модель изменена",
"githubLabel": "Github", "githubLabel": "Github",
"discordLabel": "Discord" "discordLabel": "Discord",
"statusMergingModels": "Слияние моделей",
"statusModelConverted": "Модель сконвертирована",
"statusMergedModels": "Модели объединены",
"pinOptionsPanel": "Закрепить панель настроек",
"loading": "Загрузка",
"loadingInvokeAI": "Загрузка Invoke AI",
"back": "Назад",
"statusConvertingModel": "Конвертация модели"
}, },
"gallery": { "gallery": {
"generations": "Генерации", "generations": "Генерации",
@ -323,7 +331,30 @@
"deleteConfig": "Удалить конфигурацию", "deleteConfig": "Удалить конфигурацию",
"deleteMsg1": "Вы точно хотите удалить модель из InvokeAI?", "deleteMsg1": "Вы точно хотите удалить модель из InvokeAI?",
"deleteMsg2": "Это не удалит файл модели с диска. Позже вы можете добавить его снова.", "deleteMsg2": "Это не удалит файл модели с диска. Позже вы можете добавить его снова.",
"repoIDValidationMsg": "Онлайн-репозиторий модели" "repoIDValidationMsg": "Онлайн-репозиторий модели",
"convertToDiffusersHelpText5": "Пожалуйста, убедитесь, что у вас достаточно места на диске. Модели обычно занимают 4 7 Гб.",
"invokeAIFolder": "Каталог InvokeAI",
"ignoreMismatch": "Игнорировать несоответствия между выбранными моделями",
"addCheckpointModel": "Добавить модель Checkpoint/Safetensor",
"formMessageDiffusersModelLocationDesc": "Укажите хотя бы одно.",
"convertToDiffusersHelpText3": "Файл модели на диске НЕ будет удалён или изменён. Вы сможете заново добавить его в Model Manager при необходимости.",
"vaeRepoID": "ID репозитория VAE",
"mergedModelName": "Название объединенной модели",
"checkpointModels": "Checkpoints",
"allModels": "Все модели",
"addDiffuserModel": "Добавить Diffusers",
"repo_id": "ID репозитория",
"formMessageDiffusersVAELocationDesc": "Если не указано, InvokeAI будет искать файл VAE рядом с моделью.",
"convert": "Преобразовать",
"convertToDiffusers": "Преобразовать в Diffusers",
"convertToDiffusersHelpText1": "Модель будет преобразована в формат 🧨 Diffusers.",
"convertToDiffusersHelpText4": "Это единоразовое действие. Оно может занять 30—60 секунд в зависимости от характеристик вашего компьютера.",
"convertToDiffusersHelpText6": "Вы хотите преобразовать эту модель?",
"statusConverting": "Преобразование",
"modelConverted": "Модель преобразована",
"invokeRoot": "Каталог InvokeAI",
"modelsMerged": "Модели объединены",
"mergeModels": "Объединить модели"
}, },
"parameters": { "parameters": {
"images": "Изображения", "images": "Изображения",
@ -503,5 +534,8 @@
"betaDarkenOutside": "Затемнить снаружи", "betaDarkenOutside": "Затемнить снаружи",
"betaLimitToBox": "Ограничить выделением", "betaLimitToBox": "Ограничить выделением",
"betaPreserveMasked": "Сохранять маскируемую область" "betaPreserveMasked": "Сохранять маскируемую область"
},
"accessibility": {
"modelSelect": "Выбор модели"
} }
} }

View File

@ -19,6 +19,21 @@
"discordLabel": "Discord", "discordLabel": "Discord",
"nodesDesc": "使用Node生成圖像的系統正在開發中。敬請期待有關於這項功能的更新。", "nodesDesc": "使用Node生成圖像的系統正在開發中。敬請期待有關於這項功能的更新。",
"reportBugLabel": "回報錯誤", "reportBugLabel": "回報錯誤",
"githubLabel": "GitHub" "githubLabel": "GitHub",
"langKorean": "韓語",
"langPortuguese": "葡萄牙語",
"hotkeysLabel": "快捷鍵",
"languagePickerLabel": "切換語言",
"langDutch": "荷蘭語",
"langFrench": "法語",
"langGerman": "德語",
"langItalian": "義大利語",
"langJapanese": "日語",
"langPolish": "波蘭語",
"langBrPortuguese": "巴西葡萄牙語",
"langRussian": "俄語",
"langSpanish": "西班牙語",
"text2img": "文字到圖像",
"unifiedCanvas": "統一畫布"
} }
} }

View File

@ -8,7 +8,7 @@
"build": "yarn run lint && vite build", "build": "yarn run lint && vite build",
"preview": "vite preview", "preview": "vite preview",
"lint:madge": "madge --circular src/main.tsx", "lint:madge": "madge --circular src/main.tsx",
"lint:eslint": "eslint --max-warnings=0", "lint:eslint": "eslint --max-warnings=0 .",
"lint:prettier": "prettier --check .", "lint:prettier": "prettier --check .",
"lint:tsc": "tsc --noEmit", "lint:tsc": "tsc --noEmit",
"lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:tsc && yarn run lint:madge", "lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:tsc && yarn run lint:madge",

View File

@ -49,10 +49,11 @@
"langSimplifiedChinese": "简体中文", "langSimplifiedChinese": "简体中文",
"langUkranian": "Украї́нська", "langUkranian": "Украї́нська",
"langSpanish": "Español", "langSpanish": "Español",
"text2img": "Text To Image", "txt2img": "Text To Image",
"img2img": "Image To Image", "img2img": "Image To Image",
"unifiedCanvas": "Unified Canvas", "unifiedCanvas": "Unified Canvas",
"nodes": "Nodes", "nodes": "Nodes",
"postprocessing": "Post Processing",
"nodesDesc": "A node based system for the generation of images is under development currently. Stay tuned for updates about this amazing feature.", "nodesDesc": "A node based system for the generation of images is under development currently. Stay tuned for updates about this amazing feature.",
"postProcessing": "Post Processing", "postProcessing": "Post Processing",
"postProcessDesc1": "Invoke AI offers a wide variety of post processing features. Image Upscaling and Face Restoration are already available in the WebUI. You can access them from the Advanced Options menu of the Text To Image and Image To Image tabs. You can also process images directly, using the image action buttons above the current image display or in the viewer.", "postProcessDesc1": "Invoke AI offers a wide variety of post processing features. Image Upscaling and Face Restoration are already available in the WebUI. You can access them from the Advanced Options menu of the Text To Image and Image To Image tabs. You can also process images directly, using the image action buttons above the current image display or in the viewer.",
@ -596,7 +597,7 @@
"autoSaveToGallery": "Auto Save to Gallery", "autoSaveToGallery": "Auto Save to Gallery",
"saveBoxRegionOnly": "Save Box Region Only", "saveBoxRegionOnly": "Save Box Region Only",
"limitStrokesToBox": "Limit Strokes to Box", "limitStrokesToBox": "Limit Strokes to Box",
"showCanvasDebugInfo": "Show Canvas Debug Info", "showCanvasDebugInfo": "Show Additional Canvas Info",
"clearCanvasHistory": "Clear Canvas History", "clearCanvasHistory": "Clear Canvas History",
"clearHistory": "Clear History", "clearHistory": "Clear History",
"clearCanvasHistoryMessage": "Clearing the canvas history leaves your current canvas intact, but irreversibly clears the undo and redo history.", "clearCanvasHistoryMessage": "Clearing the canvas history leaves your current canvas intact, but irreversibly clears the undo and redo history.",

View File

@ -9,34 +9,53 @@ import useToastWatcher from 'features/system/hooks/useToastWatcher';
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton'; import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons'; import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
import { Box, Grid } from '@chakra-ui/react'; import { Box, Flex, Grid, Portal, useColorMode } from '@chakra-ui/react';
import { APP_HEIGHT, APP_PADDING, APP_WIDTH } from 'theme/util/constants'; import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants';
import ImageGalleryPanel from 'features/gallery/components/ImageGalleryPanel';
import Lightbox from 'features/lightbox/components/Lightbox';
import { useAppSelector } from './storeHooks';
import { useEffect } from 'react';
keepGUIAlive(); keepGUIAlive();
const App = () => { const App = () => {
useToastWatcher(); useToastWatcher();
const currentTheme = useAppSelector((state) => state.ui.currentTheme);
const { setColorMode } = useColorMode();
useEffect(() => {
setColorMode(['light'].includes(currentTheme) ? 'light' : 'dark');
}, [setColorMode, currentTheme]);
return ( return (
<Grid w="100vw" h="100vh"> <Grid w="100vw" h="100vh">
<Lightbox />
<ImageUploader> <ImageUploader>
<ProgressBar /> <ProgressBar />
<Grid <Grid
gap={4} gap={4}
p={APP_PADDING} p={4}
gridAutoRows="min-content auto" gridAutoRows="min-content auto"
w={APP_WIDTH} w={APP_WIDTH}
h={APP_HEIGHT} h={APP_HEIGHT}
> >
<SiteHeader /> <SiteHeader />
<InvokeTabs /> <Flex gap={4} w="full" h="full">
<InvokeTabs />
<ImageGalleryPanel />
</Flex>
</Grid> </Grid>
<Box> <Box>
<Console /> <Console />
</Box> </Box>
</ImageUploader> </ImageUploader>
<FloatingParametersPanelButtons /> <Portal>
<FloatingGalleryButton /> <FloatingParametersPanelButtons />
</Portal>
<Portal>
<FloatingGalleryButton />
</Portal>
</Grid> </Grid>
); );
}; };

View File

@ -9,6 +9,15 @@ import { greenTeaThemeColors } from 'theme/colors/greenTea';
import { invokeAIThemeColors } from 'theme/colors/invokeAI'; import { invokeAIThemeColors } from 'theme/colors/invokeAI';
import { lightThemeColors } from 'theme/colors/lightTheme'; import { lightThemeColors } from 'theme/colors/lightTheme';
import { oceanBlueColors } from 'theme/colors/oceanBlue'; import { oceanBlueColors } from 'theme/colors/oceanBlue';
import '@fontsource/inter/100.css';
import '@fontsource/inter/200.css';
import '@fontsource/inter/300.css';
import '@fontsource/inter/400.css';
import '@fontsource/inter/500.css';
import '@fontsource/inter/600.css';
import '@fontsource/inter/700.css';
import '@fontsource/inter/800.css';
import '@fontsource/inter/900.css';
type ThemeLocaleProviderProps = { type ThemeLocaleProviderProps = {
children: ReactNode; children: ReactNode;

View File

@ -57,10 +57,13 @@ const galleryBlacklist = [
'currentImage', 'currentImage',
'currentImageUuid', 'currentImageUuid',
'shouldAutoSwitchToNewImages', 'shouldAutoSwitchToNewImages',
'shouldHoldGalleryOpen',
'intermediateImage', 'intermediateImage',
].map((blacklistItem) => `gallery.${blacklistItem}`); ].map((blacklistItem) => `gallery.${blacklistItem}`);
const lightboxBlacklist = ['isLightboxOpen'].map(
(blacklistItem) => `lightbox.${blacklistItem}`
);
const rootReducer = combineReducers({ const rootReducer = combineReducers({
generation: generationReducer, generation: generationReducer,
postprocessing: postprocessingReducer, postprocessing: postprocessingReducer,
@ -75,7 +78,12 @@ const rootPersistConfig = getPersistConfig({
key: 'root', key: 'root',
storage, storage,
rootReducer, rootReducer,
blacklist: [...canvasBlacklist, ...systemBlacklist, ...galleryBlacklist], blacklist: [
...canvasBlacklist,
...systemBlacklist,
...galleryBlacklist,
...lightboxBlacklist,
],
debounce: 300, debounce: 300,
}); });

View File

@ -1,5 +1,6 @@
import { Box, forwardRef, Icon } from '@chakra-ui/react'; import { Box, forwardRef, Icon } from '@chakra-ui/react';
import { Feature } from 'app/features'; import { Feature } from 'app/features';
import { memo } from 'react';
import { IconType } from 'react-icons'; import { IconType } from 'react-icons';
import { MdHelp } from 'react-icons/md'; import { MdHelp } from 'react-icons/md';
import GuidePopover from './GuidePopover'; import GuidePopover from './GuidePopover';
@ -19,4 +20,4 @@ const GuideIcon = forwardRef(
) )
); );
export default GuideIcon; export default memo(GuideIcon);

View File

@ -11,7 +11,7 @@ import { Feature, useFeatureHelpInfo } from 'app/features';
import { useAppSelector } from 'app/storeHooks'; import { useAppSelector } from 'app/storeHooks';
import { systemSelector } from 'features/system/store/systemSelectors'; import { systemSelector } from 'features/system/store/systemSelectors';
import { SystemState } from 'features/system/store/systemSlice'; import { SystemState } from 'features/system/store/systemSlice';
import { ReactElement } from 'react'; import { memo, ReactElement } from 'react';
type GuideProps = { type GuideProps = {
children: ReactElement; children: ReactElement;
@ -30,7 +30,7 @@ const GuidePopover = ({ children, feature }: GuideProps) => {
if (!shouldDisplayGuides) return null; if (!shouldDisplayGuides) return null;
return ( return (
<Popover trigger="hover"> <Popover trigger="hover" isLazy>
<PopoverTrigger> <PopoverTrigger>
<Box>{children}</Box> <Box>{children}</Box>
</PopoverTrigger> </PopoverTrigger>
@ -46,4 +46,4 @@ const GuidePopover = ({ children, feature }: GuideProps) => {
); );
}; };
export default GuidePopover; export default memo(GuidePopover);

View File

@ -8,7 +8,7 @@ import {
forwardRef, forwardRef,
useDisclosure, useDisclosure,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { cloneElement, ReactElement, ReactNode, useRef } from 'react'; import { cloneElement, memo, ReactElement, ReactNode, useRef } from 'react';
import IAIButton from './IAIButton'; import IAIButton from './IAIButton';
type Props = { type Props = {
@ -79,4 +79,4 @@ const IAIAlertDialog = forwardRef((props: Props, ref) => {
</> </>
); );
}); });
export default IAIAlertDialog; export default memo(IAIAlertDialog);

View File

@ -5,7 +5,7 @@ import {
Tooltip, Tooltip,
TooltipProps, TooltipProps,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { ReactNode } from 'react'; import { memo, ReactNode } from 'react';
export interface IAIButtonProps extends ButtonProps { export interface IAIButtonProps extends ButtonProps {
tooltip?: string; tooltip?: string;
@ -25,4 +25,4 @@ const IAIButton = forwardRef((props: IAIButtonProps, forwardedRef) => {
); );
}); });
export default IAIButton; export default memo(IAIButton);

View File

@ -1,5 +1,5 @@
import { Checkbox, CheckboxProps } from '@chakra-ui/react'; import { Checkbox, CheckboxProps } from '@chakra-ui/react';
import type { ReactNode } from 'react'; import { memo, ReactNode } from 'react';
type IAICheckboxProps = CheckboxProps & { type IAICheckboxProps = CheckboxProps & {
label: string | ReactNode; label: string | ReactNode;
@ -14,4 +14,4 @@ const IAICheckbox = (props: IAICheckboxProps) => {
); );
}; };
export default IAICheckbox; export default memo(IAICheckbox);

View File

@ -1,4 +1,5 @@
import { chakra, ChakraProps } from '@chakra-ui/react'; import { chakra, ChakraProps } from '@chakra-ui/react';
import { memo } from 'react';
import { RgbaColorPicker } from 'react-colorful'; import { RgbaColorPicker } from 'react-colorful';
import { ColorPickerBaseProps, RgbaColor } from 'react-colorful/dist/types'; import { ColorPickerBaseProps, RgbaColor } from 'react-colorful/dist/types';
@ -35,4 +36,4 @@ const IAIColorPicker = (props: IAIColorPickerProps) => {
); );
}; };
export default IAIColorPicker; export default memo(IAIColorPicker);

View File

@ -0,0 +1,15 @@
import { FormErrorMessage, FormErrorMessageProps } from '@chakra-ui/react';
import { ReactNode } from 'react';
type IAIFormErrorMessageProps = FormErrorMessageProps & {
children: ReactNode | string;
};
export default function IAIFormErrorMessage(props: IAIFormErrorMessageProps) {
const { children, ...rest } = props;
return (
<FormErrorMessage color="error.400" {...rest}>
{children}
</FormErrorMessage>
);
}

View File

@ -0,0 +1,15 @@
import { FormHelperText, FormHelperTextProps } from '@chakra-ui/react';
import { ReactNode } from 'react';
type IAIFormHelperTextProps = FormHelperTextProps & {
children: ReactNode | string;
};
export default function IAIFormHelperText(props: IAIFormHelperTextProps) {
const { children, ...rest } = props;
return (
<FormHelperText margin={0} color="base.400" {...rest}>
{children}
</FormHelperText>
);
}

View File

@ -5,6 +5,7 @@ import {
Tooltip, Tooltip,
TooltipProps, TooltipProps,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { memo } from 'react';
export type IAIIconButtonProps = IconButtonProps & { export type IAIIconButtonProps = IconButtonProps & {
tooltip?: string; tooltip?: string;
@ -33,4 +34,4 @@ const IAIIconButton = forwardRef((props: IAIIconButtonProps, forwardedRef) => {
); );
}); });
export default IAIIconButton; export default memo(IAIIconButton);

View File

@ -5,7 +5,7 @@ import {
Input, Input,
InputProps, InputProps,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { ChangeEvent } from 'react'; import { ChangeEvent, memo } from 'react';
interface IAIInputProps extends InputProps { interface IAIInputProps extends InputProps {
label?: string; label?: string;
@ -15,7 +15,7 @@ interface IAIInputProps extends InputProps {
formControlProps?: Omit<FormControlProps, 'isInvalid' | 'isDisabled'>; formControlProps?: Omit<FormControlProps, 'isInvalid' | 'isDisabled'>;
} }
export default function IAIInput(props: IAIInputProps) { const IAIInput = (props: IAIInputProps) => {
const { const {
label = '', label = '',
isDisabled = false, isDisabled = false,
@ -34,4 +34,6 @@ export default function IAIInput(props: IAIInputProps) {
<Input {...rest} /> <Input {...rest} />
</FormControl> </FormControl>
); );
} };
export default memo(IAIInput);

View File

@ -16,7 +16,7 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { clamp } from 'lodash'; import { clamp } from 'lodash';
import { FocusEvent, useEffect, useState } from 'react'; import { FocusEvent, memo, useEffect, useState } from 'react';
const numberStringRegex = /^-?(0\.)?\.?$/; const numberStringRegex = /^-?(0\.)?\.?$/;
@ -139,4 +139,4 @@ const IAINumberInput = (props: Props) => {
); );
}; };
export default IAINumberInput; export default memo(IAINumberInput);

View File

@ -0,0 +1,18 @@
import { useToken } from '@chakra-ui/react';
import { ReactNode } from 'react';
type IAIOptionProps = {
children: ReactNode | string | number;
value: string | number;
};
export default function IAIOption(props: IAIOptionProps) {
const { children, value } = props;
const [base800, base200] = useToken('colors', ['base.800', 'base.200']);
return (
<option value={value} style={{ background: base800, color: base200 }}>
{children}
</option>
);
}

View File

@ -6,7 +6,7 @@ import {
PopoverProps, PopoverProps,
PopoverTrigger, PopoverTrigger,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { ReactNode } from 'react'; import { memo, ReactNode } from 'react';
type IAIPopoverProps = PopoverProps & { type IAIPopoverProps = PopoverProps & {
triggerComponent: ReactNode; triggerComponent: ReactNode;
@ -35,4 +35,4 @@ const IAIPopover = (props: IAIPopoverProps) => {
); );
}; };
export default IAIPopover; export default memo(IAIPopover);

View File

@ -6,7 +6,8 @@ import {
Tooltip, Tooltip,
TooltipProps, TooltipProps,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { MouseEvent } from 'react'; import { memo, MouseEvent } from 'react';
import IAIOption from './IAIOption';
type IAISelectProps = SelectProps & { type IAISelectProps = SelectProps & {
label?: string; label?: string;
@ -37,13 +38,13 @@ const IAISelect = (props: IAISelectProps) => {
<Select {...rest}> <Select {...rest}>
{validValues.map((opt) => { {validValues.map((opt) => {
return typeof opt === 'string' || typeof opt === 'number' ? ( return typeof opt === 'string' || typeof opt === 'number' ? (
<option key={opt} value={opt}> <IAIOption key={opt} value={opt}>
{opt} {opt}
</option> </IAIOption>
) : ( ) : (
<option key={opt.value} value={opt.value}> <IAIOption key={opt.value} value={opt.value}>
{opt.key} {opt.key}
</option> </IAIOption>
); );
})} })}
</Select> </Select>
@ -52,4 +53,4 @@ const IAISelect = (props: IAISelectProps) => {
); );
}; };
export default IAISelect; export default memo(IAISelect);

View File

@ -11,7 +11,7 @@ import {
IconButtonProps, IconButtonProps,
ButtonProps, ButtonProps,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { MouseEventHandler, ReactNode } from 'react'; import { memo, MouseEventHandler, ReactNode } from 'react';
import { MdArrowDropDown, MdArrowDropUp } from 'react-icons/md'; import { MdArrowDropDown, MdArrowDropUp } from 'react-icons/md';
interface IAIMenuItem { interface IAIMenuItem {
@ -31,7 +31,7 @@ interface IAIMenuProps {
menuItemProps?: MenuItemProps; menuItemProps?: MenuItemProps;
} }
export default function IAISimpleMenu(props: IAIMenuProps) { const IAISimpleMenu = (props: IAIMenuProps) => {
const { const {
menuType = 'icon', menuType = 'icon',
iconTooltip, iconTooltip,
@ -83,4 +83,6 @@ export default function IAISimpleMenu(props: IAIMenuProps) {
)} )}
</Menu> </Menu>
); );
} };
export default memo(IAISimpleMenu);

View File

@ -25,8 +25,8 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { clamp } from 'lodash'; import { clamp } from 'lodash';
import { FocusEvent, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FocusEvent, memo, useEffect, useMemo, useState } from 'react';
import { BiReset } from 'react-icons/bi'; import { BiReset } from 'react-icons/bi';
import IAIIconButton, { IAIIconButtonProps } from './IAIIconButton'; import IAIIconButton, { IAIIconButtonProps } from './IAIIconButton';
@ -62,7 +62,7 @@ export type IAIFullSliderProps = {
sliderIAIIconButtonProps?: IAIIconButtonProps; sliderIAIIconButtonProps?: IAIIconButtonProps;
}; };
export default function IAISlider(props: IAIFullSliderProps) { const IAISlider = (props: IAIFullSliderProps) => {
const [showTooltip, setShowTooltip] = useState(false); const [showTooltip, setShowTooltip] = useState(false);
const { const {
label, label,
@ -174,16 +174,22 @@ export default function IAISlider(props: IAIFullSliderProps) {
<> <>
<SliderMark <SliderMark
value={min} value={min}
insetInlineStart={0} // insetInlineStart={0}
sx={{ insetInlineStart: 'unset !important' }} sx={{
insetInlineStart: '0 !important',
insetInlineEnd: 'unset !important',
}}
{...sliderMarkProps} {...sliderMarkProps}
> >
{min} {min}
</SliderMark> </SliderMark>
<SliderMark <SliderMark
value={max} value={max}
insetInlineEnd={0} // insetInlineEnd={0}
sx={{ insetInlineStart: 'unset !important' }} sx={{
insetInlineStart: 'unset !important',
insetInlineEnd: '0 !important',
}}
{...sliderMarkProps} {...sliderMarkProps}
> >
{max} {max}
@ -248,4 +254,6 @@ export default function IAISlider(props: IAIFullSliderProps) {
</HStack> </HStack>
</FormControl> </FormControl>
); );
} };
export default memo(IAISlider);

View File

@ -6,6 +6,7 @@ import {
Switch, Switch,
SwitchProps, SwitchProps,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { memo } from 'react';
interface Props extends SwitchProps { interface Props extends SwitchProps {
label?: string; label?: string;
@ -44,4 +45,4 @@ const IAISwitch = (props: Props) => {
); );
}; };
export default IAISwitch; export default memo(IAISwitch);

View File

@ -3,10 +3,11 @@ import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerCo
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import useImageUploader from 'common/hooks/useImageUploader'; import useImageUploader from 'common/hooks/useImageUploader';
import { uploadImage } from 'features/gallery/store/thunks/uploadImage'; import { uploadImage } from 'features/gallery/store/thunks/uploadImage';
import { tabDict } from 'features/ui/components/InvokeTabs';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { ResourceKey } from 'i18next';
import { import {
KeyboardEvent, KeyboardEvent,
memo,
ReactNode, ReactNode,
useCallback, useCallback,
useEffect, useEffect,
@ -134,7 +135,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
const overlaySecondaryText = ['img2img', 'unifiedCanvas'].includes( const overlaySecondaryText = ['img2img', 'unifiedCanvas'].includes(
activeTabName activeTabName
) )
? ` to ${tabDict[activeTabName as keyof typeof tabDict].tooltip}` ? ` to ${String(t(`common.${activeTabName}` as ResourceKey))}`
: ``; : ``;
return ( return (
@ -161,4 +162,4 @@ const ImageUploader = (props: ImageUploaderProps) => {
); );
}; };
export default ImageUploader; export default memo(ImageUploader);

View File

@ -1,5 +1,6 @@
// Grid drawing adapted from https://longviewcoder.com/2021/12/08/konva-a-better-grid/ // Grid drawing adapted from https://longviewcoder.com/2021/12/08/konva-a-better-grid/
import { useToken } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks'; import { useAppSelector } from 'app/storeHooks';
@ -22,13 +23,6 @@ const selector = createSelector(
} }
); );
const gridLinesColor = {
dark: 'rgba(255, 255, 255, 0.2)',
green: 'rgba(255, 255, 255, 0.2)',
light: 'rgba(0, 0, 0, 0.2)',
ocean: 'rgba(136, 148, 184, 0.2)',
};
const IAICanvasGrid = () => { const IAICanvasGrid = () => {
const currentTheme = useAppSelector( const currentTheme = useAppSelector(
(state: RootState) => state.ui.currentTheme (state: RootState) => state.ui.currentTheme
@ -37,6 +31,8 @@ const IAICanvasGrid = () => {
useAppSelector(selector); useAppSelector(selector);
const [gridLines, setGridLines] = useState<ReactNode[]>([]); const [gridLines, setGridLines] = useState<ReactNode[]>([]);
const [gridLineColor] = useToken('colors', ['gridLineColor']);
const unscale = useCallback( const unscale = useCallback(
(value: number) => { (value: number) => {
return value / stageScale; return value / stageScale;
@ -45,9 +41,6 @@ const IAICanvasGrid = () => {
); );
useLayoutEffect(() => { useLayoutEffect(() => {
const gridLineColor =
gridLinesColor[currentTheme as keyof typeof gridLinesColor];
const { width, height } = stageDimensions; const { width, height } = stageDimensions;
const { x, y } = stageCoordinates; const { x, y } = stageCoordinates;
@ -112,7 +105,14 @@ const IAICanvasGrid = () => {
)); ));
setGridLines(xLines.concat(yLines)); setGridLines(xLines.concat(yLines));
}, [stageScale, stageCoordinates, stageDimensions, currentTheme, unscale]); }, [
stageScale,
stageCoordinates,
stageDimensions,
currentTheme,
unscale,
gridLineColor,
]);
return <Group>{gridLines}</Group>; return <Group>{gridLines}</Group>;
}; };

View File

@ -104,7 +104,7 @@ const IAICanvasStatusText = () => {
margin: 1, margin: 1,
borderRadius: 'base', borderRadius: 'base',
pointerEvents: 'none', pointerEvents: 'none',
bg: 'blackAlpha.500', bg: 'base.800',
}} }}
> >
<Box <Box

View File

@ -0,0 +1,16 @@
import { AppDispatch, AppGetState } from 'app/store';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { debounce } from 'lodash';
import { setDoesCanvasNeedScaling } from '../canvasSlice';
const debouncedCanvasScale = debounce((dispatch: AppDispatch) => {
dispatch(setDoesCanvasNeedScaling(true));
}, 300);
export const requestCanvasRescale =
() => (dispatch: AppDispatch, getState: AppGetState) => {
const activeTabName = activeTabNameSelector(getState());
if (activeTabName === 'unifiedCanvas') {
debouncedCanvasScale(dispatch);
}
};

View File

@ -7,10 +7,7 @@ import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIButton from 'common/components/IAIButton'; import IAIButton from 'common/components/IAIButton';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import IAIPopover from 'common/components/IAIPopover'; import IAIPopover from 'common/components/IAIPopover';
import { import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
setDoesCanvasNeedScaling,
setInitialCanvasImage,
} from 'features/canvas/store/canvasSlice';
import { GalleryState } from 'features/gallery/store/gallerySlice'; import { GalleryState } from 'features/gallery/store/gallerySlice';
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors'; import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice'; import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
@ -52,6 +49,7 @@ import { gallerySelector } from '../store/gallerySelectors';
import DeleteImageModal from './DeleteImageModal'; import DeleteImageModal from './DeleteImageModal';
import { useCallback } from 'react'; import { useCallback } from 'react';
import useSetBothPrompts from 'features/parameters/hooks/usePrompt'; import useSetBothPrompts from 'features/parameters/hooks/usePrompt';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
const currentImageButtonsSelector = createSelector( const currentImageButtonsSelector = createSelector(
[ [
@ -361,7 +359,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
if (isLightboxOpen) dispatch(setIsLightboxOpen(false)); if (isLightboxOpen) dispatch(setIsLightboxOpen(false));
dispatch(setInitialCanvasImage(currentImage)); dispatch(setInitialCanvasImage(currentImage));
dispatch(setDoesCanvasNeedScaling(true)); dispatch(requestCanvasRescale());
if (activeTabName !== 'unifiedCanvas') { if (activeTabName !== 'unifiedCanvas') {
dispatch(setActiveTab('unifiedCanvas')); dispatch(setActiveTab('unifiedCanvas'));
@ -419,7 +417,6 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
sx={{ sx={{
flexDirection: 'column', flexDirection: 'column',
rowGap: 2, rowGap: 2,
w: 52,
}} }}
> >
<IAIButton <IAIButton

View File

@ -1,9 +1,10 @@
import { Flex, Image } from '@chakra-ui/react'; import { Box, Flex, Image } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/storeHooks'; import { useAppSelector } from 'app/storeHooks';
import { GalleryState } from 'features/gallery/store/gallerySlice'; import { GalleryState } from 'features/gallery/store/gallerySlice';
import { uiSelector } from 'features/ui/store/uiSelectors'; import { uiSelector } from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { APP_METADATA_HEIGHT } from 'theme/util/constants';
import { gallerySelector } from '../store/gallerySelectors'; import { gallerySelector } from '../store/gallerySelectors';
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer'; import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
@ -45,6 +46,8 @@ export default function CurrentImagePreview() {
{imageToDisplay && ( {imageToDisplay && (
<Image <Image
src={imageToDisplay.url} src={imageToDisplay.url}
width={imageToDisplay.width}
height={imageToDisplay.height}
sx={{ sx={{
objectFit: 'contain', objectFit: 'contain',
maxWidth: '100%', maxWidth: '100%',
@ -54,18 +57,23 @@ export default function CurrentImagePreview() {
imageRendering: isIntermediate ? 'pixelated' : 'initial', imageRendering: isIntermediate ? 'pixelated' : 'initial',
borderRadius: 'base', borderRadius: 'base',
}} }}
{...(isIntermediate && {
width: imageToDisplay.width,
height: imageToDisplay.height,
})}
/> />
)} )}
{!shouldShowImageDetails && <NextPrevImageButtons />} {!shouldShowImageDetails && <NextPrevImageButtons />}
{shouldShowImageDetails && imageToDisplay && ( {shouldShowImageDetails && imageToDisplay && (
<ImageMetadataViewer <Box
image={imageToDisplay} sx={{
styleClass="current-image-metadata" position: 'absolute',
/> top: '0',
width: '100%',
height: '100%',
borderRadius: 'base',
overflow: 'scroll',
maxHeight: APP_METADATA_HEIGHT,
}}
>
<ImageMetadataViewer image={imageToDisplay} />
</Box>
)} )}
</Flex> </Flex>
); );

View File

@ -0,0 +1,254 @@
import { ButtonGroup, Flex, Grid, Icon, Text } from '@chakra-ui/react';
import { requestImages } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIButton from 'common/components/IAIButton';
import IAICheckbox from 'common/components/IAICheckbox';
import IAIIconButton from 'common/components/IAIIconButton';
import IAIPopover from 'common/components/IAIPopover';
import IAISlider from 'common/components/IAISlider';
import { imageGallerySelector } from 'features/gallery/store/gallerySelectors';
import {
setCurrentCategory,
setGalleryImageMinimumWidth,
setGalleryImageObjectFit,
setShouldAutoSwitchToNewImages,
setShouldUseSingleGalleryColumn,
} from 'features/gallery/store/gallerySlice';
import { togglePinGalleryPanel } from 'features/ui/store/uiSlice';
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
import { FaImage, FaUser, FaWrench } from 'react-icons/fa';
import { MdPhotoLibrary } from 'react-icons/md';
import HoverableImage from './HoverableImage';
import Scrollable from 'features/ui/components/common/Scrollable';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290;
const ImageGalleryContent = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const resizeObserverRef = useRef<HTMLDivElement>(null);
const [shouldShouldIconButtons, setShouldShouldIconButtons] = useState(true);
const {
images,
currentCategory,
currentImageUuid,
shouldPinGallery,
galleryImageMinimumWidth,
galleryGridTemplateColumns,
galleryImageObjectFit,
shouldAutoSwitchToNewImages,
areMoreImagesAvailable,
shouldUseSingleGalleryColumn,
} = useAppSelector(imageGallerySelector);
const handleClickLoadMore = () => {
dispatch(requestImages(currentCategory));
};
const handleChangeGalleryImageMinimumWidth = (v: number) => {
dispatch(setGalleryImageMinimumWidth(v));
};
const handleSetShouldPinGallery = () => {
dispatch(togglePinGalleryPanel());
dispatch(requestCanvasRescale());
};
useEffect(() => {
if (!resizeObserverRef.current) {
return;
}
const resizeObserver = new ResizeObserver(() => {
if (!resizeObserverRef.current) {
return;
}
if (
resizeObserverRef.current.clientWidth < GALLERY_SHOW_BUTTONS_MIN_WIDTH
) {
setShouldShouldIconButtons(true);
return;
}
setShouldShouldIconButtons(false);
});
resizeObserver.observe(resizeObserverRef.current);
return () => resizeObserver.disconnect(); // clean up
}, []);
return (
<Flex flexDirection="column" w="full" h="full" gap={4}>
<Flex
ref={resizeObserverRef}
alignItems="center"
justifyContent="space-between"
>
<ButtonGroup
size="sm"
isAttached
w="max-content"
justifyContent="stretch"
>
{shouldShouldIconButtons ? (
<>
<IAIIconButton
aria-label={t('gallery.showGenerations')}
tooltip={t('gallery.showGenerations')}
isChecked={currentCategory === 'result'}
icon={<FaImage />}
onClick={() => dispatch(setCurrentCategory('result'))}
/>
<IAIIconButton
aria-label={t('gallery.showUploads')}
tooltip={t('gallery.showUploads')}
isChecked={currentCategory === 'user'}
icon={<FaUser />}
onClick={() => dispatch(setCurrentCategory('user'))}
/>
</>
) : (
<>
<IAIButton
size="sm"
isChecked={currentCategory === 'result'}
onClick={() => dispatch(setCurrentCategory('result'))}
flexGrow={1}
>
{t('gallery.generations')}
</IAIButton>
<IAIButton
size="sm"
isChecked={currentCategory === 'user'}
onClick={() => dispatch(setCurrentCategory('user'))}
flexGrow={1}
>
{t('gallery.uploads')}
</IAIButton>
</>
)}
</ButtonGroup>
<Flex gap={2}>
<IAIPopover
triggerComponent={
<IAIIconButton
size="sm"
aria-label={t('gallery.gallerySettings')}
icon={<FaWrench />}
/>
}
>
<Flex direction="column" gap={2}>
<IAISlider
value={galleryImageMinimumWidth}
onChange={handleChangeGalleryImageMinimumWidth}
min={32}
max={256}
hideTooltip={true}
label={t('gallery.galleryImageSize')}
withReset
handleReset={() => dispatch(setGalleryImageMinimumWidth(64))}
/>
<IAICheckbox
label={t('gallery.maintainAspectRatio')}
isChecked={galleryImageObjectFit === 'contain'}
onChange={() =>
dispatch(
setGalleryImageObjectFit(
galleryImageObjectFit === 'contain' ? 'cover' : 'contain'
)
)
}
/>
<IAICheckbox
label={t('gallery.autoSwitchNewImages')}
isChecked={shouldAutoSwitchToNewImages}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldAutoSwitchToNewImages(e.target.checked))
}
/>
<IAICheckbox
label={t('gallery.singleColumnLayout')}
isChecked={shouldUseSingleGalleryColumn}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldUseSingleGalleryColumn(e.target.checked))
}
/>
</Flex>
</IAIPopover>
<IAIIconButton
size="sm"
aria-label={t('gallery.pinGallery')}
tooltip={`${t('gallery.pinGallery')} (Shift+G)`}
onClick={handleSetShouldPinGallery}
icon={shouldPinGallery ? <BsPinAngleFill /> : <BsPinAngle />}
/>
</Flex>
</Flex>
<Scrollable>
<Flex direction="column" gap={2} h="full">
{images.length || areMoreImagesAvailable ? (
<>
<Grid
gap={2}
style={{ gridTemplateColumns: galleryGridTemplateColumns }}
>
{images.map((image) => {
const { uuid } = image;
const isSelected = currentImageUuid === uuid;
return (
<HoverableImage
key={uuid}
image={image}
isSelected={isSelected}
/>
);
})}
</Grid>
<IAIButton
onClick={handleClickLoadMore}
isDisabled={!areMoreImagesAvailable}
flexShrink={0}
>
{areMoreImagesAvailable
? t('gallery.loadMore')
: t('gallery.allImagesLoaded')}
</IAIButton>
</>
) : (
<Flex
sx={{
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 2,
padding: 8,
h: '100%',
w: '100%',
color: 'base.500',
}}
>
<Icon
as={MdPhotoLibrary}
sx={{
w: 16,
h: 16,
}}
/>
<Text textAlign="center">{t('gallery.noImagesInGallery')}</Text>
</Flex>
)}
</Flex>
</Scrollable>
</Flex>
);
};
export default ImageGalleryContent;

View File

@ -0,0 +1,202 @@
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
import {
selectNextImage,
selectPrevImage,
setGalleryImageMinimumWidth,
} from 'features/gallery/store/gallerySlice';
import { InvokeTabName } from 'features/ui/store/tabMap';
import { clamp, isEqual } from 'lodash';
import { useHotkeys } from 'react-hotkeys-hook';
import './ImageGallery.css';
import ImageGalleryContent from './ImageGalleryContent';
import ResizableDrawer from 'features/ui/components/common/ResizableDrawer/ResizableDrawer';
import {
setShouldShowGallery,
toggleGalleryPanel,
togglePinGalleryPanel,
} from 'features/ui/store/uiSlice';
import { createSelector } from '@reduxjs/toolkit';
import {
activeTabNameSelector,
uiSelector,
} from 'features/ui/store/uiSelectors';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
const GALLERY_TAB_WIDTHS: Record<
InvokeTabName,
{ galleryMinWidth: number; galleryMaxWidth: number }
> = {
txt2img: { galleryMinWidth: 200, galleryMaxWidth: 500 },
img2img: { galleryMinWidth: 200, galleryMaxWidth: 500 },
unifiedCanvas: { galleryMinWidth: 200, galleryMaxWidth: 200 },
nodes: { galleryMinWidth: 200, galleryMaxWidth: 500 },
postprocessing: { galleryMinWidth: 200, galleryMaxWidth: 500 },
training: { galleryMinWidth: 200, galleryMaxWidth: 500 },
};
const galleryPanelSelector = createSelector(
[
activeTabNameSelector,
uiSelector,
gallerySelector,
isStagingSelector,
lightboxSelector,
],
(activeTabName, ui, gallery, isStaging, lightbox) => {
const { shouldPinGallery, shouldShowGallery } = ui;
const { galleryImageMinimumWidth } = gallery;
const { isLightboxOpen } = lightbox;
return {
activeTabName,
isStaging,
shouldPinGallery,
shouldShowGallery,
galleryImageMinimumWidth,
isResizable: activeTabName !== 'unifiedCanvas',
isLightboxOpen,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
export default function ImageGalleryPanel() {
const dispatch = useAppDispatch();
const {
shouldPinGallery,
shouldShowGallery,
galleryImageMinimumWidth,
activeTabName,
isStaging,
isResizable,
isLightboxOpen,
} = useAppSelector(galleryPanelSelector);
const handleSetShouldPinGallery = () => {
dispatch(togglePinGalleryPanel());
dispatch(requestCanvasRescale());
};
const handleToggleGallery = () => {
dispatch(toggleGalleryPanel());
shouldPinGallery && dispatch(requestCanvasRescale());
};
const handleCloseGallery = () => {
dispatch(setShouldShowGallery(false));
shouldPinGallery && dispatch(requestCanvasRescale());
};
useHotkeys(
'g',
() => {
handleToggleGallery();
},
[shouldPinGallery]
);
useHotkeys(
'left',
() => {
dispatch(selectPrevImage());
},
{
enabled: !isStaging || activeTabName !== 'unifiedCanvas',
},
[isStaging, activeTabName]
);
useHotkeys(
'right',
() => {
dispatch(selectNextImage());
},
{
enabled: !isStaging || activeTabName !== 'unifiedCanvas',
},
[isStaging, activeTabName]
);
useHotkeys(
'shift+g',
() => {
handleSetShouldPinGallery();
},
[shouldPinGallery]
);
useHotkeys(
'esc',
() => {
dispatch(setShouldShowGallery(false));
},
{
enabled: () => !shouldPinGallery,
preventDefault: true,
},
[shouldPinGallery]
);
const IMAGE_SIZE_STEP = 32;
useHotkeys(
'shift+up',
() => {
if (galleryImageMinimumWidth < 256) {
const newMinWidth = clamp(
galleryImageMinimumWidth + IMAGE_SIZE_STEP,
32,
256
);
dispatch(setGalleryImageMinimumWidth(newMinWidth));
}
},
[galleryImageMinimumWidth]
);
useHotkeys(
'shift+down',
() => {
if (galleryImageMinimumWidth > 32) {
const newMinWidth = clamp(
galleryImageMinimumWidth - IMAGE_SIZE_STEP,
32,
256
);
dispatch(setGalleryImageMinimumWidth(newMinWidth));
}
},
[galleryImageMinimumWidth]
);
return (
<ResizableDrawer
direction="right"
isResizable={isResizable || !shouldPinGallery}
isOpen={shouldShowGallery}
onClose={handleCloseGallery}
isPinned={shouldPinGallery && !isLightboxOpen}
minWidth={
shouldPinGallery
? GALLERY_TAB_WIDTHS[activeTabName].galleryMinWidth
: 200
}
maxWidth={
shouldPinGallery
? GALLERY_TAB_WIDTHS[activeTabName].galleryMaxWidth
: undefined
}
>
<ImageGalleryContent />
</ResizableDrawer>
);
}

View File

@ -45,7 +45,6 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaCopy } from 'react-icons/fa'; import { FaCopy } from 'react-icons/fa';
import { IoArrowUndoCircleOutline } from 'react-icons/io5'; import { IoArrowUndoCircleOutline } from 'react-icons/io5';
import { APP_METADATA_HEIGHT } from 'theme/util/constants';
type MetadataItemProps = { type MetadataItemProps = {
isLink?: boolean; isLink?: boolean;
@ -115,7 +114,6 @@ const MetadataItem = ({
type ImageMetadataViewerProps = { type ImageMetadataViewerProps = {
image: InvokeAI.Image; image: InvokeAI.Image;
styleClass?: string;
}; };
// TODO: I don't know if this is needed. // TODO: I don't know if this is needed.
@ -130,345 +128,328 @@ const memoEqualityCheck = (
* Image metadata viewer overlays currently selected image and provides * Image metadata viewer overlays currently selected image and provides
* access to any of its metadata for use in processing. * access to any of its metadata for use in processing.
*/ */
const ImageMetadataViewer = memo( const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
({ image, styleClass }: ImageMetadataViewerProps) => { const dispatch = useAppDispatch();
const dispatch = useAppDispatch();
const setBothPrompts = useSetBothPrompts(); const setBothPrompts = useSetBothPrompts();
useHotkeys('esc', () => { useHotkeys('esc', () => {
dispatch(setShouldShowImageDetails(false)); dispatch(setShouldShowImageDetails(false));
}); });
const metadata = image?.metadata?.image || {}; const metadata = image?.metadata?.image || {};
const dreamPrompt = image?.dreamPrompt; const dreamPrompt = image?.dreamPrompt;
const { const {
cfg_scale, cfg_scale,
fit, fit,
height, height,
hires_fix, hires_fix,
init_image_path, init_image_path,
mask_image_path, mask_image_path,
orig_path, orig_path,
perlin, perlin,
postprocessing, postprocessing,
prompt, prompt,
sampler, sampler,
seamless, seamless,
seed, seed,
steps, steps,
strength, strength,
threshold, threshold,
type, type,
variations, variations,
width, width,
} = metadata; } = metadata;
const { t } = useTranslation(); const { t } = useTranslation();
const metadataJSON = JSON.stringify(image.metadata, null, 2); const metadataJSON = JSON.stringify(image.metadata, null, 2);
return ( return (
<Box <Flex
className={styleClass} sx={{
sx={{ padding: 4,
position: 'absolute', gap: 1,
top: '0', flexDirection: 'column',
width: '100%', width: 'full',
borderRadius: 'base', height: 'full',
padding: 4, backdropFilter: 'blur(20px)',
overflow: 'scroll', bg: 'whiteAlpha.600',
maxHeight: APP_METADATA_HEIGHT, _dark: {
height: '100%',
zIndex: '10',
backdropFilter: 'blur(10px)',
bg: 'blackAlpha.600', bg: 'blackAlpha.600',
}} },
> }}
<Flex gap={1} direction="column" width="100%"> >
<Flex gap={2}> <Flex gap={2}>
<Text fontWeight="semibold">File:</Text> <Text fontWeight="semibold">File:</Text>
<Link href={image.url} isExternal maxW="calc(100% - 3rem)"> <Link href={image.url} isExternal maxW="calc(100% - 3rem)">
{image.url.length > 64 {image.url.length > 64
? image.url.substring(0, 64).concat('...') ? image.url.substring(0, 64).concat('...')
: image.url} : image.url}
<ExternalLinkIcon mx="2px" /> <ExternalLinkIcon mx="2px" />
</Link> </Link>
</Flex> </Flex>
{Object.keys(metadata).length > 0 ? ( {Object.keys(metadata).length > 0 ? (
<> <>
{type && <MetadataItem label="Generation type" value={type} />} {type && <MetadataItem label="Generation type" value={type} />}
{image.metadata?.model_weights && ( {image.metadata?.model_weights && (
<MetadataItem <MetadataItem label="Model" value={image.metadata.model_weights} />
label="Model"
value={image.metadata.model_weights}
/>
)}
{['esrgan', 'gfpgan'].includes(type) && (
<MetadataItem label="Original image" value={orig_path} />
)}
{prompt && (
<MetadataItem
label="Prompt"
labelPosition="top"
value={
typeof prompt === 'string' ? prompt : promptToString(prompt)
}
onClick={() => setBothPrompts(prompt)}
/>
)}
{seed !== undefined && (
<MetadataItem
label="Seed"
value={seed}
onClick={() => dispatch(setSeed(seed))}
/>
)}
{threshold !== undefined && (
<MetadataItem
label="Noise Threshold"
value={threshold}
onClick={() => dispatch(setThreshold(threshold))}
/>
)}
{perlin !== undefined && (
<MetadataItem
label="Perlin Noise"
value={perlin}
onClick={() => dispatch(setPerlin(perlin))}
/>
)}
{sampler && (
<MetadataItem
label="Sampler"
value={sampler}
onClick={() => dispatch(setSampler(sampler))}
/>
)}
{steps && (
<MetadataItem
label="Steps"
value={steps}
onClick={() => dispatch(setSteps(steps))}
/>
)}
{cfg_scale !== undefined && (
<MetadataItem
label="CFG scale"
value={cfg_scale}
onClick={() => dispatch(setCfgScale(cfg_scale))}
/>
)}
{variations && variations.length > 0 && (
<MetadataItem
label="Seed-weight pairs"
value={seedWeightsToString(variations)}
onClick={() =>
dispatch(setSeedWeights(seedWeightsToString(variations)))
}
/>
)}
{seamless && (
<MetadataItem
label="Seamless"
value={seamless}
onClick={() => dispatch(setSeamless(seamless))}
/>
)}
{hires_fix && (
<MetadataItem
label="High Resolution Optimization"
value={hires_fix}
onClick={() => dispatch(setHiresFix(hires_fix))}
/>
)}
{width && (
<MetadataItem
label="Width"
value={width}
onClick={() => dispatch(setWidth(width))}
/>
)}
{height && (
<MetadataItem
label="Height"
value={height}
onClick={() => dispatch(setHeight(height))}
/>
)}
{init_image_path && (
<MetadataItem
label="Initial image"
value={init_image_path}
isLink
onClick={() => dispatch(setInitialImage(init_image_path))}
/>
)}
{mask_image_path && (
<MetadataItem
label="Mask image"
value={mask_image_path}
isLink
onClick={() => dispatch(setMaskPath(mask_image_path))}
/>
)}
{type === 'img2img' && strength && (
<MetadataItem
label="Image to image strength"
value={strength}
onClick={() => dispatch(setImg2imgStrength(strength))}
/>
)}
{fit && (
<MetadataItem
label="Image to image fit"
value={fit}
onClick={() => dispatch(setShouldFitToWidthHeight(fit))}
/>
)}
{postprocessing && postprocessing.length > 0 && (
<>
<Heading size="sm">Postprocessing</Heading>
{postprocessing.map(
(
postprocess: InvokeAI.PostProcessedImageMetadata,
i: number
) => {
if (postprocess.type === 'esrgan') {
const { scale, strength, denoise_str } = postprocess;
return (
<Flex key={i} pl={8} gap={1} direction="column">
<Text size="md">{`${
i + 1
}: Upscale (ESRGAN)`}</Text>
<MetadataItem
label="Scale"
value={scale}
onClick={() => dispatch(setUpscalingLevel(scale))}
/>
<MetadataItem
label="Strength"
value={strength}
onClick={() =>
dispatch(setUpscalingStrength(strength))
}
/>
{denoise_str !== undefined && (
<MetadataItem
label="Denoising strength"
value={denoise_str}
onClick={() =>
dispatch(setUpscalingDenoising(denoise_str))
}
/>
)}
</Flex>
);
} else if (postprocess.type === 'gfpgan') {
const { strength } = postprocess;
return (
<Flex key={i} pl={8} gap={1} direction="column">
<Text size="md">{`${
i + 1
}: Face restoration (GFPGAN)`}</Text>
<MetadataItem
label="Strength"
value={strength}
onClick={() => {
dispatch(setFacetoolStrength(strength));
dispatch(setFacetoolType('gfpgan'));
}}
/>
</Flex>
);
} else if (postprocess.type === 'codeformer') {
const { strength, fidelity } = postprocess;
return (
<Flex key={i} pl={8} gap={1} direction="column">
<Text size="md">{`${
i + 1
}: Face restoration (Codeformer)`}</Text>
<MetadataItem
label="Strength"
value={strength}
onClick={() => {
dispatch(setFacetoolStrength(strength));
dispatch(setFacetoolType('codeformer'));
}}
/>
{fidelity && (
<MetadataItem
label="Fidelity"
value={fidelity}
onClick={() => {
dispatch(setCodeformerFidelity(fidelity));
dispatch(setFacetoolType('codeformer'));
}}
/>
)}
</Flex>
);
}
}
)}
</>
)}
{dreamPrompt && (
<MetadataItem
withCopy
label="Dream Prompt"
value={dreamPrompt}
/>
)}
<Flex gap={2} direction="column">
<Flex gap={2}>
<Tooltip label="Copy metadata JSON">
<IconButton
aria-label={t('accessibility.copyMetadataJson')}
icon={<FaCopy />}
size="xs"
variant="ghost"
fontSize={14}
onClick={() =>
navigator.clipboard.writeText(metadataJSON)
}
/>
</Tooltip>
<Text fontWeight="semibold">Metadata JSON:</Text>
</Flex>
<Box
sx={{
mt: 0,
mr: 2,
mb: 4,
ml: 2,
padding: 4,
borderRadius: 'base',
overflowX: 'scroll',
wordBreak: 'break-all',
bg: 'whiteAlpha.100',
}}
>
<pre>{metadataJSON}</pre>
</Box>
</Flex>
</>
) : (
<Center width="100%" pt={10}>
<Text fontSize="lg" fontWeight="semibold">
No metadata available
</Text>
</Center>
)} )}
</Flex> {['esrgan', 'gfpgan'].includes(type) && (
</Box> <MetadataItem label="Original image" value={orig_path} />
); )}
}, {prompt && (
memoEqualityCheck <MetadataItem
); label="Prompt"
labelPosition="top"
value={
typeof prompt === 'string' ? prompt : promptToString(prompt)
}
onClick={() => setBothPrompts(prompt)}
/>
)}
{seed !== undefined && (
<MetadataItem
label="Seed"
value={seed}
onClick={() => dispatch(setSeed(seed))}
/>
)}
{threshold !== undefined && (
<MetadataItem
label="Noise Threshold"
value={threshold}
onClick={() => dispatch(setThreshold(threshold))}
/>
)}
{perlin !== undefined && (
<MetadataItem
label="Perlin Noise"
value={perlin}
onClick={() => dispatch(setPerlin(perlin))}
/>
)}
{sampler && (
<MetadataItem
label="Sampler"
value={sampler}
onClick={() => dispatch(setSampler(sampler))}
/>
)}
{steps && (
<MetadataItem
label="Steps"
value={steps}
onClick={() => dispatch(setSteps(steps))}
/>
)}
{cfg_scale !== undefined && (
<MetadataItem
label="CFG scale"
value={cfg_scale}
onClick={() => dispatch(setCfgScale(cfg_scale))}
/>
)}
{variations && variations.length > 0 && (
<MetadataItem
label="Seed-weight pairs"
value={seedWeightsToString(variations)}
onClick={() =>
dispatch(setSeedWeights(seedWeightsToString(variations)))
}
/>
)}
{seamless && (
<MetadataItem
label="Seamless"
value={seamless}
onClick={() => dispatch(setSeamless(seamless))}
/>
)}
{hires_fix && (
<MetadataItem
label="High Resolution Optimization"
value={hires_fix}
onClick={() => dispatch(setHiresFix(hires_fix))}
/>
)}
{width && (
<MetadataItem
label="Width"
value={width}
onClick={() => dispatch(setWidth(width))}
/>
)}
{height && (
<MetadataItem
label="Height"
value={height}
onClick={() => dispatch(setHeight(height))}
/>
)}
{init_image_path && (
<MetadataItem
label="Initial image"
value={init_image_path}
isLink
onClick={() => dispatch(setInitialImage(init_image_path))}
/>
)}
{mask_image_path && (
<MetadataItem
label="Mask image"
value={mask_image_path}
isLink
onClick={() => dispatch(setMaskPath(mask_image_path))}
/>
)}
{type === 'img2img' && strength && (
<MetadataItem
label="Image to image strength"
value={strength}
onClick={() => dispatch(setImg2imgStrength(strength))}
/>
)}
{fit && (
<MetadataItem
label="Image to image fit"
value={fit}
onClick={() => dispatch(setShouldFitToWidthHeight(fit))}
/>
)}
{postprocessing && postprocessing.length > 0 && (
<>
<Heading size="sm">Postprocessing</Heading>
{postprocessing.map(
(
postprocess: InvokeAI.PostProcessedImageMetadata,
i: number
) => {
if (postprocess.type === 'esrgan') {
const { scale, strength, denoise_str } = postprocess;
return (
<Flex key={i} pl={8} gap={1} direction="column">
<Text size="md">{`${i + 1}: Upscale (ESRGAN)`}</Text>
<MetadataItem
label="Scale"
value={scale}
onClick={() => dispatch(setUpscalingLevel(scale))}
/>
<MetadataItem
label="Strength"
value={strength}
onClick={() =>
dispatch(setUpscalingStrength(strength))
}
/>
{denoise_str !== undefined && (
<MetadataItem
label="Denoising strength"
value={denoise_str}
onClick={() =>
dispatch(setUpscalingDenoising(denoise_str))
}
/>
)}
</Flex>
);
} else if (postprocess.type === 'gfpgan') {
const { strength } = postprocess;
return (
<Flex key={i} pl={8} gap={1} direction="column">
<Text size="md">{`${
i + 1
}: Face restoration (GFPGAN)`}</Text>
<MetadataItem
label="Strength"
value={strength}
onClick={() => {
dispatch(setFacetoolStrength(strength));
dispatch(setFacetoolType('gfpgan'));
}}
/>
</Flex>
);
} else if (postprocess.type === 'codeformer') {
const { strength, fidelity } = postprocess;
return (
<Flex key={i} pl={8} gap={1} direction="column">
<Text size="md">{`${
i + 1
}: Face restoration (Codeformer)`}</Text>
<MetadataItem
label="Strength"
value={strength}
onClick={() => {
dispatch(setFacetoolStrength(strength));
dispatch(setFacetoolType('codeformer'));
}}
/>
{fidelity && (
<MetadataItem
label="Fidelity"
value={fidelity}
onClick={() => {
dispatch(setCodeformerFidelity(fidelity));
dispatch(setFacetoolType('codeformer'));
}}
/>
)}
</Flex>
);
}
}
)}
</>
)}
{dreamPrompt && (
<MetadataItem withCopy label="Dream Prompt" value={dreamPrompt} />
)}
<Flex gap={2} direction="column">
<Flex gap={2}>
<Tooltip label="Copy metadata JSON">
<IconButton
aria-label={t('accessibility.copyMetadataJson')}
icon={<FaCopy />}
size="xs"
variant="ghost"
fontSize={14}
onClick={() => navigator.clipboard.writeText(metadataJSON)}
/>
</Tooltip>
<Text fontWeight="semibold">Metadata JSON:</Text>
</Flex>
<Box
sx={{
mt: 0,
mr: 2,
mb: 4,
ml: 2,
padding: 4,
borderRadius: 'base',
overflowX: 'scroll',
wordBreak: 'break-all',
bg: 'whiteAlpha.500',
_dark: { bg: 'blackAlpha.500' },
}}
>
<pre>{metadataJSON}</pre>
</Box>
</Flex>
</>
) : (
<Center width="100%" pt={10}>
<Text fontSize="lg" fontWeight="semibold">
No metadata available
</Text>
</Center>
)}
</Flex>
);
}, memoEqualityCheck);
ImageMetadataViewer.displayName = 'ImageMetadataViewer'; ImageMetadataViewer.displayName = 'ImageMetadataViewer';

View File

@ -1,53 +1,47 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors'; import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
import { systemSelector } from 'features/system/store/systemSelectors'; import { systemSelector } from 'features/system/store/systemSelectors';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import {
activeTabNameSelector,
uiSelector,
} from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { GalleryState } from './gallerySlice';
export const gallerySelector = (state: RootState) => state.gallery; export const gallerySelector = (state: RootState) => state.gallery;
export const imageGallerySelector = createSelector( export const imageGallerySelector = createSelector(
[gallerySelector, lightboxSelector, isStagingSelector, activeTabNameSelector], [gallerySelector, uiSelector, lightboxSelector, activeTabNameSelector],
(gallery: GalleryState, lightbox, isStaging, activeTabName) => { (gallery, ui, lightbox, activeTabName) => {
const { const {
categories, categories,
currentCategory, currentCategory,
currentImageUuid, currentImageUuid,
shouldPinGallery,
shouldShowGallery,
galleryImageMinimumWidth, galleryImageMinimumWidth,
galleryImageObjectFit, galleryImageObjectFit,
shouldHoldGalleryOpen,
shouldAutoSwitchToNewImages, shouldAutoSwitchToNewImages,
galleryWidth, galleryWidth,
shouldUseSingleGalleryColumn, shouldUseSingleGalleryColumn,
} = gallery; } = gallery;
const { shouldPinGallery } = ui;
const { isLightboxOpen } = lightbox; const { isLightboxOpen } = lightbox;
return { return {
currentImageUuid, currentImageUuid,
shouldPinGallery, shouldPinGallery,
shouldShowGallery,
galleryImageMinimumWidth, galleryImageMinimumWidth,
galleryImageObjectFit, galleryImageObjectFit,
galleryGridTemplateColumns: shouldUseSingleGalleryColumn galleryGridTemplateColumns: shouldUseSingleGalleryColumn
? 'auto' ? 'auto'
: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`, : `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`,
activeTabName,
shouldHoldGalleryOpen,
shouldAutoSwitchToNewImages, shouldAutoSwitchToNewImages,
currentCategory,
images: categories[currentCategory].images, images: categories[currentCategory].images,
areMoreImagesAvailable: areMoreImagesAvailable:
categories[currentCategory].areMoreImagesAvailable, categories[currentCategory].areMoreImagesAvailable,
currentCategory,
galleryWidth, galleryWidth,
isLightboxOpen,
isStaging,
shouldEnableResize: shouldEnableResize:
isLightboxOpen || isLightboxOpen ||
(activeTabName === 'unifiedCanvas' && shouldPinGallery) (activeTabName === 'unifiedCanvas' && shouldPinGallery)
@ -65,7 +59,7 @@ export const imageGallerySelector = createSelector(
export const hoverableImageSelector = createSelector( export const hoverableImageSelector = createSelector(
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector], [gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector],
(gallery: GalleryState, system, lightbox, activeTabName) => { (gallery, system, lightbox, activeTabName) => {
return { return {
mayDeleteImage: system.isConnected && !system.isProcessing, mayDeleteImage: system.isConnected && !system.isProcessing,
galleryImageObjectFit: gallery.galleryImageObjectFit, galleryImageObjectFit: gallery.galleryImageObjectFit,

View File

@ -29,11 +29,8 @@ export interface GalleryState {
boundingBox?: IRect; boundingBox?: IRect;
generationMode?: InvokeTabName; generationMode?: InvokeTabName;
}; };
shouldPinGallery: boolean;
shouldShowGallery: boolean;
galleryImageMinimumWidth: number; galleryImageMinimumWidth: number;
galleryImageObjectFit: GalleryImageObjectFitType; galleryImageObjectFit: GalleryImageObjectFitType;
shouldHoldGalleryOpen: boolean;
shouldAutoSwitchToNewImages: boolean; shouldAutoSwitchToNewImages: boolean;
categories: { categories: {
user: Gallery; user: Gallery;
@ -46,11 +43,8 @@ export interface GalleryState {
const initialState: GalleryState = { const initialState: GalleryState = {
currentImageUuid: '', currentImageUuid: '',
shouldPinGallery: true,
shouldShowGallery: true,
galleryImageMinimumWidth: 64, galleryImageMinimumWidth: 64,
galleryImageObjectFit: 'cover', galleryImageObjectFit: 'cover',
shouldHoldGalleryOpen: false,
shouldAutoSwitchToNewImages: true, shouldAutoSwitchToNewImages: true,
currentCategory: 'result', currentCategory: 'result',
categories: { categories: {
@ -233,13 +227,6 @@ export const gallerySlice = createSlice({
areMoreImagesAvailable; areMoreImagesAvailable;
} }
}, },
setShouldPinGallery: (state, action: PayloadAction<boolean>) => {
state.shouldPinGallery = action.payload;
},
setShouldShowGallery: (state, action: PayloadAction<boolean>) => {
state.shouldShowGallery = action.payload;
},
setGalleryImageMinimumWidth: (state, action: PayloadAction<number>) => { setGalleryImageMinimumWidth: (state, action: PayloadAction<number>) => {
state.galleryImageMinimumWidth = action.payload; state.galleryImageMinimumWidth = action.payload;
}, },
@ -249,9 +236,6 @@ export const gallerySlice = createSlice({
) => { ) => {
state.galleryImageObjectFit = action.payload; state.galleryImageObjectFit = action.payload;
}, },
setShouldHoldGalleryOpen: (state, action: PayloadAction<boolean>) => {
state.shouldHoldGalleryOpen = action.payload;
},
setShouldAutoSwitchToNewImages: (state, action: PayloadAction<boolean>) => { setShouldAutoSwitchToNewImages: (state, action: PayloadAction<boolean>) => {
state.shouldAutoSwitchToNewImages = action.payload; state.shouldAutoSwitchToNewImages = action.payload;
}, },
@ -279,11 +263,8 @@ export const {
setIntermediateImage, setIntermediateImage,
selectNextImage, selectNextImage,
selectPrevImage, selectPrevImage,
setShouldPinGallery,
setShouldShowGallery,
setGalleryImageMinimumWidth, setGalleryImageMinimumWidth,
setGalleryImageObjectFit, setGalleryImageObjectFit,
setShouldHoldGalleryOpen,
setShouldAutoSwitchToNewImages, setShouldAutoSwitchToNewImages,
setCurrentCategory, setCurrentCategory,
setGalleryWidth, setGalleryWidth,

View File

@ -1,20 +1,20 @@
import { Box, Flex, Grid } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import CurrentImageButtons from 'features/gallery/components/CurrentImageButtons'; import CurrentImageButtons from 'features/gallery/components/CurrentImageButtons';
import ImageGallery from 'features/gallery/components/ImageGallery';
import ImageMetadataViewer from 'features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer'; import ImageMetadataViewer from 'features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer';
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons'; import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
import { gallerySelector } from 'features/gallery/store/gallerySelectors'; import { gallerySelector } from 'features/gallery/store/gallerySelectors';
import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice'; import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
import { uiSelector } from 'features/ui/store/uiSelectors'; import { uiSelector } from 'features/ui/store/uiSelectors';
import { AnimatePresence, motion } from 'framer-motion';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { BiExit } from 'react-icons/bi'; import { BiExit } from 'react-icons/bi';
import { TransformWrapper } from 'react-zoom-pan-pinch'; import { TransformWrapper } from 'react-zoom-pan-pinch';
import { PROGRESS_BAR_THICKNESS } from 'theme/util/constants';
import useImageTransform from '../hooks/useImageTransform'; import useImageTransform from '../hooks/useImageTransform';
import ReactPanZoomButtons from './ReactPanZoomButtons'; import ReactPanZoomButtons from './ReactPanZoomButtons';
import ReactPanZoomImage from './ReactPanZoomImage'; import ReactPanZoomImage from './ReactPanZoomImage';
@ -39,7 +39,6 @@ export const lightboxSelector = createSelector(
export default function Lightbox() { export default function Lightbox() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation();
const isLightBoxOpen = useAppSelector( const isLightBoxOpen = useAppSelector(
(state: RootState) => state.lightbox.isLightboxOpen (state: RootState) => state.lightbox.isLightboxOpen
); );
@ -67,106 +66,102 @@ export default function Lightbox() {
); );
return ( return (
<TransformWrapper <AnimatePresence>
centerOnInit {isLightBoxOpen && (
minScale={0.1} <motion.div
initialPositionX={50} key="lightbox"
initialPositionY={50} initial={{ opacity: 0 }}
> animate={{ opacity: 1 }}
<Box exit={{ opacity: 0 }}
sx={{ transition={{ duration: 0.15, ease: 'easeInOut' }}
width: '100%', style={{
height: '100%', display: 'flex',
overflow: 'hidden', width: '100vw',
position: 'absolute', height: `calc(100vh - ${PROGRESS_BAR_THICKNESS * 4}px)`,
insetInlineStart: 0, position: 'fixed',
top: 0, top: `${PROGRESS_BAR_THICKNESS * 4}px`,
zIndex: 30, background: 'var(--invokeai-colors-base-900)',
animation: 'popIn 0.3s ease-in', zIndex: 99,
bg: 'base.800',
}}
>
<Flex
sx={{
flexDir: 'column',
position: 'absolute',
top: 4,
insetInlineStart: 4,
gap: 4,
zIndex: 3,
}} }}
> >
<IAIIconButton <TransformWrapper
icon={<BiExit />} centerOnInit
aria-label={t('accessibility.exitViewer')} minScale={0.1}
onClick={() => { initialPositionX={50}
dispatch(setIsLightboxOpen(false)); initialPositionY={50}
}}
fontSize={20}
/>
<ReactPanZoomButtons
flipHorizontally={flipHorizontally}
flipVertically={flipVertically}
rotateCounterClockwise={rotateCounterClockwise}
rotateClockwise={rotateClockwise}
reset={reset}
/>
</Flex>
<Flex>
<Grid
sx={{
overflow: 'hidden',
gridTemplateColumns: 'auto max-content',
placeItems: 'center',
width: '100vw',
height: '100vh',
bg: 'base.850',
}}
> >
<Flex
sx={{
flexDir: 'column',
position: 'absolute',
insetInlineStart: 4,
gap: 4,
zIndex: 3,
top: 4,
}}
>
<IAIIconButton
icon={<BiExit />}
aria-label="Exit Viewer"
className="lightbox-close-btn"
onClick={() => {
dispatch(setIsLightboxOpen(false));
}}
fontSize={20}
/>
<ReactPanZoomButtons
flipHorizontally={flipHorizontally}
flipVertically={flipVertically}
rotateCounterClockwise={rotateCounterClockwise}
rotateClockwise={rotateClockwise}
reset={reset}
/>
</Flex>
<Flex
sx={{
position: 'absolute',
top: 4,
zIndex: 3,
insetInlineStart: '50%',
transform: 'translate(-50%, 0)',
}}
>
<CurrentImageButtons />
</Flex>
{viewerImageToDisplay && ( {viewerImageToDisplay && (
<> <>
<ReactPanZoomImage <ReactPanZoomImage
rotation={rotation} rotation={rotation}
scaleX={scaleX} scaleX={scaleX}
scaleY={scaleY} scaleY={scaleY}
image={viewerImageToDisplay.url} image={viewerImageToDisplay}
styleClass="lightbox-image" styleClass="lightbox-image"
/> />
{shouldShowImageDetails && ( {shouldShowImageDetails && (
<ImageMetadataViewer image={viewerImageToDisplay} /> <ImageMetadataViewer image={viewerImageToDisplay} />
)} )}
{!shouldShowImageDetails && (
<Box
sx={{
position: 'absolute',
top: 0,
insetInlineStart: 0,
w: '100vw',
h: '100vh',
px: 16,
pointerEvents: 'none',
}}
>
<NextPrevImageButtons />
</Box>
)}
</> </>
)} )}
</TransformWrapper>
{!shouldShowImageDetails && ( </motion.div>
<Box )}
sx={{ </AnimatePresence>
position: 'absolute',
top: 0,
insetInlineStart: 0,
w: `calc(100vw - ${8 * 2 * 4}px)`,
h: '100vh',
mx: 8,
pointerEvents: 'none',
}}
>
<NextPrevImageButtons />
</Box>
)}
<Box
sx={{
position: 'absolute',
top: 4,
}}
>
<CurrentImageButtons />
</Box>
</Grid>
<ImageGallery />
</Flex>
</Box>
</TransformWrapper>
); );
} }

View File

@ -1,8 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import { TransformComponent, useTransformContext } from 'react-zoom-pan-pinch'; import { TransformComponent, useTransformContext } from 'react-zoom-pan-pinch';
import * as InvokeAI from 'app/invokeai';
type ReactPanZoomProps = { type ReactPanZoomProps = {
image: string; image: InvokeAI.Image;
styleClass?: string; styleClass?: string;
alt?: string; alt?: string;
ref?: React.Ref<HTMLImageElement>; ref?: React.Ref<HTMLImageElement>;
@ -34,7 +35,7 @@ export default function ReactPanZoomImage({
transform: `rotate(${rotation}deg) scaleX(${scaleX}) scaleY(${scaleY})`, transform: `rotate(${rotation}deg) scaleX(${scaleX}) scaleY(${scaleY})`,
width: '100%', width: '100%',
}} }}
src={image} src={image.url}
alt={alt} alt={alt}
ref={ref} ref={ref}
className={styleClass ? styleClass : ''} className={styleClass ? styleClass : ''}

View File

@ -59,6 +59,11 @@ const ParametersAccordion = (props: ParametersAccordionsType) => {
allowMultiple allowMultiple
reduceMotion reduceMotion
onChange={handleChangeAccordionState} onChange={handleChangeAccordionState}
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
}}
> >
{renderAccordions()} {renderAccordions()}
</Accordion> </Accordion>

View File

@ -11,7 +11,7 @@ import {
setCancelType, setCancelType,
} from 'features/system/store/systemSlice'; } from 'features/system/store/systemSlice';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { useEffect, useCallback } from 'react'; import { useEffect, useCallback, memo } from 'react';
import { ButtonSpinner, ButtonGroup } from '@chakra-ui/react'; import { ButtonSpinner, ButtonGroup } from '@chakra-ui/react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
@ -44,9 +44,9 @@ interface CancelButtonProps {
btnGroupWidth?: string | number; btnGroupWidth?: string | number;
} }
export default function CancelButton( const CancelButton = (
props: CancelButtonProps & Omit<IAIIconButtonProps, 'aria-label'> props: CancelButtonProps & Omit<IAIIconButtonProps, 'aria-label'>
) { ) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { btnGroupWidth = 'auto', ...rest } = props; const { btnGroupWidth = 'auto', ...rest } = props;
const { const {
@ -146,4 +146,6 @@ export default function CancelButton(
/> />
</ButtonGroup> </ButtonGroup>
); );
} };
export default memo(CancelButton);

View File

@ -329,69 +329,71 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
<ModalCloseButton /> <ModalCloseButton />
<ModalBody> <ModalBody>
<Accordion allowMultiple> <Accordion allowMultiple>
<AccordionItem> <Flex flexDir="column" gap={2}>
<AccordionButton> <AccordionItem>
<Flex <AccordionButton>
width="100%" <Flex
justifyContent="space-between" width="100%"
alignItems="center" justifyContent="space-between"
> alignItems="center"
<h2>{t('hotkeys.appHotkeys')}</h2> >
<AccordionIcon /> <h2>{t('hotkeys.appHotkeys')}</h2>
</Flex> <AccordionIcon />
</AccordionButton> </Flex>
<AccordionPanel> </AccordionButton>
{renderHotkeyModalItems(appHotkeys)} <AccordionPanel>
</AccordionPanel> {renderHotkeyModalItems(appHotkeys)}
</AccordionItem> </AccordionPanel>
</AccordionItem>
<AccordionItem> <AccordionItem>
<AccordionButton> <AccordionButton>
<Flex <Flex
width="100%" width="100%"
justifyContent="space-between" justifyContent="space-between"
alignItems="center" alignItems="center"
> >
<h2>{t('hotkeys.generalHotkeys')}</h2> <h2>{t('hotkeys.generalHotkeys')}</h2>
<AccordionIcon /> <AccordionIcon />
</Flex> </Flex>
</AccordionButton> </AccordionButton>
<AccordionPanel> <AccordionPanel>
{renderHotkeyModalItems(generalHotkeys)} {renderHotkeyModalItems(generalHotkeys)}
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
<AccordionItem> <AccordionItem>
<AccordionButton> <AccordionButton>
<Flex <Flex
width="100%" width="100%"
justifyContent="space-between" justifyContent="space-between"
alignItems="center" alignItems="center"
> >
<h2>{t('hotkeys.galleryHotkeys')}</h2> <h2>{t('hotkeys.galleryHotkeys')}</h2>
<AccordionIcon /> <AccordionIcon />
</Flex> </Flex>
</AccordionButton> </AccordionButton>
<AccordionPanel> <AccordionPanel>
{renderHotkeyModalItems(galleryHotkeys)} {renderHotkeyModalItems(galleryHotkeys)}
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
<AccordionItem> <AccordionItem>
<AccordionButton> <AccordionButton>
<Flex <Flex
width="100%" width="100%"
justifyContent="space-between" justifyContent="space-between"
alignItems="center" alignItems="center"
> >
<h2>{t('hotkeys.unifiedCanvasHotkeys')}</h2> <h2>{t('hotkeys.unifiedCanvasHotkeys')}</h2>
<AccordionIcon /> <AccordionIcon />
</Flex> </Flex>
</AccordionButton> </AccordionButton>
<AccordionPanel> <AccordionPanel>
{renderHotkeyModalItems(unifiedCanvasHotkeys)} {renderHotkeyModalItems(unifiedCanvasHotkeys)}
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
</Flex>
</Accordion> </Accordion>
</ModalBody> </ModalBody>
<ModalFooter /> <ModalFooter />

View File

@ -11,8 +11,6 @@ import { systemSelector } from 'features/system/store/systemSelectors';
import { import {
Flex, Flex,
FormControl, FormControl,
FormErrorMessage,
FormHelperText,
FormLabel, FormLabel,
HStack, HStack,
Text, Text,
@ -28,6 +26,8 @@ import type { RootState } from 'app/store';
import type { FieldInputProps, FormikProps } from 'formik'; import type { FieldInputProps, FormikProps } from 'formik';
import { isEqual, pickBy } from 'lodash'; import { isEqual, pickBy } from 'lodash';
import ModelConvert from './ModelConvert'; import ModelConvert from './ModelConvert';
import IAIFormHelperText from 'common/components/IAIForms/IAIFormHelperText';
import IAIFormErrorMessage from 'common/components/IAIForms/IAIFormErrorMessage';
const selector = createSelector( const selector = createSelector(
[systemSelector], [systemSelector],
@ -139,11 +139,13 @@ export default function CheckpointModelEdit() {
width="full" width="full"
/> />
{!!errors.description && touched.description ? ( {!!errors.description && touched.description ? (
<FormErrorMessage>{errors.description}</FormErrorMessage> <IAIFormErrorMessage>
{errors.description}
</IAIFormErrorMessage>
) : ( ) : (
<FormHelperText margin={0}> <IAIFormHelperText>
{t('modelManager.descriptionValidationMsg')} {t('modelManager.descriptionValidationMsg')}
</FormHelperText> </IAIFormHelperText>
)} )}
</VStack> </VStack>
</FormControl> </FormControl>
@ -165,11 +167,11 @@ export default function CheckpointModelEdit() {
width="full" width="full"
/> />
{!!errors.config && touched.config ? ( {!!errors.config && touched.config ? (
<FormErrorMessage>{errors.config}</FormErrorMessage> <IAIFormErrorMessage>{errors.config}</IAIFormErrorMessage>
) : ( ) : (
<FormHelperText margin={0}> <IAIFormHelperText>
{t('modelManager.configValidationMsg')} {t('modelManager.configValidationMsg')}
</FormHelperText> </IAIFormHelperText>
)} )}
</VStack> </VStack>
</FormControl> </FormControl>
@ -191,11 +193,13 @@ export default function CheckpointModelEdit() {
width="full" width="full"
/> />
{!!errors.weights && touched.weights ? ( {!!errors.weights && touched.weights ? (
<FormErrorMessage>{errors.weights}</FormErrorMessage> <IAIFormErrorMessage>
{errors.weights}
</IAIFormErrorMessage>
) : ( ) : (
<FormHelperText margin={0}> <IAIFormHelperText>
{t('modelManager.modelLocationValidationMsg')} {t('modelManager.modelLocationValidationMsg')}
</FormHelperText> </IAIFormHelperText>
)} )}
</VStack> </VStack>
</FormControl> </FormControl>
@ -214,11 +218,11 @@ export default function CheckpointModelEdit() {
width="full" width="full"
/> />
{!!errors.vae && touched.vae ? ( {!!errors.vae && touched.vae ? (
<FormErrorMessage>{errors.vae}</FormErrorMessage> <IAIFormErrorMessage>{errors.vae}</IAIFormErrorMessage>
) : ( ) : (
<FormHelperText margin={0}> <IAIFormHelperText>
{t('modelManager.vaeLocationValidationMsg')} {t('modelManager.vaeLocationValidationMsg')}
</FormHelperText> </IAIFormHelperText>
)} )}
</VStack> </VStack>
</FormControl> </FormControl>
@ -253,11 +257,13 @@ export default function CheckpointModelEdit() {
</Field> </Field>
{!!errors.width && touched.width ? ( {!!errors.width && touched.width ? (
<FormErrorMessage>{errors.width}</FormErrorMessage> <IAIFormErrorMessage>
{errors.width}
</IAIFormErrorMessage>
) : ( ) : (
<FormHelperText margin={0}> <IAIFormHelperText>
{t('modelManager.widthValidationMsg')} {t('modelManager.widthValidationMsg')}
</FormHelperText> </IAIFormHelperText>
)} )}
</VStack> </VStack>
</FormControl> </FormControl>
@ -291,11 +297,13 @@ export default function CheckpointModelEdit() {
</Field> </Field>
{!!errors.height && touched.height ? ( {!!errors.height && touched.height ? (
<FormErrorMessage>{errors.height}</FormErrorMessage> <IAIFormErrorMessage>
{errors.height}
</IAIFormErrorMessage>
) : ( ) : (
<FormHelperText margin={0}> <IAIFormHelperText>
{t('modelManager.heightValidationMsg')} {t('modelManager.heightValidationMsg')}
</FormHelperText> </IAIFormHelperText>
)} )}
</VStack> </VStack>
</FormControl> </FormControl>

View File

@ -7,15 +7,7 @@ import { useEffect, useState } from 'react';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { systemSelector } from 'features/system/store/systemSelectors'; import { systemSelector } from 'features/system/store/systemSelectors';
import { import { Flex, FormControl, FormLabel, Text, VStack } from '@chakra-ui/react';
Flex,
FormControl,
FormErrorMessage,
FormHelperText,
FormLabel,
Text,
VStack,
} from '@chakra-ui/react';
import { addNewModel } from 'app/socketio/actions'; import { addNewModel } from 'app/socketio/actions';
import { Field, Formik } from 'formik'; import { Field, Formik } from 'formik';
@ -24,6 +16,8 @@ import { useTranslation } from 'react-i18next';
import type { InvokeDiffusersModelConfigProps } from 'app/invokeai'; import type { InvokeDiffusersModelConfigProps } from 'app/invokeai';
import type { RootState } from 'app/store'; import type { RootState } from 'app/store';
import { isEqual, pickBy } from 'lodash'; import { isEqual, pickBy } from 'lodash';
import IAIFormHelperText from 'common/components/IAIForms/IAIFormHelperText';
import IAIFormErrorMessage from 'common/components/IAIForms/IAIFormErrorMessage';
const selector = createSelector( const selector = createSelector(
[systemSelector], [systemSelector],
@ -141,11 +135,13 @@ export default function DiffusersModelEdit() {
width="full" width="full"
/> />
{!!errors.description && touched.description ? ( {!!errors.description && touched.description ? (
<FormErrorMessage>{errors.description}</FormErrorMessage> <IAIFormErrorMessage>
{errors.description}
</IAIFormErrorMessage>
) : ( ) : (
<FormHelperText margin={0}> <IAIFormHelperText>
{t('modelManager.descriptionValidationMsg')} {t('modelManager.descriptionValidationMsg')}
</FormHelperText> </IAIFormHelperText>
)} )}
</VStack> </VStack>
</FormControl> </FormControl>
@ -167,11 +163,11 @@ export default function DiffusersModelEdit() {
width="full" width="full"
/> />
{!!errors.path && touched.path ? ( {!!errors.path && touched.path ? (
<FormErrorMessage>{errors.path}</FormErrorMessage> <IAIFormErrorMessage>{errors.path}</IAIFormErrorMessage>
) : ( ) : (
<FormHelperText margin={0}> <IAIFormHelperText>
{t('modelManager.modelLocationValidationMsg')} {t('modelManager.modelLocationValidationMsg')}
</FormHelperText> </IAIFormHelperText>
)} )}
</VStack> </VStack>
</FormControl> </FormControl>
@ -190,11 +186,13 @@ export default function DiffusersModelEdit() {
width="full" width="full"
/> />
{!!errors.repo_id && touched.repo_id ? ( {!!errors.repo_id && touched.repo_id ? (
<FormErrorMessage>{errors.repo_id}</FormErrorMessage> <IAIFormErrorMessage>
{errors.repo_id}
</IAIFormErrorMessage>
) : ( ) : (
<FormHelperText margin={0}> <IAIFormHelperText>
{t('modelManager.repoIDValidationMsg')} {t('modelManager.repoIDValidationMsg')}
</FormHelperText> </IAIFormHelperText>
)} )}
</VStack> </VStack>
</FormControl> </FormControl>
@ -215,11 +213,13 @@ export default function DiffusersModelEdit() {
width="full" width="full"
/> />
{!!errors.vae?.path && touched.vae?.path ? ( {!!errors.vae?.path && touched.vae?.path ? (
<FormErrorMessage>{errors.vae?.path}</FormErrorMessage> <IAIFormErrorMessage>
{errors.vae?.path}
</IAIFormErrorMessage>
) : ( ) : (
<FormHelperText margin={0}> <IAIFormHelperText>
{t('modelManager.vaeLocationValidationMsg')} {t('modelManager.vaeLocationValidationMsg')}
</FormHelperText> </IAIFormHelperText>
)} )}
</VStack> </VStack>
</FormControl> </FormControl>
@ -240,11 +240,13 @@ export default function DiffusersModelEdit() {
width="full" width="full"
/> />
{!!errors.vae?.repo_id && touched.vae?.repo_id ? ( {!!errors.vae?.repo_id && touched.vae?.repo_id ? (
<FormErrorMessage>{errors.vae?.repo_id}</FormErrorMessage> <IAIFormErrorMessage>
{errors.vae?.repo_id}
</IAIFormErrorMessage>
) : ( ) : (
<FormHelperText margin={0}> <IAIFormHelperText>
{t('modelManager.vaeRepoIDValidationMsg')} {t('modelManager.vaeRepoIDValidationMsg')}
</FormHelperText> </IAIFormHelperText>
)} )}
</VStack> </VStack>
</FormControl> </FormControl>

View File

@ -34,9 +34,6 @@ export default function ThemeChanger() {
Object.keys(THEMES).forEach((theme) => { Object.keys(THEMES).forEach((theme) => {
themesToRender.push( themesToRender.push(
<IAIButton <IAIButton
sx={{
width: 24,
}}
isChecked={currentTheme === theme} isChecked={currentTheme === theme}
leftIcon={currentTheme === theme ? <FaCheck /> : undefined} leftIcon={currentTheme === theme ? <FaCheck /> : undefined}
size="sm" size="sm"

View File

@ -1,22 +1,38 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import { setDoesCanvasNeedScaling } from 'features/canvas/store/canvasSlice';
import { setShouldShowGallery } from 'features/gallery/store/gallerySlice';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import { setShouldShowGallery } from 'features/ui/store/uiSlice';
import { isEqual } from 'lodash';
import { MdPhotoLibrary } from 'react-icons/md'; import { MdPhotoLibrary } from 'react-icons/md';
import { floatingSelector } from './FloatingParametersPanelButtons'; import { activeTabNameSelector, uiSelector } from '../store/uiSelectors';
const floatingGalleryButtonSelector = createSelector(
[activeTabNameSelector, uiSelector],
(activeTabName, ui) => {
const { shouldPinGallery, shouldShowGallery } = ui;
return {
shouldPinGallery,
shouldShowGalleryButton:
(!shouldPinGallery || !shouldShowGallery) &&
['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName),
};
},
{ memoizeOptions: { resultEqualityCheck: isEqual } }
);
const FloatingGalleryButton = () => { const FloatingGalleryButton = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { shouldPinGallery, shouldShowGalleryButton } = useAppSelector(
floatingGalleryButtonSelector
);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { shouldShowGalleryButton, shouldPinGallery } =
useAppSelector(floatingSelector);
const handleShowGallery = () => { const handleShowGallery = () => {
dispatch(setShouldShowGallery(true)); dispatch(setShouldShowGallery(true));
if (shouldPinGallery) { shouldPinGallery && dispatch(requestCanvasRescale());
dispatch(setDoesCanvasNeedScaling(true));
}
}; };
return shouldShowGalleryButton ? ( return shouldShowGalleryButton ? (

View File

@ -2,9 +2,7 @@ import { ChakraProps, Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import { setDoesCanvasNeedScaling } from 'features/canvas/store/canvasSlice'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
import { GalleryState } from 'features/gallery/store/gallerySlice';
import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton'; import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton';
import InvokeButton from 'features/parameters/components/ProcessButtons/InvokeButton'; import InvokeButton from 'features/parameters/components/ProcessButtons/InvokeButton';
import { import {
@ -22,46 +20,31 @@ const floatingButtonStyles: ChakraProps['sx'] = {
borderEndStartRadius: 0, borderEndStartRadius: 0,
}; };
export const floatingSelector = createSelector( export const floatingParametersPanelButtonSelector = createSelector(
[gallerySelector, uiSelector, activeTabNameSelector], [uiSelector, activeTabNameSelector],
(gallery: GalleryState, ui, activeTabName) => { (ui, activeTabName) => {
const { const {
shouldPinParametersPanel, shouldPinParametersPanel,
shouldShowParametersPanel,
shouldHoldParametersPanelOpen,
shouldUseCanvasBetaLayout, shouldUseCanvasBetaLayout,
shouldShowParametersPanel,
} = ui; } = ui;
const { shouldShowGallery, shouldPinGallery, shouldHoldGalleryOpen } =
gallery;
const canvasBetaLayoutCheck = const canvasBetaLayoutCheck =
shouldUseCanvasBetaLayout && activeTabName === 'unifiedCanvas'; shouldUseCanvasBetaLayout && activeTabName === 'unifiedCanvas';
const shouldShowParametersPanelButton =
!canvasBetaLayoutCheck &&
!(
shouldShowParametersPanel ||
(shouldHoldParametersPanelOpen && !shouldPinParametersPanel)
) &&
['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName);
const shouldShowGalleryButton =
!(shouldShowGallery || (shouldHoldGalleryOpen && !shouldPinGallery)) &&
['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName);
const shouldShowProcessButtons = const shouldShowProcessButtons =
!canvasBetaLayoutCheck && !canvasBetaLayoutCheck &&
(!shouldPinParametersPanel || !shouldShowParametersPanel); (!shouldPinParametersPanel || !shouldShowParametersPanel);
const shouldShowParametersPanelButton =
!canvasBetaLayoutCheck &&
(!shouldPinParametersPanel || !shouldShowParametersPanel) &&
['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName);
return { return {
shouldPinParametersPanel, shouldPinParametersPanel,
shouldShowProcessButtons,
shouldShowParametersPanelButton, shouldShowParametersPanelButton,
shouldShowParametersPanel, shouldShowProcessButtons,
shouldShowGallery,
shouldPinGallery,
shouldShowGalleryButton,
}; };
}, },
{ memoizeOptions: { resultEqualityCheck: isEqual } } { memoizeOptions: { resultEqualityCheck: isEqual } }
@ -71,16 +54,14 @@ const FloatingParametersPanelButtons = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
shouldShowParametersPanelButton,
shouldShowProcessButtons, shouldShowProcessButtons,
shouldShowParametersPanelButton,
shouldPinParametersPanel, shouldPinParametersPanel,
} = useAppSelector(floatingSelector); } = useAppSelector(floatingParametersPanelButtonSelector);
const handleShowOptionsPanel = () => { const handleShowOptionsPanel = () => {
dispatch(setShouldShowParametersPanel(true)); dispatch(setShouldShowParametersPanel(true));
if (shouldPinParametersPanel) { shouldPinParametersPanel && dispatch(requestCanvasRescale());
setTimeout(() => dispatch(setDoesCanvasNeedScaling(true)), 400);
}
}; };
return shouldShowParametersPanelButton ? ( return shouldShowParametersPanelButton ? (

View File

@ -1,18 +0,0 @@
import { Image } from '@chakra-ui/react';
import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks';
export default function InitialImageOverlay() {
const initialImage = useAppSelector(
(state: RootState) => state.generation.initialImage
);
return initialImage ? (
<Image
fit="contain"
src={typeof initialImage === 'string' ? initialImage : initialImage.url}
rounded="md"
className="checkerboard"
/>
) : null;
}

View File

@ -1,11 +0,0 @@
import InvokeWorkarea from 'features/ui/components/InvokeWorkarea';
import ImageToImageDisplay from './ImageToImageDisplay';
import ImageToImagePanel from './ImageToImagePanel';
export default function ImageToImageWorkarea() {
return (
<InvokeWorkarea optionsPanel={<ImageToImagePanel />}>
<ImageToImageDisplay />
</InvokeWorkarea>
);
}

View File

@ -1,35 +0,0 @@
.ltr-parameters-panel-transition-enter {
transform: translateX(-150%);
}
.ltr-parameters-panel-transition-enter-active {
transform: translateX(0);
transition: all 120ms ease-out;
}
.ltr-parameters-panel-transition-exit {
transform: translateX(0);
}
.ltr-parameters-panel-transition-exit-active {
transform: translateX(-150%);
transition: all 120ms ease-out;
}
.rtl-parameters-panel-transition-enter {
transform: translateX(150%);
}
.rtl-parameters-panel-transition-enter-active {
transform: translateX(0);
transition: all 120ms ease-out;
}
.rtl-parameters-panel-transition-exit {
transform: translateX(0);
}
.rtl-parameters-panel-transition-exit-active {
transform: translateX(150%);
transition: all 120ms ease-out;
}

View File

@ -1,249 +0,0 @@
import { Box, Flex, Tooltip, Icon, useTheme } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import {
setShouldHoldParametersPanelOpen,
setShouldPinParametersPanel,
setShouldShowParametersPanel,
} from 'features/ui/store/uiSlice';
import React, { ReactNode, useCallback, useEffect, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
import { CSSTransition } from 'react-transition-group';
import { setDoesCanvasNeedScaling } from 'features/canvas/store/canvasSlice';
import { setParametersPanelScrollPosition } from 'features/ui/store/uiSlice';
import { isEqual } from 'lodash';
import { uiSelector } from '../store/uiSelectors';
import { useTranslation } from 'react-i18next';
import {
APP_CONTENT_HEIGHT,
OPTIONS_BAR_MAX_WIDTH,
PROGRESS_BAR_THICKNESS,
} from 'theme/util/constants';
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
import './InvokeParametersPanel.css';
import { no_scrollbar } from 'theme/components/scrollbar';
type Props = { children: ReactNode };
const optionsPanelSelector = createSelector(
uiSelector,
(ui) => {
const {
shouldShowParametersPanel,
shouldHoldParametersPanelOpen,
shouldPinParametersPanel,
parametersPanelScrollPosition,
} = ui;
return {
shouldShowParametersPanel,
shouldHoldParametersPanelOpen,
shouldPinParametersPanel,
parametersPanelScrollPosition,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
const InvokeOptionsPanel = (props: Props) => {
const dispatch = useAppDispatch();
const { direction } = useTheme();
const {
shouldShowParametersPanel,
shouldHoldParametersPanelOpen,
shouldPinParametersPanel,
} = useAppSelector(optionsPanelSelector);
const optionsPanelRef = useRef<HTMLDivElement>(null);
const optionsPanelContainerRef = useRef<HTMLDivElement>(null);
const timeoutIdRef = useRef<number | null>(null);
const { children } = props;
const { t } = useTranslation();
// Hotkeys
useHotkeys(
'o',
() => {
dispatch(setShouldShowParametersPanel(!shouldShowParametersPanel));
shouldPinParametersPanel &&
setTimeout(() => dispatch(setDoesCanvasNeedScaling(true)), 400);
},
[shouldShowParametersPanel, shouldPinParametersPanel]
);
useHotkeys(
'esc',
() => {
dispatch(setShouldShowParametersPanel(false));
},
{
enabled: () => !shouldPinParametersPanel,
preventDefault: true,
},
[shouldPinParametersPanel]
);
useHotkeys(
'shift+o',
() => {
handleClickPinOptionsPanel();
dispatch(setDoesCanvasNeedScaling(true));
},
[shouldPinParametersPanel]
);
const handleCloseOptionsPanel = useCallback(() => {
if (shouldPinParametersPanel) return;
dispatch(
setParametersPanelScrollPosition(
optionsPanelContainerRef.current
? optionsPanelContainerRef.current.scrollTop
: 0
)
);
dispatch(setShouldShowParametersPanel(false));
dispatch(setShouldHoldParametersPanelOpen(false));
}, [dispatch, shouldPinParametersPanel]);
const setCloseOptionsPanelTimer = () => {
timeoutIdRef.current = window.setTimeout(
() => handleCloseOptionsPanel(),
500
);
};
const cancelCloseOptionsPanelTimer = () => {
timeoutIdRef.current && window.clearTimeout(timeoutIdRef.current);
};
const handleClickPinOptionsPanel = () => {
dispatch(setShouldPinParametersPanel(!shouldPinParametersPanel));
dispatch(setDoesCanvasNeedScaling(true));
};
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (
optionsPanelRef.current &&
!optionsPanelRef.current.contains(e.target as Node)
) {
handleCloseOptionsPanel();
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [handleCloseOptionsPanel]);
return (
<CSSTransition
nodeRef={optionsPanelRef}
in={
shouldShowParametersPanel ||
(shouldHoldParametersPanelOpen && !shouldPinParametersPanel)
}
unmountOnExit
timeout={200}
classNames={`${direction}-parameters-panel-transition`}
>
<Box
className={`${direction}-parameters-panel-transition`}
tabIndex={1}
ref={optionsPanelRef}
onMouseEnter={
!shouldPinParametersPanel ? cancelCloseOptionsPanelTimer : undefined
}
onMouseOver={
!shouldPinParametersPanel ? cancelCloseOptionsPanelTimer : undefined
}
sx={{
borderInlineEndWidth: !shouldPinParametersPanel ? 5 : 0,
borderInlineEndStyle: 'solid',
bg: 'base.900',
borderColor: 'base.700',
height: APP_CONTENT_HEIGHT,
width: OPTIONS_BAR_MAX_WIDTH,
maxWidth: OPTIONS_BAR_MAX_WIDTH,
flexShrink: 0,
position: 'relative',
overflowY: 'scroll',
overflowX: 'hidden',
...no_scrollbar,
...(!shouldPinParametersPanel && {
zIndex: 20,
position: 'fixed',
top: 0,
insetInlineStart: 0,
width: `calc(${OPTIONS_BAR_MAX_WIDTH} + 2rem)`,
maxWidth: `calc(${OPTIONS_BAR_MAX_WIDTH} + 2rem)`,
height: '100%',
}),
}}
>
<Box sx={{ margin: !shouldPinParametersPanel && 4 }}>
<Flex
ref={optionsPanelContainerRef}
onMouseLeave={(e: React.MouseEvent<HTMLDivElement>) => {
if (e.target !== optionsPanelContainerRef.current) {
cancelCloseOptionsPanelTimer();
} else {
!shouldPinParametersPanel && setCloseOptionsPanelTimer();
}
}}
sx={{
display: 'flex',
flexDirection: 'column',
rowGap: 2,
height: '100%',
}}
>
<Tooltip label={t('common.pinOptionsPanel')}>
<Box
onClick={handleClickPinOptionsPanel}
sx={{
position: 'absolute',
cursor: 'pointer',
padding: 2,
top: 4,
insetInlineEnd: 4,
zIndex: 20,
...(shouldPinParametersPanel && {
top: 0,
insetInlineEnd: 0,
}),
}}
>
<Icon
sx={{ opacity: 0.2 }}
as={shouldPinParametersPanel ? BsPinAngleFill : BsPinAngle}
/>
</Box>
</Tooltip>
{!shouldPinParametersPanel && (
<Box sx={{ pt: PROGRESS_BAR_THICKNESS, pb: 2 }}>
<InvokeAILogoComponent />
</Box>
)}
{children}
</Flex>
</Box>
</Box>
</CSSTransition>
);
};
export default InvokeOptionsPanel;

View File

@ -1,4 +1,5 @@
import { import {
ChakraProps,
Icon, Icon,
Tab, Tab,
TabList, TabList,
@ -13,18 +14,10 @@ import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import NodesWIP from 'common/components/WorkInProgress/NodesWIP'; import NodesWIP from 'common/components/WorkInProgress/NodesWIP';
import { PostProcessingWIP } from 'common/components/WorkInProgress/PostProcessingWIP'; import { PostProcessingWIP } from 'common/components/WorkInProgress/PostProcessingWIP';
import TrainingWIP from 'common/components/WorkInProgress/Training'; import TrainingWIP from 'common/components/WorkInProgress/Training';
import useUpdateTranslations from 'common/hooks/useUpdateTranslations';
import { setDoesCanvasNeedScaling } from 'features/canvas/store/canvasSlice';
import { setShouldShowGallery } from 'features/gallery/store/gallerySlice';
import Lightbox from 'features/lightbox/components/Lightbox';
import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice'; import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
import { InvokeTabName } from 'features/ui/store/tabMap'; import { InvokeTabName } from 'features/ui/store/tabMap';
import { import { setActiveTab, togglePanels } from 'features/ui/store/uiSlice';
setActiveTab, import { ReactNode, useMemo } from 'react';
setShouldShowParametersPanel,
} from 'features/ui/store/uiSlice';
import i18n from 'i18n';
import { ReactElement } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { import {
MdDeviceHub, MdDeviceHub,
@ -35,58 +28,55 @@ import {
MdTextFields, MdTextFields,
} from 'react-icons/md'; } from 'react-icons/md';
import { activeTabIndexSelector } from '../store/uiSelectors'; import { activeTabIndexSelector } from '../store/uiSelectors';
import { floatingSelector } from './FloatingParametersPanelButtons'; import ImageToImageWorkarea from 'features/ui/components/tabs/ImageToImage/ImageToImageWorkarea';
import ImageToImageWorkarea from './ImageToImage'; import TextToImageWorkarea from 'features/ui/components/tabs/TextToImage/TextToImageWorkarea';
import TextToImageWorkarea from './TextToImage'; import UnifiedCanvasWorkarea from 'features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasWorkarea';
import UnifiedCanvasWorkarea from './UnifiedCanvas/UnifiedCanvasWorkarea'; import { useTranslation } from 'react-i18next';
import { ResourceKey } from 'i18next';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
export interface InvokeTabInfo { export interface InvokeTabInfo {
title: ReactElement; id: InvokeTabName;
workarea: ReactElement; icon: ReactNode;
tooltip: string; workarea: ReactNode;
} }
export const tabDict: Record<InvokeTabName, InvokeTabInfo> = { const tabIconStyles: ChakraProps['sx'] = {
txt2img: { boxSize: 6,
title: <Icon as={MdTextFields} boxSize={6} />,
workarea: <TextToImageWorkarea />,
tooltip: 'Text To Image',
},
img2img: {
title: <Icon as={MdPhotoLibrary} boxSize={6} />,
workarea: <ImageToImageWorkarea />,
tooltip: 'Image To Image',
},
unifiedCanvas: {
title: <Icon as={MdGridOn} boxSize={6} />,
workarea: <UnifiedCanvasWorkarea />,
tooltip: 'Unified Canvas',
},
nodes: {
title: <Icon as={MdDeviceHub} boxSize={6} />,
workarea: <NodesWIP />,
tooltip: 'Nodes',
},
postprocess: {
title: <Icon as={MdPhotoFilter} boxSize={6} />,
workarea: <PostProcessingWIP />,
tooltip: 'Post Processing',
},
training: {
title: <Icon as={MdFlashOn} boxSize={6} />,
workarea: <TrainingWIP />,
tooltip: 'Training',
},
}; };
function updateTabTranslations() { const tabInfo: InvokeTabInfo[] = [
tabDict.txt2img.tooltip = i18n.t('common.text2img'); {
tabDict.img2img.tooltip = i18n.t('common.img2img'); id: 'txt2img',
tabDict.unifiedCanvas.tooltip = i18n.t('common.unifiedCanvas'); icon: <Icon as={MdTextFields} sx={tabIconStyles} />,
tabDict.nodes.tooltip = i18n.t('common.nodes'); workarea: <TextToImageWorkarea />,
tabDict.postprocess.tooltip = i18n.t('common.postProcessing'); },
tabDict.training.tooltip = i18n.t('common.training'); {
} id: 'img2img',
icon: <Icon as={MdPhotoLibrary} sx={tabIconStyles} />,
workarea: <ImageToImageWorkarea />,
},
{
id: 'unifiedCanvas',
icon: <Icon as={MdGridOn} sx={tabIconStyles} />,
workarea: <UnifiedCanvasWorkarea />,
},
{
id: 'nodes',
icon: <Icon as={MdDeviceHub} sx={tabIconStyles} />,
workarea: <NodesWIP />,
},
{
id: 'postprocessing',
icon: <Icon as={MdPhotoFilter} sx={tabIconStyles} />,
workarea: <PostProcessingWIP />,
},
{
id: 'training',
icon: <Icon as={MdFlashOn} sx={tabIconStyles} />,
workarea: <TrainingWIP />,
},
];
export default function InvokeTabs() { export default function InvokeTabs() {
const activeTab = useAppSelector(activeTabIndexSelector); const activeTab = useAppSelector(activeTabIndexSelector);
@ -95,14 +85,15 @@ export default function InvokeTabs() {
(state: RootState) => state.lightbox.isLightboxOpen (state: RootState) => state.lightbox.isLightboxOpen
); );
const { const shouldPinGallery = useAppSelector(
shouldShowGallery, (state: RootState) => state.ui.shouldPinGallery
shouldShowParametersPanel, );
shouldPinGallery,
shouldPinParametersPanel,
} = useAppSelector(floatingSelector);
useUpdateTranslations(updateTabTranslations); const shouldPinParametersPanel = useAppSelector(
(state: RootState) => state.ui.shouldPinParametersPanel
);
const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -142,53 +133,50 @@ export default function InvokeTabs() {
useHotkeys( useHotkeys(
'f', 'f',
() => { () => {
if (shouldShowGallery || shouldShowParametersPanel) { dispatch(togglePanels());
dispatch(setShouldShowParametersPanel(false)); (shouldPinGallery || shouldPinParametersPanel) &&
dispatch(setShouldShowGallery(false)); dispatch(requestCanvasRescale());
} else {
dispatch(setShouldShowParametersPanel(true));
dispatch(setShouldShowGallery(true));
}
if (shouldPinGallery || shouldPinParametersPanel)
setTimeout(() => dispatch(setDoesCanvasNeedScaling(true)), 400);
}, },
[shouldShowGallery, shouldShowParametersPanel] [shouldPinGallery, shouldPinParametersPanel]
); );
const renderTabs = () => { const tabs = useMemo(
const tabsToRender: ReactElement[] = []; () =>
Object.keys(tabDict).forEach((key) => { tabInfo.map((tab) => (
tabsToRender.push(
<Tooltip <Tooltip
key={key} key={tab.id}
hasArrow hasArrow
label={tabDict[key as keyof typeof tabDict].tooltip} label={String(t(`common.${tab.id}` as ResourceKey))}
placement="end" placement="end"
> >
<Tab> <Tab>
<VisuallyHidden> <VisuallyHidden>
{tabDict[key as keyof typeof tabDict].tooltip} {String(t(`common.${tab.id}` as ResourceKey))}
</VisuallyHidden> </VisuallyHidden>
{tabDict[key as keyof typeof tabDict].title} {tab.icon}
</Tab> </Tab>
</Tooltip> </Tooltip>
); )),
}); [t]
return tabsToRender; );
};
const renderTabPanels = () => { const tabPanels = useMemo(
const tabPanelsToRender: ReactElement[] = []; () =>
Object.keys(tabDict).forEach((key) => { tabInfo.map((tab) => <TabPanel key={tab.id}>{tab.workarea}</TabPanel>),
tabPanelsToRender.push( []
<TabPanel key={key}> );
{tabDict[key as keyof typeof tabDict].workarea}
</TabPanel>
);
});
return tabPanelsToRender;
};
/**
* isLazy means the tabs are mounted and unmounted when changing them. There is a tradeoff here,
* as mounting is expensive, but so is retaining all tabs in the DOM at all times.
*
* Removing isLazy messes with the outside click watcher, which is used by ResizableDrawer.
* Because you have multiple handlers listening for an outside click, any click anywhere triggers
* the watcher for the hidden drawers, closing the open drawer.
*
* TODO: Add logic to the `useOutsideClick` in ResizableDrawer to enable it only for the active
* tab's drawer.
*/
return ( return (
<Tabs <Tabs
isLazy isLazy
@ -197,9 +185,10 @@ export default function InvokeTabs() {
onChange={(index: number) => { onChange={(index: number) => {
dispatch(setActiveTab(index)); dispatch(setActiveTab(index));
}} }}
flexGrow={1}
> >
<TabList>{renderTabs()}</TabList> <TabList>{tabs}</TabList>
<TabPanels>{isLightBoxOpen ? <Lightbox /> : renderTabPanels()}</TabPanels> <TabPanels>{tabPanels}</TabPanels>
</Tabs> </Tabs>
); );
} }

View File

@ -1,7 +1,6 @@
import { Box, BoxProps, Flex } from '@chakra-ui/react'; import { Box, BoxProps, Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import ImageGallery from 'features/gallery/components/ImageGallery';
import { setInitialImage } from 'features/parameters/store/generationSlice'; import { setInitialImage } from 'features/parameters/store/generationSlice';
import { import {
activeTabNameSelector, activeTabNameSelector,
@ -11,17 +10,16 @@ import { DragEvent, ReactNode } from 'react';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import useGetImageByUuid from 'features/gallery/hooks/useGetImageByUuid'; import useGetImageByUuid from 'features/gallery/hooks/useGetImageByUuid';
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { APP_CONTENT_HEIGHT } from 'theme/util/constants';
import ParametersPanel from './ParametersPanel';
const workareaSelector = createSelector( const workareaSelector = createSelector(
[uiSelector, lightboxSelector, activeTabNameSelector], [uiSelector, activeTabNameSelector],
(ui, lightbox, activeTabName) => { (ui, activeTabName) => {
const { shouldPinParametersPanel } = ui; const { shouldPinParametersPanel } = ui;
const { isLightboxOpen } = lightbox;
return { return {
shouldPinParametersPanel, shouldPinParametersPanel,
isLightboxOpen,
activeTabName, activeTabName,
}; };
}, },
@ -33,14 +31,14 @@ const workareaSelector = createSelector(
); );
type InvokeWorkareaProps = BoxProps & { type InvokeWorkareaProps = BoxProps & {
optionsPanel: ReactNode; parametersPanelContent: ReactNode;
children: ReactNode; children: ReactNode;
}; };
const InvokeWorkarea = (props: InvokeWorkareaProps) => { const InvokeWorkarea = (props: InvokeWorkareaProps) => {
const { parametersPanelContent, children, ...rest } = props;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { optionsPanel, children, ...rest } = props; const { activeTabName } = useAppSelector(workareaSelector);
const { activeTabName, isLightboxOpen } = useAppSelector(workareaSelector);
const getImageByUuid = useGetImageByUuid(); const getImageByUuid = useGetImageByUuid();
@ -56,15 +54,12 @@ const InvokeWorkarea = (props: InvokeWorkareaProps) => {
}; };
return ( return (
<Box {...rest} pos="relative" w="100%" h="100%"> <Flex {...rest} pos="relative" w="full" h={APP_CONTENT_HEIGHT} gap={4}>
<Flex gap={4} h="100%"> <ParametersPanel>{parametersPanelContent}</ParametersPanel>
{optionsPanel} <Box pos="relative" w="100%" h="100%" onDrop={handleDrop}>
<Box pos="relative" w="100%" h="100%" onDrop={handleDrop}> {children}
{children} </Box>
</Box> </Flex>
{!isLightboxOpen && <ImageGallery />}
</Flex>
</Box>
); );
}; };

View File

@ -0,0 +1,129 @@
import { Flex } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { memo, ReactNode } from 'react';
import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants';
import ResizableDrawer from 'features/ui/components/common/ResizableDrawer/ResizableDrawer';
import {
setShouldShowParametersPanel,
toggleParametersPanel,
togglePinParametersPanel,
} from 'features/ui/store/uiSlice';
import { useHotkeys } from 'react-hotkeys-hook';
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
import Scrollable from './common/Scrollable';
import PinParametersPanelButton from './PinParametersPanelButton';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import { createSelector } from '@reduxjs/toolkit';
import { activeTabNameSelector, uiSelector } from '../store/uiSelectors';
import { isEqual } from 'lodash';
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
const parametersPanelSelector = createSelector(
[uiSelector, activeTabNameSelector, lightboxSelector],
(ui, activeTabName, lightbox) => {
const { shouldPinParametersPanel, shouldShowParametersPanel } = ui;
const { isLightboxOpen } = lightbox;
return {
shouldPinParametersPanel,
shouldShowParametersPanel,
isResizable: activeTabName !== 'unifiedCanvas',
isLightboxOpen,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
type ParametersPanelProps = {
children: ReactNode;
};
const ParametersPanel = ({ children }: ParametersPanelProps) => {
const dispatch = useAppDispatch();
const {
shouldPinParametersPanel,
shouldShowParametersPanel,
isResizable,
isLightboxOpen,
} = useAppSelector(parametersPanelSelector);
const closeParametersPanel = () => {
dispatch(setShouldShowParametersPanel(false));
};
useHotkeys(
'o',
() => {
dispatch(toggleParametersPanel());
shouldPinParametersPanel && dispatch(requestCanvasRescale());
},
{ enabled: () => !isLightboxOpen },
[shouldPinParametersPanel, isLightboxOpen]
);
useHotkeys(
'esc',
() => {
dispatch(setShouldShowParametersPanel(false));
},
{
enabled: () => !shouldPinParametersPanel,
preventDefault: true,
},
[shouldPinParametersPanel]
);
useHotkeys(
'shift+o',
() => {
dispatch(togglePinParametersPanel());
dispatch(requestCanvasRescale());
},
[]
);
return (
<ResizableDrawer
direction="left"
isResizable={isResizable || !shouldPinParametersPanel}
isOpen={shouldShowParametersPanel}
onClose={closeParametersPanel}
isPinned={shouldPinParametersPanel || isLightboxOpen}
sx={{
borderColor: 'base.700',
p: shouldPinParametersPanel ? 0 : 4,
bg: 'base.900',
}}
initialWidth={PARAMETERS_PANEL_WIDTH}
minWidth={PARAMETERS_PANEL_WIDTH}
>
<Flex flexDir="column" position="relative" h="full" w="full">
{!shouldPinParametersPanel && (
<Flex
paddingTop={1.5}
paddingBottom={4}
justifyContent="space-between"
alignItems="center"
>
<InvokeAILogoComponent />
<PinParametersPanelButton />
</Flex>
)}
<Scrollable>{children}</Scrollable>
{shouldPinParametersPanel && (
<PinParametersPanelButton
sx={{ position: 'absolute', top: 0, insetInlineEnd: 0 }}
/>
)}
</Flex>
</ResizableDrawer>
);
};
export default memo(ParametersPanel);

View File

@ -0,0 +1,51 @@
import { Tooltip } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIIconButton, {
IAIIconButtonProps,
} from 'common/components/IAIIconButton';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import { useTranslation } from 'react-i18next';
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
import { setShouldPinParametersPanel } from '../store/uiSlice';
type PinParametersPanelButtonProps = Omit<IAIIconButtonProps, 'aria-label'>;
const PinParametersPanelButton = (props: PinParametersPanelButtonProps) => {
const { sx } = props;
const dispatch = useAppDispatch();
const shouldPinParametersPanel = useAppSelector(
(state) => state.ui.shouldPinParametersPanel
);
const { t } = useTranslation();
const handleClickPinOptionsPanel = () => {
dispatch(setShouldPinParametersPanel(!shouldPinParametersPanel));
dispatch(requestCanvasRescale());
};
return (
<Tooltip label={t('common.pinOptionsPanel')}>
<IAIIconButton
{...props}
aria-label={t('common.pinOptionsPanel')}
onClick={handleClickPinOptionsPanel}
icon={shouldPinParametersPanel ? <BsPinAngleFill /> : <BsPinAngle />}
variant="ghost"
size="sm"
sx={{
color: 'base.700',
_hover: {
color: 'base.550',
},
_active: {
color: 'base.500',
},
...sx,
}}
/>
</Tooltip>
);
};
export default PinParametersPanelButton;

View File

@ -1,11 +0,0 @@
import InvokeWorkarea from 'features/ui/components/InvokeWorkarea';
import TextToImageDisplay from './TextToImageDisplay';
import TextToImagePanel from './TextToImagePanel';
export default function TextToImageWorkarea() {
return (
<InvokeWorkarea optionsPanel={<TextToImagePanel />}>
<TextToImageDisplay />
</InvokeWorkarea>
);
}

View File

@ -1,21 +0,0 @@
import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks';
import InvokeWorkarea from 'features/ui/components/InvokeWorkarea';
import UnifiedCanvasDisplayBeta from './UnifiedCanvasBeta/UnifiedCanvasDisplayBeta';
import UnifiedCanvasDisplay from './UnifiedCanvasDisplay';
import UnifiedCanvasPanel from './UnifiedCanvasPanel';
export default function UnifiedCanvasWorkarea() {
const shouldUseCanvasBetaLayout = useAppSelector(
(state: RootState) => state.ui.shouldUseCanvasBetaLayout
);
return (
<InvokeWorkarea optionsPanel={<UnifiedCanvasPanel />}>
{shouldUseCanvasBetaLayout ? (
<UnifiedCanvasDisplayBeta />
) : (
<UnifiedCanvasDisplay />
)}
</InvokeWorkarea>
);
}

View File

@ -0,0 +1,222 @@
import {
Box,
chakra,
ChakraProps,
Slide,
useOutsideClick,
useTheme,
SlideDirection,
} from '@chakra-ui/react';
import {
Resizable,
ResizableProps,
ResizeCallback,
ResizeStartCallback,
} from 're-resizable';
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { LangDirection } from './types';
import {
getHandleEnables,
getMinMaxDimensions,
getSlideDirection,
getStyles,
parseAndPadSize,
} from './util';
type ResizableDrawerProps = ResizableProps & {
children: ReactNode;
isResizable: boolean;
isPinned: boolean;
isOpen: boolean;
onClose: () => void;
direction?: SlideDirection;
initialWidth?: number;
minWidth?: number;
maxWidth?: number;
initialHeight?: number;
minHeight?: number;
maxHeight?: number;
onResizeStart?: ResizeStartCallback;
onResizeStop?: ResizeCallback;
onResize?: ResizeCallback;
handleWidth?: string | number;
handleInteractWidth?: string | number;
sx?: ChakraProps['sx'];
};
const ChakraResizeable = chakra(Resizable, {
shouldForwardProp: (prop) => !['sx'].includes(prop),
});
const ResizableDrawer = ({
direction = 'left',
isResizable,
isPinned,
isOpen,
onClose,
children,
initialWidth,
minWidth,
maxWidth,
initialHeight,
minHeight,
maxHeight,
onResizeStart,
onResizeStop,
onResize,
sx = {},
}: ResizableDrawerProps) => {
const langDirection = useTheme().direction as LangDirection;
const outsideClickRef = useRef<HTMLDivElement>(null);
const defaultWidth = useMemo(
() =>
initialWidth ??
minWidth ??
(['left', 'right'].includes(direction) ? 500 : '100%'),
[initialWidth, minWidth, direction]
);
const defaultHeight = useMemo(
() =>
initialHeight ??
minHeight ??
(['top', 'bottom'].includes(direction) ? 500 : '100%'),
[initialHeight, minHeight, direction]
);
const [width, setWidth] = useState<number | string>(defaultWidth);
const [height, setHeight] = useState<number | string>(defaultHeight);
useOutsideClick({
ref: outsideClickRef,
handler: () => {
if (isPinned) {
return;
}
onClose();
},
});
const handleEnables = useMemo(
() => (isResizable ? getHandleEnables({ direction, langDirection }) : {}),
[isResizable, langDirection, direction]
);
const minMaxDimensions = useMemo(
() =>
getMinMaxDimensions({
direction,
minWidth: isResizable
? parseAndPadSize(minWidth, 18)
: parseAndPadSize(minWidth),
maxWidth: isResizable
? parseAndPadSize(maxWidth, 18)
: parseAndPadSize(maxWidth),
minHeight: isResizable
? parseAndPadSize(minHeight, 18)
: parseAndPadSize(minHeight),
maxHeight: isResizable
? parseAndPadSize(maxHeight, 18)
: parseAndPadSize(maxHeight),
}),
[minWidth, maxWidth, minHeight, maxHeight, direction, isResizable]
);
const { containerStyles, handleStyles } = useMemo(
() =>
getStyles({
isPinned,
isResizable,
direction,
}),
[isPinned, isResizable, direction]
);
const slideDirection = useMemo(
() => getSlideDirection(direction, langDirection),
[direction, langDirection]
);
useEffect(() => {
if (['left', 'right'].includes(direction)) {
setHeight(isPinned ? '100%' : '100vh');
}
if (['top', 'bottom'].includes(direction)) {
setWidth(isPinned ? '100%' : '100vw');
}
}, [isPinned, direction]);
return (
<Slide
direction={slideDirection}
in={isOpen}
unmountOnExit={isPinned}
motionProps={{ initial: isPinned }}
{...(isPinned
? {
style: {
position: undefined,
left: undefined,
right: undefined,
top: undefined,
bottom: undefined,
width: undefined,
},
}
: {
// transition: { enter: { duration: 0.15 }, exit: { duration: 0.15 } },
style: { zIndex: 99, width: 'full' },
})}
>
<Box
ref={outsideClickRef}
sx={{
width: 'full',
height: 'full',
}}
>
<ChakraResizeable
size={{
width: isResizable ? width : defaultWidth,
height: isResizable ? height : defaultHeight,
}}
enable={handleEnables}
handleStyles={handleStyles}
{...minMaxDimensions}
sx={{
borderColor: 'base.800',
p: isPinned ? 0 : 4,
bg: 'base.900',
height: 'full',
boxShadow: !isPinned ? '0 0 4rem 0 rgba(0, 0, 0, 0.8)' : '',
...containerStyles,
...sx,
}}
onResizeStart={(event, direction, elementRef) => {
onResizeStart && onResizeStart(event, direction, elementRef);
}}
onResize={(event, direction, elementRef, delta) => {
onResize && onResize(event, direction, elementRef, delta);
}}
onResizeStop={(event, direction, elementRef, delta) => {
if (['left', 'right'].includes(direction)) {
setWidth(Number(width) + delta.width);
}
if (['top', 'bottom'].includes(direction)) {
setHeight(Number(height) + delta.height);
}
onResizeStop && onResizeStop(event, direction, elementRef, delta);
}}
>
{children}
</ChakraResizeable>
</Box>
</Slide>
);
};
export default ResizableDrawer;

View File

@ -0,0 +1,2 @@
export type Placement = 'top' | 'right' | 'bottom' | 'left';
export type LangDirection = 'ltr' | 'rtl' | undefined;

View File

@ -0,0 +1,294 @@
import { SlideDirection } from '@chakra-ui/react';
import { AnimationProps } from 'framer-motion';
import { HandleStyles } from 're-resizable';
import { CSSProperties } from 'react';
import { LangDirection } from './types';
export type GetHandleEnablesOptions = {
direction: SlideDirection;
langDirection: LangDirection;
};
/**
* Determine handles to enable. `re-resizable` doesn't handle RTL, so we have to do that here.
*/
export const getHandleEnables = ({
direction,
langDirection,
}: GetHandleEnablesOptions) => {
const top = direction === 'bottom';
const right =
(langDirection !== 'rtl' && direction === 'left') ||
(langDirection === 'rtl' && direction === 'right');
const bottom = direction === 'top';
const left =
(langDirection !== 'rtl' && direction === 'right') ||
(langDirection === 'rtl' && direction === 'left');
return {
top,
right,
bottom,
left,
};
};
export type GetDefaultSizeOptions = {
initialWidth?: string | number;
initialHeight?: string | number;
direction: SlideDirection;
};
// Get default sizes based on direction and initial values
export const getDefaultSize = ({
initialWidth,
initialHeight,
direction,
}: GetDefaultSizeOptions) => {
const width =
initialWidth ?? (['left', 'right'].includes(direction) ? 500 : '100vw');
const height =
initialHeight ?? (['top', 'bottom'].includes(direction) ? 500 : '100vh');
return { width, height };
};
export type GetMinMaxDimensionsOptions = {
direction: SlideDirection;
minWidth?: number;
maxWidth?: number;
minHeight?: number;
maxHeight?: number;
};
// Get the min/max width/height based on direction and provided values
export const getMinMaxDimensions = ({
direction,
minWidth,
maxWidth,
minHeight,
maxHeight,
}: GetMinMaxDimensionsOptions) => {
const minW =
minWidth ?? (['left', 'right'].includes(direction) ? 10 : undefined);
const maxW =
maxWidth ?? (['left', 'right'].includes(direction) ? '95vw' : undefined);
const minH =
minHeight ?? (['top', 'bottom'].includes(direction) ? 10 : undefined);
const maxH =
maxHeight ?? (['top', 'bottom'].includes(direction) ? '95vh' : undefined);
return {
...(minW ? { minWidth: minW } : {}),
...(maxW ? { maxWidth: maxW } : {}),
...(minH ? { minHeight: minH } : {}),
...(maxH ? { maxHeight: maxH } : {}),
};
};
export type GetAnimationsOptions = {
direction: SlideDirection;
langDirection: LangDirection;
};
// Get the framer-motion animation props, taking into account language direction
export const getAnimations = ({
direction,
langDirection,
}: GetAnimationsOptions): AnimationProps => {
const baseAnimation = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
// chakra consumes the transition prop, which, for it, is a string.
// however we know the transition prop will make it to framer motion,
// which wants it as an object. cast as string to satisfy TS.
transition: { duration: 0.2, ease: 'easeInOut' },
};
const langDirectionFactor = langDirection === 'rtl' ? -1 : 1;
if (direction === 'top') {
return {
...baseAnimation,
initial: { y: -999 },
animate: { y: 0 },
exit: { y: -999 },
};
}
if (direction === 'right') {
return {
...baseAnimation,
initial: { x: 999 * langDirectionFactor },
animate: { x: 0 },
exit: { x: 999 * langDirectionFactor },
};
}
if (direction === 'bottom') {
return {
...baseAnimation,
initial: { y: 999 },
animate: { y: 0 },
exit: { y: 999 },
};
}
if (direction === 'left') {
return {
...baseAnimation,
initial: { x: -999 * langDirectionFactor },
animate: { x: 0 },
exit: { x: -999 * langDirectionFactor },
};
}
return {};
};
export type GetResizableStylesProps = {
isPinned: boolean;
isResizable: boolean;
direction: SlideDirection;
};
// Expand the handle hitbox
const HANDLE_INTERACT_PADDING = '0.75rem';
// Visible padding around handle
const HANDLE_PADDING = '1rem';
const HANDLE_WIDTH_PINNED = '2px';
const HANDLE_WIDTH_UNPINNED = '5px';
// Get the styles for the container and handle. Do not need to handle langDirection here bc we use direction-agnostic CSS
export const getStyles = ({
isPinned,
isResizable,
direction,
}: GetResizableStylesProps): {
containerStyles: CSSProperties; // technically this could be ChakraProps['sx'], but we cannot use this for HandleStyles so leave it as CSSProperties to be consistent
handleStyles: HandleStyles;
} => {
if (!isResizable) {
return { containerStyles: {}, handleStyles: {} };
}
const handleWidth = isPinned ? HANDLE_WIDTH_PINNED : HANDLE_WIDTH_UNPINNED;
// Calculate the positioning offset of the handle hitbox so it is centered over the handle
const handleOffset = `calc((2 * ${HANDLE_INTERACT_PADDING} + ${handleWidth}) / -2)`;
if (direction === 'top') {
return {
containerStyles: {
borderBottomWidth: handleWidth,
paddingBottom: HANDLE_PADDING,
},
handleStyles: {
top: {
paddingTop: HANDLE_INTERACT_PADDING,
paddingBottom: HANDLE_INTERACT_PADDING,
bottom: handleOffset,
},
},
};
}
if (direction === 'left') {
return {
containerStyles: {
borderInlineEndWidth: handleWidth,
paddingInlineEnd: HANDLE_PADDING,
},
handleStyles: {
right: {
paddingInlineStart: HANDLE_INTERACT_PADDING,
paddingInlineEnd: HANDLE_INTERACT_PADDING,
insetInlineEnd: handleOffset,
},
},
};
}
if (direction === 'bottom') {
return {
containerStyles: {
borderTopWidth: handleWidth,
paddingTop: HANDLE_PADDING,
},
handleStyles: {
bottom: {
paddingTop: HANDLE_INTERACT_PADDING,
paddingBottom: HANDLE_INTERACT_PADDING,
top: handleOffset,
},
},
};
}
if (direction === 'right') {
return {
containerStyles: {
borderInlineStartWidth: handleWidth,
paddingInlineStart: HANDLE_PADDING,
},
handleStyles: {
left: {
paddingInlineStart: HANDLE_INTERACT_PADDING,
paddingInlineEnd: HANDLE_INTERACT_PADDING,
insetInlineStart: handleOffset,
},
},
};
}
return { containerStyles: {}, handleStyles: {} };
};
// Chakra's Slide does not handle langDirection, so we need to do it here
export const getSlideDirection = (
direction: SlideDirection,
langDirection: LangDirection
) => {
if (['top', 'bottom'].includes(direction)) {
return direction;
}
if (direction === 'left') {
if (langDirection === 'rtl') {
return 'right';
}
return 'left';
}
if (direction === 'right') {
if (langDirection === 'rtl') {
return 'left';
}
return 'right';
}
return 'left';
};
// Hack to correct the width of panels while pinned and unpinned, due to different padding in pinned vs unpinned
export const parseAndPadSize = (size?: number, padding?: number) => {
if (!size) {
return undefined;
}
if (!padding) {
return size;
}
return size + padding;
};

View File

@ -0,0 +1,88 @@
import { Box, ChakraProps } from '@chakra-ui/react';
import { throttle } from 'lodash';
import { ReactNode, useEffect, useRef } from 'react';
const scrollShadowBaseStyles: ChakraProps['sx'] = {
position: 'absolute',
width: 'full',
height: 24,
left: 0,
pointerEvents: 'none',
transition: 'opacity 0.2s ease-in-out',
};
type ScrollableProps = {
children: ReactNode;
};
const Scrollable = ({ children }: ScrollableProps) => {
const scrollableRef = useRef<HTMLDivElement>(null);
const topShadowRef = useRef<HTMLDivElement>(null);
const bottomShadowRef = useRef<HTMLDivElement>(null);
const updateScrollShadowOpacity = throttle(
() => {
if (
!scrollableRef.current ||
!topShadowRef.current ||
!bottomShadowRef.current
) {
return;
}
const { scrollTop, scrollHeight, offsetHeight } = scrollableRef.current;
if (scrollTop > 0) {
topShadowRef.current.style.opacity = '1';
} else {
topShadowRef.current.style.opacity = '0';
}
if (scrollTop >= scrollHeight - offsetHeight) {
bottomShadowRef.current.style.opacity = '0';
} else {
bottomShadowRef.current.style.opacity = '1';
}
},
33,
{ leading: true }
);
useEffect(() => {
updateScrollShadowOpacity();
}, [updateScrollShadowOpacity]);
return (
<Box position="relative" w="full" h="full">
<Box
ref={scrollableRef}
position="absolute"
w="full"
h="full"
overflowY="scroll"
onScroll={updateScrollShadowOpacity}
>
{children}
</Box>
<Box
ref={bottomShadowRef}
sx={{
...scrollShadowBaseStyles,
bottom: 0,
boxShadow:
'inset 0 -3.5rem 2rem -2rem var(--invokeai-colors-base-900)',
}}
></Box>
<Box
ref={topShadowRef}
sx={{
...scrollShadowBaseStyles,
top: 0,
boxShadow:
'inset 0 3.5rem 2rem -2rem var(--invokeai-colors-base-900)',
}}
></Box>
</Box>
);
};
export default Scrollable;

View File

@ -14,7 +14,7 @@ const workareaSplitViewStyle: ChakraProps['sx'] = {
padding: 4, padding: 4,
}; };
const ImageToImageDisplay = () => { const ImageToImageContent = () => {
const initialImage = useAppSelector( const initialImage = useAppSelector(
(state: RootState) => state.generation.initialImage (state: RootState) => state.generation.initialImage
); );
@ -47,4 +47,4 @@ const ImageToImageDisplay = () => {
); );
}; };
export default ImageToImageDisplay; export default ImageToImageContent;

View File

@ -15,11 +15,11 @@ import ParametersAccordion from 'features/parameters/components/ParametersAccord
import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons'; import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons';
import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput'; import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput';
import PromptInput from 'features/parameters/components/PromptInput/PromptInput'; import PromptInput from 'features/parameters/components/PromptInput/PromptInput';
import InvokeOptionsPanel from 'features/ui/components/InvokeParametersPanel';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ImageToImageSettings from './ImageToImageSettings'; import ImageToImageSettings from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageSettings';
import { memo } from 'react';
export default function ImageToImagePanel() { const ImageToImageParameters = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const imageToImageAccordions = { const imageToImageAccordions = {
@ -69,13 +69,13 @@ export default function ImageToImagePanel() {
}; };
return ( return (
<InvokeOptionsPanel> <Flex flexDir="column" gap={2} position="relative">
<Flex flexDir="column" rowGap={2}> <PromptInput />
<PromptInput /> <NegativePromptInput />
<NegativePromptInput />
</Flex>
<ProcessButtons /> <ProcessButtons />
<ParametersAccordion accordionInfo={imageToImageAccordions} /> <ParametersAccordion accordionInfo={imageToImageAccordions} />
</InvokeOptionsPanel> </Flex>
); );
} };
export default memo(ImageToImageParameters);

View File

@ -0,0 +1,11 @@
import InvokeWorkarea from 'features/ui/components/InvokeWorkarea';
import ImageToImageContent from './ImageToImageContent';
import ImageToImageParameters from './ImageToImageParameters';
export default function ImageToImageWorkarea() {
return (
<InvokeWorkarea parametersPanelContent={<ImageToImageParameters />}>
<ImageToImageContent />
</InvokeWorkarea>
);
}

Some files were not shown because too many files have changed in this diff Show More