Merge branch 'main' into lstein/new-model-manager

This commit is contained in:
Lincoln Stein 2023-05-13 22:01:34 -04:00 committed by GitHub
commit baf5451fa0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
79 changed files with 717 additions and 550 deletions

View File

@ -51,7 +51,7 @@ class TextToImageInvocation(BaseInvocation, SDImageInvocation):
width: int = Field(default=512, multiple_of=8, gt=0, description="The width of the resulting image", ) width: int = Field(default=512, multiple_of=8, gt=0, description="The width of the resulting image", )
height: int = Field(default=512, multiple_of=8, gt=0, description="The height of the resulting image", ) height: int = Field(default=512, multiple_of=8, gt=0, description="The height of the resulting image", )
cfg_scale: float = Field(default=7.5, ge=1, description="The Classifier-Free Guidance, higher values may result in a result closer to the prompt", ) cfg_scale: float = Field(default=7.5, ge=1, description="The Classifier-Free Guidance, higher values may result in a result closer to the prompt", )
scheduler: SAMPLER_NAME_VALUES = Field(default="k_lms", description="The scheduler to use" ) scheduler: SAMPLER_NAME_VALUES = Field(default="lms", description="The scheduler to use" )
model: str = Field(default="", description="The model to use (currently ignored)") model: str = Field(default="", description="The model to use (currently ignored)")
# fmt: on # fmt: on

View File

@ -33,8 +33,8 @@ class ImageOutput(BaseInvocationOutput):
# fmt: off # fmt: off
type: Literal["image"] = "image" type: Literal["image"] = "image"
image: ImageField = Field(default=None, description="The output image") image: ImageField = Field(default=None, description="The output image")
width: Optional[int] = Field(default=None, description="The width of the image in pixels") width: int = Field(description="The width of the image in pixels")
height: Optional[int] = Field(default=None, description="The height of the image in pixels") height: int = Field(description="The height of the image in pixels")
# fmt: on # fmt: on
class Config: class Config:

View File

@ -14,6 +14,7 @@ from invokeai.app.util.misc import SEED_MAX, get_random_seed
from invokeai.app.util.step_callback import stable_diffusion_step_callback from invokeai.app.util.step_callback import stable_diffusion_step_callback
from ...backend.image_util.seamless import configure_model_padding from ...backend.image_util.seamless import configure_model_padding
from ...backend.stable_diffusion import PipelineIntermediateState from ...backend.stable_diffusion import PipelineIntermediateState
from ...backend.stable_diffusion.diffusers_pipeline import ( from ...backend.stable_diffusion.diffusers_pipeline import (
ConditioningData, StableDiffusionGeneratorPipeline, ConditioningData, StableDiffusionGeneratorPipeline,
@ -21,6 +22,10 @@ from ...backend.stable_diffusion.diffusers_pipeline import (
from ...backend.stable_diffusion.diffusion.shared_invokeai_diffusion import \ from ...backend.stable_diffusion.diffusion.shared_invokeai_diffusion import \
PostprocessingSettings PostprocessingSettings
from ...backend.util.devices import choose_torch_device, torch_dtype from ...backend.util.devices import choose_torch_device, torch_dtype
from ...backend.prompting.conditioning import get_uc_and_c_and_ec
from ...backend.stable_diffusion.schedulers import SCHEDULER_MAP
from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationContext, InvocationConfig
import numpy as np
from ..services.image_storage import ImageType from ..services.image_storage import ImageType
from .baseinvocation import (BaseInvocation, BaseInvocationOutput, from .baseinvocation import (BaseInvocation, BaseInvocationOutput,
InvocationConfig, InvocationContext) InvocationConfig, InvocationContext)
@ -42,48 +47,59 @@ class LatentsField(BaseModel):
class LatentsOutput(BaseInvocationOutput): class LatentsOutput(BaseInvocationOutput):
"""Base class for invocations that output latents""" """Base class for invocations that output latents"""
#fmt: off #fmt: off
type: Literal["latent_output"] = "latent_output" type: Literal["latents_output"] = "latents_output"
latents: LatentsField = Field(default=None, description="The output latents")
# Inputs
latents: LatentsField = Field(default=None, description="The output latents")
width: int = Field(description="The width of the latents in pixels")
height: int = Field(description="The height of the latents in pixels")
#fmt: on #fmt: on
def build_latents_output(latents_name: str, latents: torch.Tensor):
return LatentsOutput(
latents=LatentsField(latents_name=latents_name),
width=latents.size()[3] * 8,
height=latents.size()[2] * 8,
)
class NoiseOutput(BaseInvocationOutput): class NoiseOutput(BaseInvocationOutput):
"""Invocation noise output""" """Invocation noise output"""
#fmt: off #fmt: off
type: Literal["noise_output"] = "noise_output" type: Literal["noise_output"] = "noise_output"
# Inputs
noise: LatentsField = Field(default=None, description="The output noise") noise: LatentsField = Field(default=None, description="The output noise")
width: int = Field(description="The width of the noise in pixels")
height: int = Field(description="The height of the noise in pixels")
#fmt: on #fmt: on
def build_noise_output(latents_name: str, latents: torch.Tensor):
# TODO: this seems like a hack return NoiseOutput(
scheduler_map = dict( noise=LatentsField(latents_name=latents_name),
ddim=diffusers.DDIMScheduler, width=latents.size()[3] * 8,
dpmpp_2=diffusers.DPMSolverMultistepScheduler, height=latents.size()[2] * 8,
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,
)
SAMPLER_NAME_VALUES = Literal[ SAMPLER_NAME_VALUES = Literal[
tuple(list(scheduler_map.keys())) tuple(list(SCHEDULER_MAP.keys()))
] ]
def get_scheduler( def get_scheduler(
context: InvocationContext, context: InvocationContext,
scheduler_info: ModelInfo, scheduler_info: ModelInfo,
scheduler_name: str, scheduler_name: str,
) -> Scheduler: ) -> Scheduler:
scheduler_class, scheduler_extra_config = SCHEDULER_MAP.get(scheduler_name, SCHEDULER_MAP['ddim'])
orig_scheduler_info = context.services.model_manager.get_model(**scheduler_info.dict()) orig_scheduler_info = context.services.model_manager.get_model(**scheduler_info.dict())
with orig_scheduler_info as orig_scheduler: with orig_scheduler_info as orig_scheduler:
scheduler_config = orig_scheduler.config scheduler_config = orig_scheduler.config
if "_backup" in scheduler_config:
scheduler_class = scheduler_map.get(scheduler_name,'ddim') scheduler_config = scheduler_config["_backup"]
scheduler_config = {**scheduler_config, **scheduler_extra_config, "_backup": scheduler_config}
scheduler = scheduler_class.from_config(scheduler_config) scheduler = scheduler_class.from_config(scheduler_config)
# hack copied over from generate.py # hack copied over from generate.py
if not hasattr(scheduler, 'uses_inpainting_model'): if not hasattr(scheduler, 'uses_inpainting_model'):
@ -139,9 +155,7 @@ class NoiseInvocation(BaseInvocation):
name = f'{context.graph_execution_state_id}__{self.id}' name = f'{context.graph_execution_state_id}__{self.id}'
context.services.latents.set(name, noise) context.services.latents.set(name, noise)
return NoiseOutput( return build_noise_output(latents_name=name, latents=noise)
noise=LatentsField(latents_name=name)
)
# Text to image # Text to image
@ -157,7 +171,8 @@ class TextToLatentsInvocation(BaseInvocation):
noise: Optional[LatentsField] = Field(description="The noise to use") noise: Optional[LatentsField] = Field(description="The noise to use")
steps: int = Field(default=10, gt=0, description="The number of steps to use to generate the image") steps: int = Field(default=10, gt=0, description="The number of steps to use to generate the image")
cfg_scale: float = Field(default=7.5, gt=0, description="The Classifier-Free Guidance, higher values may result in a result closer to the prompt", ) cfg_scale: float = Field(default=7.5, gt=0, description="The Classifier-Free Guidance, higher values may result in a result closer to the prompt", )
scheduler: SAMPLER_NAME_VALUES = Field(default="k_lms", description="The scheduler to use" ) scheduler: SAMPLER_NAME_VALUES = Field(default="lms", description="The scheduler to use" )
model: str = Field(default="", description="The model to use (currently ignored)")
seamless: bool = Field(default=False, description="Whether or not to generate an image that can tile without seams", ) seamless: bool = Field(default=False, description="Whether or not to generate an image that can tile without seams", )
seamless_axes: str = Field(default="", description="The axes to tile the image on, 'x' and/or 'y'") seamless_axes: str = Field(default="", description="The axes to tile the image on, 'x' and/or 'y'")
@ -264,9 +279,7 @@ class TextToLatentsInvocation(BaseInvocation):
name = f'{context.graph_execution_state_id}__{self.id}' name = f'{context.graph_execution_state_id}__{self.id}'
context.services.latents.set(name, result_latents) context.services.latents.set(name, result_latents)
return LatentsOutput( return build_latents_output(latents_name=name, latents=result_latents)
latents=LatentsField(latents_name=name)
)
class LatentsToLatentsInvocation(TextToLatentsInvocation): class LatentsToLatentsInvocation(TextToLatentsInvocation):
@ -337,9 +350,7 @@ class LatentsToLatentsInvocation(TextToLatentsInvocation):
name = f'{context.graph_execution_state_id}__{self.id}' name = f'{context.graph_execution_state_id}__{self.id}'
context.services.latents.set(name, result_latents) context.services.latents.set(name, result_latents)
return LatentsOutput( return build_latents_output(latents_name=name, latents=result_latents)
latents=LatentsField(latents_name=name)
)
# Latent to image # Latent to image
@ -417,11 +428,11 @@ class ResizeLatentsInvocation(BaseInvocation):
type: Literal["lresize"] = "lresize" type: Literal["lresize"] = "lresize"
# Inputs # Inputs
latents: Optional[LatentsField] = Field(description="The latents to resize") latents: Optional[LatentsField] = Field(description="The latents to resize")
width: int = Field(ge=64, multiple_of=8, description="The width to resize to (px)") width: int = Field(ge=64, multiple_of=8, description="The width to resize to (px)")
height: int = Field(ge=64, multiple_of=8, description="The height to resize to (px)") height: int = Field(ge=64, multiple_of=8, description="The height to resize to (px)")
mode: Optional[LATENTS_INTERPOLATION_MODE] = Field(default="bilinear", description="The interpolation mode") mode: LATENTS_INTERPOLATION_MODE = Field(default="bilinear", description="The interpolation mode")
antialias: Optional[bool] = Field(default=False, description="Whether or not to antialias (applied in bilinear and bicubic modes only)") antialias: bool = Field(default=False, description="Whether or not to antialias (applied in bilinear and bicubic modes only)")
def invoke(self, context: InvocationContext) -> LatentsOutput: def invoke(self, context: InvocationContext) -> LatentsOutput:
latents = context.services.latents.get(self.latents.latents_name) latents = context.services.latents.get(self.latents.latents_name)
@ -438,7 +449,7 @@ class ResizeLatentsInvocation(BaseInvocation):
name = f"{context.graph_execution_state_id}__{self.id}" name = f"{context.graph_execution_state_id}__{self.id}"
context.services.latents.set(name, resized_latents) context.services.latents.set(name, resized_latents)
return LatentsOutput(latents=LatentsField(latents_name=name)) return build_latents_output(latents_name=name, latents=resized_latents)
class ScaleLatentsInvocation(BaseInvocation): class ScaleLatentsInvocation(BaseInvocation):
@ -447,10 +458,10 @@ class ScaleLatentsInvocation(BaseInvocation):
type: Literal["lscale"] = "lscale" type: Literal["lscale"] = "lscale"
# Inputs # Inputs
latents: Optional[LatentsField] = Field(description="The latents to scale") latents: Optional[LatentsField] = Field(description="The latents to scale")
scale_factor: float = Field(gt=0, description="The factor by which to scale the latents") scale_factor: float = Field(gt=0, description="The factor by which to scale the latents")
mode: Optional[LATENTS_INTERPOLATION_MODE] = Field(default="bilinear", description="The interpolation mode") mode: LATENTS_INTERPOLATION_MODE = Field(default="bilinear", description="The interpolation mode")
antialias: Optional[bool] = Field(default=False, description="Whether or not to antialias (applied in bilinear and bicubic modes only)") antialias: bool = Field(default=False, description="Whether or not to antialias (applied in bilinear and bicubic modes only)")
def invoke(self, context: InvocationContext) -> LatentsOutput: def invoke(self, context: InvocationContext) -> LatentsOutput:
latents = context.services.latents.get(self.latents.latents_name) latents = context.services.latents.get(self.latents.latents_name)
@ -468,7 +479,7 @@ class ScaleLatentsInvocation(BaseInvocation):
name = f"{context.graph_execution_state_id}__{self.id}" name = f"{context.graph_execution_state_id}__{self.id}"
context.services.latents.set(name, resized_latents) context.services.latents.set(name, resized_latents)
return LatentsOutput(latents=LatentsField(latents_name=name)) return build_latents_output(latents_name=name, latents=resized_latents)
class ImageToLatentsInvocation(BaseInvocation): class ImageToLatentsInvocation(BaseInvocation):
@ -522,4 +533,4 @@ class ImageToLatentsInvocation(BaseInvocation):
name = f"{context.graph_execution_state_id}__{self.id}" name = f"{context.graph_execution_state_id}__{self.id}"
context.services.latents.set(name, latents) context.services.latents.set(name, latents)
return LatentsOutput(latents=LatentsField(latents_name=name)) return build_latents_output(latents_name=name, latents=latents)

View File

@ -3,6 +3,7 @@
from typing import Literal from typing import Literal
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
import numpy as np
from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationContext, InvocationConfig from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationContext, InvocationConfig
@ -73,3 +74,12 @@ class DivideInvocation(BaseInvocation, MathInvocationConfig):
def invoke(self, context: InvocationContext) -> IntOutput: def invoke(self, context: InvocationContext) -> IntOutput:
return IntOutput(a=int(self.a / self.b)) return IntOutput(a=int(self.a / self.b))
class RandomIntInvocation(BaseInvocation):
"""Outputs a single random integer."""
#fmt: off
type: Literal["rand_int"] = "rand_int"
#fmt: on
def invoke(self, context: InvocationContext) -> IntOutput:
return IntOutput(a=np.random.randint(0, np.iinfo(np.int32).max))

View File

@ -48,13 +48,14 @@ def create_text_to_image() -> LibraryGraph:
def create_system_graphs(graph_library: ItemStorageABC[LibraryGraph]) -> list[LibraryGraph]: def create_system_graphs(graph_library: ItemStorageABC[LibraryGraph]) -> list[LibraryGraph]:
"""Creates the default system graphs, or adds new versions if the old ones don't match""" """Creates the default system graphs, or adds new versions if the old ones don't match"""
# TODO: Uncomment this when we are ready to fix this up to prevent breaking changes
graphs: list[LibraryGraph] = list() graphs: list[LibraryGraph] = list()
# text_to_image = graph_library.get(default_text_to_image_graph_id) # text_to_image = graph_library.get(default_text_to_image_graph_id)
# TODO: Check if the graph is the same as the default one, and if not, update it # # TODO: Check if the graph is the same as the default one, and if not, update it
#if text_to_image is None: # #if text_to_image is None:
text_to_image = create_text_to_image() text_to_image = create_text_to_image()
graph_library.set(text_to_image) graph_library.set(text_to_image)

View File

@ -1,3 +1,4 @@
import time
import traceback import traceback
from threading import Event, Thread, BoundedSemaphore from threading import Event, Thread, BoundedSemaphore
@ -6,6 +7,7 @@ from .invocation_queue import InvocationQueueItem
from .invoker import InvocationProcessorABC, Invoker from .invoker import InvocationProcessorABC, Invoker
from ..models.exceptions import CanceledException from ..models.exceptions import CanceledException
import invokeai.backend.util.logging as logger
class DefaultInvocationProcessor(InvocationProcessorABC): class DefaultInvocationProcessor(InvocationProcessorABC):
__invoker_thread: Thread __invoker_thread: Thread
__stop_event: Event __stop_event: Event
@ -34,8 +36,14 @@ class DefaultInvocationProcessor(InvocationProcessorABC):
try: try:
self.__threadLimit.acquire() self.__threadLimit.acquire()
while not stop_event.is_set(): while not stop_event.is_set():
queue_item: InvocationQueueItem = self.__invoker.services.queue.get() try:
queue_item: InvocationQueueItem = self.__invoker.services.queue.get()
except Exception as e:
logger.debug("Exception while getting from queue: %s" % e)
if not queue_item: # Probably stopping if not queue_item: # Probably stopping
# do not hammer the queue
time.sleep(0.5)
continue continue
graph_execution_state = ( graph_execution_state = (
@ -124,7 +132,16 @@ class DefaultInvocationProcessor(InvocationProcessorABC):
# Queue any further commands if invoking all # Queue any further commands if invoking all
is_complete = graph_execution_state.is_complete() is_complete = graph_execution_state.is_complete()
if queue_item.invoke_all and not is_complete: if queue_item.invoke_all and not is_complete:
self.__invoker.invoke(graph_execution_state, invoke_all=True) try:
self.__invoker.invoke(graph_execution_state, invoke_all=True)
except Exception as e:
logger.error("Error while invoking: %s" % e)
self.__invoker.services.events.emit_invocation_error(
graph_execution_state_id=graph_execution_state.id,
node=invocation.dict(),
source_node_id=source_node_id,
error=traceback.format_exc()
)
elif is_complete: elif is_complete:
self.__invoker.services.events.emit_graph_execution_complete( self.__invoker.services.events.emit_graph_execution_complete(
graph_execution_state.id graph_execution_state.id

View File

@ -108,17 +108,20 @@ APP_VERSION = invokeai.version.__version__
SAMPLER_CHOICES = [ SAMPLER_CHOICES = [
"ddim", "ddim",
"k_dpm_2_a", "ddpm",
"k_dpm_2", "deis",
"k_dpmpp_2_a", "lms",
"k_dpmpp_2",
"k_euler_a",
"k_euler",
"k_heun",
"k_lms",
"plms",
# diffusers:
"pndm", "pndm",
"heun",
"euler",
"euler_k",
"euler_a",
"kdpm_2",
"kdpm_2_a",
"dpmpp_2s",
"dpmpp_2m",
"dpmpp_2m_k",
"unipc",
] ]
PRECISION_CHOICES = [ PRECISION_CHOICES = [
@ -625,7 +628,7 @@ class Args(object):
choices=SAMPLER_CHOICES, choices=SAMPLER_CHOICES,
metavar="SAMPLER_NAME", metavar="SAMPLER_NAME",
help=f'Set the default sampler. Supported samplers: {", ".join(SAMPLER_CHOICES)}', help=f'Set the default sampler. Supported samplers: {", ".join(SAMPLER_CHOICES)}',
default="k_lms", default="lms",
) )
render_group.add_argument( render_group.add_argument(
"--log_tokenization", "--log_tokenization",

View File

@ -37,6 +37,7 @@ 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 .stable_diffusion.schedulers import SCHEDULER_MAP
from .util import choose_precision, choose_torch_device, torch_dtype from .util import choose_precision, choose_torch_device, torch_dtype
def fix_func(orig): def fix_func(orig):
@ -140,7 +141,7 @@ class Generate:
model=None, model=None,
conf="configs/models.yaml", conf="configs/models.yaml",
embedding_path=None, embedding_path=None,
sampler_name="k_lms", sampler_name="lms",
ddim_eta=0.0, # deterministic ddim_eta=0.0, # deterministic
full_precision=False, full_precision=False,
precision="auto", precision="auto",
@ -1050,29 +1051,12 @@ class Generate:
def _set_scheduler(self,model): def _set_scheduler(self,model):
default = model.scheduler default = model.scheduler
# See https://github.com/huggingface/diffusers/issues/277#issuecomment-1371428672 if self.sampler_name in SCHEDULER_MAP:
scheduler_map = dict( sampler_class, sampler_extra_config = SCHEDULER_MAP[self.sampler_name]
ddim=diffusers.DDIMScheduler,
dpmpp_2=diffusers.DPMSolverMultistepScheduler,
k_dpm_2=diffusers.KDPM2DiscreteScheduler,
k_dpm_2_a=diffusers.KDPM2AncestralDiscreteScheduler,
# DPMSolverMultistepScheduler is technically not `k_` anything, as it is neither
# the k-diffusers implementation nor included in EDM (Karras 2022), but we can
# provide an alias for compatibility.
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,
)
if self.sampler_name in scheduler_map:
sampler_class = scheduler_map[self.sampler_name]
msg = ( msg = (
f"Setting Sampler to {self.sampler_name} ({sampler_class.__name__})" f"Setting Sampler to {self.sampler_name} ({sampler_class.__name__})"
) )
self.sampler = sampler_class.from_config(model.scheduler.config) self.sampler = sampler_class.from_config({**model.scheduler.config, **sampler_extra_config})
else: else:
msg = ( msg = (
f" Unsupported Sampler: {self.sampler_name} "+ f" Unsupported Sampler: {self.sampler_name} "+

View File

@ -31,6 +31,7 @@ from ..util.util import rand_perlin_2d
from ..safety_checker import SafetyChecker from ..safety_checker import SafetyChecker
from ..prompting.conditioning import get_uc_and_c_and_ec from ..prompting.conditioning import get_uc_and_c_and_ec
from ..stable_diffusion.diffusers_pipeline import StableDiffusionGeneratorPipeline from ..stable_diffusion.diffusers_pipeline import StableDiffusionGeneratorPipeline
from ..stable_diffusion.schedulers import SCHEDULER_MAP
downsampling = 8 downsampling = 8
@ -71,19 +72,6 @@ class InvokeAIGeneratorOutput:
# we are interposing a wrapper around the original Generator classes so that # we are interposing a wrapper around the original Generator classes so that
# old code that calls Generate will continue to work. # old code that calls Generate will continue to work.
class InvokeAIGenerator(metaclass=ABCMeta): 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, def __init__(self,
model_info: dict, model_info: dict,
params: InvokeAIGeneratorBasicParams=InvokeAIGeneratorBasicParams(), params: InvokeAIGeneratorBasicParams=InvokeAIGeneratorBasicParams(),
@ -175,14 +163,20 @@ class InvokeAIGenerator(metaclass=ABCMeta):
''' '''
Return list of all the schedulers that we currently handle. Return list of all the schedulers that we currently handle.
''' '''
return list(self.scheduler_map.keys()) return list(SCHEDULER_MAP.keys())
def load_generator(self, model: StableDiffusionGeneratorPipeline, generator_class: Type[Generator]): def load_generator(self, model: StableDiffusionGeneratorPipeline, generator_class: Type[Generator]):
return generator_class(model, self.params.precision) return generator_class(model, self.params.precision)
def get_scheduler(self, scheduler_name:str, model: StableDiffusionGeneratorPipeline)->Scheduler: def get_scheduler(self, scheduler_name:str, model: StableDiffusionGeneratorPipeline)->Scheduler:
scheduler_class = self.scheduler_map.get(scheduler_name,'ddim') scheduler_class, scheduler_extra_config = SCHEDULER_MAP.get(scheduler_name, SCHEDULER_MAP['ddim'])
scheduler = scheduler_class.from_config(model.scheduler.config)
scheduler_config = model.scheduler.config
if "_backup" in scheduler_config:
scheduler_config = scheduler_config["_backup"]
scheduler_config = {**scheduler_config, **scheduler_extra_config, "_backup": scheduler_config}
scheduler = scheduler_class.from_config(scheduler_config)
# hack copied over from generate.py # hack copied over from generate.py
if not hasattr(scheduler, 'uses_inpainting_model'): if not hasattr(scheduler, 'uses_inpainting_model'):
scheduler.uses_inpainting_model = lambda: False scheduler.uses_inpainting_model = lambda: False

View File

@ -48,6 +48,7 @@ from diffusers import (
LDMTextToImagePipeline, LDMTextToImagePipeline,
LMSDiscreteScheduler, LMSDiscreteScheduler,
PNDMScheduler, PNDMScheduler,
UniPCMultistepScheduler,
StableDiffusionPipeline, StableDiffusionPipeline,
UNet2DConditionModel, UNet2DConditionModel,
) )
@ -1210,6 +1211,8 @@ def load_pipeline_from_original_stable_diffusion_ckpt(
scheduler = EulerAncestralDiscreteScheduler.from_config(scheduler.config) scheduler = EulerAncestralDiscreteScheduler.from_config(scheduler.config)
elif scheduler_type == "dpm": elif scheduler_type == "dpm":
scheduler = DPMSolverMultistepScheduler.from_config(scheduler.config) scheduler = DPMSolverMultistepScheduler.from_config(scheduler.config)
elif scheduler_type == 'unipc':
scheduler = UniPCMultistepScheduler.from_config(scheduler.config)
elif scheduler_type == "ddim": elif scheduler_type == "ddim":
scheduler = scheduler scheduler = scheduler
else: else:

View File

@ -509,10 +509,13 @@ class StableDiffusionGeneratorPipeline(StableDiffusionPipeline):
run_id=None, run_id=None,
callback: Callable[[PipelineIntermediateState], None] = None, callback: Callable[[PipelineIntermediateState], None] = None,
) -> tuple[torch.Tensor, Optional[AttentionMapSaver]]: ) -> tuple[torch.Tensor, Optional[AttentionMapSaver]]:
if self.scheduler.config.get("cpu_only", False):
scheduler_device = torch.device('cpu')
else:
scheduler_device = self._model_group.device_for(self.unet)
if timesteps is None: if timesteps is None:
self.scheduler.set_timesteps( self.scheduler.set_timesteps(num_inference_steps, device=scheduler_device)
num_inference_steps, device=self._model_group.device_for(self.unet)
)
timesteps = self.scheduler.timesteps timesteps = self.scheduler.timesteps
infer_latents_from_embeddings = GeneratorToCallbackinator( infer_latents_from_embeddings = GeneratorToCallbackinator(
self.generate_latents_from_embeddings, PipelineIntermediateState self.generate_latents_from_embeddings, PipelineIntermediateState
@ -725,12 +728,8 @@ class StableDiffusionGeneratorPipeline(StableDiffusionPipeline):
noise: torch.Tensor, noise: torch.Tensor,
run_id=None, run_id=None,
callback=None, callback=None,
) -> InvokeAIStableDiffusionPipelineOutput: ) -> InvokeAIStableDiffusionPipelineOutput:
timesteps, _ = self.get_img2img_timesteps( timesteps, _ = self.get_img2img_timesteps(num_inference_steps, strength)
num_inference_steps,
strength,
device=self._model_group.device_for(self.unet),
)
result_latents, result_attention_maps = self.latents_from_embeddings( result_latents, result_attention_maps = self.latents_from_embeddings(
latents=initial_latents if strength < 1.0 else torch.zeros_like( latents=initial_latents if strength < 1.0 else torch.zeros_like(
initial_latents, device=initial_latents.device, dtype=initial_latents.dtype initial_latents, device=initial_latents.device, dtype=initial_latents.dtype
@ -756,13 +755,19 @@ class StableDiffusionGeneratorPipeline(StableDiffusionPipeline):
return self.check_for_safety(output, dtype=conditioning_data.dtype) return self.check_for_safety(output, dtype=conditioning_data.dtype)
def get_img2img_timesteps( def get_img2img_timesteps(
self, num_inference_steps: int, strength: float, device self, num_inference_steps: int, strength: float, device=None
) -> (torch.Tensor, int): ) -> (torch.Tensor, int):
img2img_pipeline = StableDiffusionImg2ImgPipeline(**self.components) img2img_pipeline = StableDiffusionImg2ImgPipeline(**self.components)
assert img2img_pipeline.scheduler is self.scheduler assert img2img_pipeline.scheduler is self.scheduler
img2img_pipeline.scheduler.set_timesteps(num_inference_steps, device=device)
if self.scheduler.config.get("cpu_only", False):
scheduler_device = torch.device('cpu')
else:
scheduler_device = self._model_group.device_for(self.unet)
img2img_pipeline.scheduler.set_timesteps(num_inference_steps, device=scheduler_device)
timesteps, adjusted_steps = img2img_pipeline.get_timesteps( timesteps, adjusted_steps = img2img_pipeline.get_timesteps(
num_inference_steps, strength, device=device num_inference_steps, strength, device=scheduler_device
) )
# Workaround for low strength resulting in zero timesteps. # Workaround for low strength resulting in zero timesteps.
# TODO: submit upstream fix for zero-step img2img # TODO: submit upstream fix for zero-step img2img
@ -796,9 +801,7 @@ class StableDiffusionGeneratorPipeline(StableDiffusionPipeline):
if init_image.dim() == 3: if init_image.dim() == 3:
init_image = init_image.unsqueeze(0) init_image = init_image.unsqueeze(0)
timesteps, _ = self.get_img2img_timesteps( timesteps, _ = self.get_img2img_timesteps(num_inference_steps, strength)
num_inference_steps, strength, device=device
)
# 6. Prepare latent variables # 6. Prepare latent variables
# can't quite use upstream StableDiffusionImg2ImgPipeline.prepare_latents # can't quite use upstream StableDiffusionImg2ImgPipeline.prepare_latents

View File

@ -0,0 +1 @@
from .schedulers import SCHEDULER_MAP

View File

@ -0,0 +1,22 @@
from diffusers import DDIMScheduler, DPMSolverMultistepScheduler, KDPM2DiscreteScheduler, \
KDPM2AncestralDiscreteScheduler, EulerDiscreteScheduler, EulerAncestralDiscreteScheduler, \
HeunDiscreteScheduler, LMSDiscreteScheduler, PNDMScheduler, UniPCMultistepScheduler, \
DPMSolverSinglestepScheduler, DEISMultistepScheduler, DDPMScheduler
SCHEDULER_MAP = dict(
ddim=(DDIMScheduler, dict()),
ddpm=(DDPMScheduler, dict()),
deis=(DEISMultistepScheduler, dict()),
lms=(LMSDiscreteScheduler, dict()),
pndm=(PNDMScheduler, dict()),
heun=(HeunDiscreteScheduler, dict()),
euler=(EulerDiscreteScheduler, dict(use_karras_sigmas=False)),
euler_k=(EulerDiscreteScheduler, dict(use_karras_sigmas=True)),
euler_a=(EulerAncestralDiscreteScheduler, dict()),
kdpm_2=(KDPM2DiscreteScheduler, dict()),
kdpm_2_a=(KDPM2AncestralDiscreteScheduler, dict()),
dpmpp_2s=(DPMSolverSinglestepScheduler, dict()),
dpmpp_2m=(DPMSolverMultistepScheduler, dict(use_karras_sigmas=False)),
dpmpp_2m_k=(DPMSolverMultistepScheduler, dict(use_karras_sigmas=True)),
unipc=(UniPCMultistepScheduler, dict(cpu_only=True))
)

View File

@ -4,17 +4,20 @@ from .parse_seed_weights import parse_seed_weights
SAMPLER_CHOICES = [ SAMPLER_CHOICES = [
"ddim", "ddim",
"k_dpm_2_a", "ddpm",
"k_dpm_2", "deis",
"k_dpmpp_2_a", "lms",
"k_dpmpp_2",
"k_euler_a",
"k_euler",
"k_heun",
"k_lms",
"plms",
# diffusers:
"pndm", "pndm",
"heun",
"euler",
"euler_k",
"euler_a",
"kdpm_2",
"kdpm_2_a",
"dpmpp_2s",
"dpmpp_2m",
"dpmpp_2m_k",
"unipc",
] ]

View File

@ -5,6 +5,7 @@ import { PluginOption, UserConfig } from 'vite';
import dts from 'vite-plugin-dts'; import dts from 'vite-plugin-dts';
import eslint from 'vite-plugin-eslint'; import eslint from 'vite-plugin-eslint';
import tsconfigPaths from 'vite-tsconfig-paths'; import tsconfigPaths from 'vite-tsconfig-paths';
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
export const packageConfig: UserConfig = { export const packageConfig: UserConfig = {
base: './', base: './',
@ -16,9 +17,10 @@ export const packageConfig: UserConfig = {
dts({ dts({
insertTypesEntry: true, insertTypesEntry: true,
}), }),
cssInjectedByJsPlugin(),
], ],
build: { build: {
chunkSizeWarningLimit: 1500, cssCodeSplit: true,
lib: { lib: {
entry: path.resolve(__dirname, '../src/index.ts'), entry: path.resolve(__dirname, '../src/index.ts'),
name: 'InvokeAIUI', name: 'InvokeAIUI',
@ -30,6 +32,7 @@ export const packageConfig: UserConfig = {
globals: { globals: {
react: 'React', react: 'React',
'react-dom': 'ReactDOM', 'react-dom': 'ReactDOM',
'@emotion/react': 'EmotionReact',
}, },
}, },
}, },

View File

@ -37,7 +37,7 @@ From `invokeai/frontend/web/` run `yarn install` to get everything set up.
Start everything in dev mode: Start everything in dev mode:
1. Start the dev server: `yarn dev` 1. Start the dev server: `yarn dev`
2. Start the InvokeAI UI per usual: `invokeai --web` 2. Start the InvokeAI Nodes backend: `python scripts/invokeai-new.py --web # run from the repo root`
3. Point your browser to the dev server address e.g. <http://localhost:5173/> 3. Point your browser to the dev server address e.g. <http://localhost:5173/>
### Production builds ### Production builds

View File

@ -145,6 +145,7 @@
"terser": "^5.17.1", "terser": "^5.17.1",
"ts-toolbelt": "^9.6.0", "ts-toolbelt": "^9.6.0",
"vite": "^4.3.3", "vite": "^4.3.3",
"vite-plugin-css-injected-by-js": "^3.1.1",
"vite-plugin-dts": "^2.3.0", "vite-plugin-dts": "^2.3.0",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.2.0", "vite-tsconfig-paths": "^4.2.0",

View File

@ -25,7 +25,7 @@
"common": { "common": {
"hotkeysLabel": "Hotkeys", "hotkeysLabel": "Hotkeys",
"themeLabel": "Theme", "themeLabel": "Theme",
"languagePickerLabel": "Language Picker", "languagePickerLabel": "Language",
"reportBugLabel": "Report Bug", "reportBugLabel": "Report Bug",
"githubLabel": "Github", "githubLabel": "Github",
"discordLabel": "Discord", "discordLabel": "Discord",

View File

@ -7,18 +7,12 @@ 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, Flex, Grid, Portal, useColorMode } from '@chakra-ui/react'; import { Box, Flex, Grid, Portal } from '@chakra-ui/react';
import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants'; import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants';
import GalleryDrawer from 'features/gallery/components/ImageGalleryPanel'; import GalleryDrawer from 'features/gallery/components/ImageGalleryPanel';
import Lightbox from 'features/lightbox/components/Lightbox'; import Lightbox from 'features/lightbox/components/Lightbox';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { import { memo, ReactNode, useCallback, useEffect, useState } from 'react';
memo,
PropsWithChildren,
useCallback,
useEffect,
useState,
} from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import Loading from 'common/components/Loading/Loading'; import Loading from 'common/components/Loading/Loading';
import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady'; import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady';
@ -27,21 +21,24 @@ import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
import { configChanged } from 'features/system/store/configSlice'; import { configChanged } from 'features/system/store/configSlice';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useLogger } from 'app/logging/useLogger'; import { useLogger } from 'app/logging/useLogger';
import ProgressImagePreview from 'features/parameters/components/_ProgressImagePreview';
import ParametersDrawer from 'features/ui/components/ParametersDrawer'; import ParametersDrawer from 'features/ui/components/ParametersDrawer';
import { languageSelector } from 'features/system/store/systemSelectors';
import i18n from 'i18n';
const DEFAULT_CONFIG = {}; const DEFAULT_CONFIG = {};
interface Props extends PropsWithChildren { interface Props {
config?: PartialAppConfig; config?: PartialAppConfig;
headerComponent?: ReactNode;
} }
const App = ({ config = DEFAULT_CONFIG, children }: Props) => { const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
useToastWatcher(); useToastWatcher();
useGlobalHotkeys(); useGlobalHotkeys();
const log = useLogger();
const currentTheme = useAppSelector((state) => state.ui.currentTheme); const language = useAppSelector(languageSelector);
const log = useLogger();
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled; const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
@ -49,18 +46,17 @@ const App = ({ config = DEFAULT_CONFIG, children }: Props) => {
const [loadingOverridden, setLoadingOverridden] = useState(false); const [loadingOverridden, setLoadingOverridden] = useState(false);
const { setColorMode } = useColorMode();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
useEffect(() => {
i18n.changeLanguage(language);
}, [language]);
useEffect(() => { useEffect(() => {
log.info({ namespace: 'App', data: config }, 'Received config'); log.info({ namespace: 'App', data: config }, 'Received config');
dispatch(configChanged(config)); dispatch(configChanged(config));
}, [dispatch, config, log]); }, [dispatch, config, log]);
useEffect(() => {
setColorMode(['light'].includes(currentTheme) ? 'light' : 'dark');
}, [setColorMode, currentTheme]);
const handleOverrideClicked = useCallback(() => { const handleOverrideClicked = useCallback(() => {
setLoadingOverridden(true); setLoadingOverridden(true);
}, []); }, []);
@ -77,7 +73,7 @@ const App = ({ config = DEFAULT_CONFIG, children }: Props) => {
w={APP_WIDTH} w={APP_WIDTH}
h={APP_HEIGHT} h={APP_HEIGHT}
> >
{children || <SiteHeader />} {headerComponent || <SiteHeader />}
<Flex <Flex
gap={4} gap={4}
w={{ base: '100vw', xl: 'full' }} w={{ base: '100vw', xl: 'full' }}

View File

@ -1,16 +1,13 @@
import React, { lazy, memo, PropsWithChildren, useEffect } from 'react'; import React, {
lazy,
memo,
PropsWithChildren,
ReactNode,
useEffect,
} from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { store } from 'app/store/store'; import { store } from 'app/store/store';
import { OpenAPI } from 'services/api'; import { OpenAPI } from 'services/api';
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';
import Loading from '../../common/components/Loading/Loading'; import Loading from '../../common/components/Loading/Loading';
import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares'; import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares';
@ -26,9 +23,10 @@ interface Props extends PropsWithChildren {
apiUrl?: string; apiUrl?: string;
token?: string; token?: string;
config?: PartialAppConfig; config?: PartialAppConfig;
headerComponent?: ReactNode;
} }
const InvokeAIUI = ({ apiUrl, token, config, children }: Props) => { const InvokeAIUI = ({ apiUrl, token, config, headerComponent }: Props) => {
useEffect(() => { useEffect(() => {
// configure API client token // configure API client token
if (token) { if (token) {
@ -57,7 +55,7 @@ const InvokeAIUI = ({ apiUrl, token, config, children }: Props) => {
<Provider store={store}> <Provider store={store}>
<React.Suspense fallback={<Loading />}> <React.Suspense fallback={<Loading />}>
<ThemeLocaleProvider> <ThemeLocaleProvider>
<App config={config}>{children}</App> <App config={config} headerComponent={headerComponent} />
</ThemeLocaleProvider> </ThemeLocaleProvider>
</React.Suspense> </React.Suspense>
</Provider> </Provider>

View File

@ -1,4 +1,8 @@
import { ChakraProvider, extendTheme } from '@chakra-ui/react'; import {
ChakraProvider,
createLocalStorageManager,
extendTheme,
} from '@chakra-ui/react';
import { ReactNode, useEffect } from 'react'; import { ReactNode, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { theme as invokeAITheme } from 'theme/theme'; import { theme as invokeAITheme } from 'theme/theme';
@ -9,15 +13,8 @@ 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/variable.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';
import 'overlayscrollbars/overlayscrollbars.css'; import 'overlayscrollbars/overlayscrollbars.css';
import 'theme/css/overlayscrollbars.css'; import 'theme/css/overlayscrollbars.css';
@ -32,6 +29,8 @@ const THEMES = {
ocean: oceanBlueColors, ocean: oceanBlueColors,
}; };
const manager = createLocalStorageManager('@@invokeai-color-mode');
function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) { function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
@ -51,7 +50,11 @@ function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) {
document.body.dir = direction; document.body.dir = direction;
}, [direction]); }, [direction]);
return <ChakraProvider theme={theme}>{children}</ChakraProvider>; return (
<ChakraProvider theme={theme} colorModeManager={manager}>
{children}
</ChakraProvider>
);
} }
export default ThemeLocaleProvider; export default ThemeLocaleProvider;

View File

@ -2,17 +2,28 @@
export const DIFFUSERS_SCHEDULERS: Array<string> = [ export const DIFFUSERS_SCHEDULERS: Array<string> = [
'ddim', 'ddim',
'plms', 'ddpm',
'k_lms', 'deis',
'dpmpp_2', 'lms',
'k_dpm_2', 'pndm',
'k_dpm_2_a', 'heun',
'k_dpmpp_2', 'euler',
'k_euler', 'euler_k',
'k_euler_a', 'euler_a',
'k_heun', 'kdpm_2',
'kdpm_2_a',
'dpmpp_2s',
'dpmpp_2m',
'dpmpp_2m_k',
'unipc',
]; ];
export const IMG2IMG_DIFFUSERS_SCHEDULERS = DIFFUSERS_SCHEDULERS.filter(
(scheduler) => {
return scheduler !== 'dpmpp_2s';
}
);
// Valid image widths // Valid image widths
export const WIDTHS: Array<number> = Array.from(Array(64)).map( export const WIDTHS: Array<number> = Array.from(Array(64)).map(
(_x, i) => (i + 1) * 64 (_x, i) => (i + 1) * 64

View File

@ -6,9 +6,12 @@ import { imageUploaded } from 'services/thunks/image';
export const addImageUploadedListener = () => { export const addImageUploadedListener = () => {
startAppListening({ startAppListening({
actionCreator: imageUploaded.fulfilled, predicate: (action): action is ReturnType<typeof imageUploaded.fulfilled> =>
imageUploaded.fulfilled.match(action) &&
action.payload.response.image_type !== 'intermediates',
effect: (action, { dispatch, getState }) => { effect: (action, { dispatch, getState }) => {
const { response } = action.payload; const { response } = action.payload;
const state = getState(); const state = getState();
const image = deserializeImageResponse(response); const image = deserializeImageResponse(response);

View File

@ -47,15 +47,20 @@ export type CommonGeneratedImageMetadata = {
postprocessing: null | Array<ESRGANMetadata | FacetoolMetadata>; postprocessing: null | Array<ESRGANMetadata | FacetoolMetadata>;
sampler: sampler:
| 'ddim' | 'ddim'
| 'k_dpm_2_a' | 'ddpm'
| 'k_dpm_2' | 'deis'
| 'k_dpmpp_2_a' | 'lms'
| 'k_dpmpp_2' | 'pndm'
| 'k_euler_a' | 'heun'
| 'k_euler' | 'euler'
| 'k_heun' | 'euler_k'
| 'k_lms' | 'euler_a'
| 'plms'; | 'kdpm_2'
| 'kdpm_2_a'
| 'dpmpp_2s'
| 'dpmpp_2m'
| 'dpmpp_2m_k'
| 'unipc';
prompt: Prompt; prompt: Prompt;
seed: number; seed: number;
variations: SeedWeights; variations: SeedWeights;
@ -321,11 +326,11 @@ export type AppFeature =
/** /**
* A disable-able Stable Diffusion feature * A disable-able Stable Diffusion feature
*/ */
export type StableDiffusionFeature = export type SDFeature =
| 'noiseConfig' | 'noise'
| 'variations' | 'variation'
| 'symmetry' | 'symmetry'
| 'tiling' | 'seamless'
| 'hires'; | 'hires';
/** /**
@ -343,6 +348,7 @@ export type AppConfig = {
shouldFetchImages: boolean; shouldFetchImages: boolean;
disabledTabs: InvokeTabName[]; disabledTabs: InvokeTabName[];
disabledFeatures: AppFeature[]; disabledFeatures: AppFeature[];
disabledSDFeatures: SDFeature[];
canRestoreDeletedImagesFromBin: boolean; canRestoreDeletedImagesFromBin: boolean;
sd: { sd: {
iterations: { iterations: {

View File

@ -0,0 +1,54 @@
import { Badge, Flex } from '@chakra-ui/react';
import { Image } from 'app/types/invokeai';
import { isNumber, isString } from 'lodash-es';
import { useMemo } from 'react';
type ImageMetadataOverlayProps = {
image: Image;
};
const ImageMetadataOverlay = ({ image }: ImageMetadataOverlayProps) => {
const dimensions = useMemo(() => {
if (!isNumber(image.metadata?.width) || isNumber(!image.metadata?.height)) {
return;
}
return `${image.metadata?.width} × ${image.metadata?.height}`;
}, [image.metadata]);
const model = useMemo(() => {
if (!isString(image.metadata?.invokeai?.node?.model)) {
return;
}
return image.metadata?.invokeai?.node?.model;
}, [image.metadata]);
return (
<Flex
sx={{
pointerEvents: 'none',
flexDirection: 'column',
position: 'absolute',
top: 0,
right: 0,
p: 2,
alignItems: 'flex-end',
gap: 2,
}}
>
{dimensions && (
<Badge variant="solid" colorScheme="base">
{dimensions}
</Badge>
)}
{model && (
<Badge variant="solid" colorScheme="base">
{model}
</Badge>
)}
</Flex>
);
};
export default ImageMetadataOverlay;

View File

@ -1,37 +0,0 @@
import { Badge, Box, Flex } from '@chakra-ui/react';
import { Image } from 'app/types/invokeai';
type ImageToImageOverlayProps = {
image: Image;
};
const ImageToImageOverlay = ({ image }: ImageToImageOverlayProps) => {
return (
<Box
sx={{
top: 0,
left: 0,
w: 'full',
h: 'full',
position: 'absolute',
pointerEvents: 'none',
}}
>
<Flex
sx={{
position: 'absolute',
top: 0,
right: 0,
p: 2,
alignItems: 'flex-start',
}}
>
<Badge variant="solid" colorScheme="base">
{image.metadata?.width} × {image.metadata?.height}
</Badge>
</Flex>
</Box>
);
};
export default ImageToImageOverlay;

View File

@ -152,6 +152,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
} = useAppSelector(currentImageButtonsSelector); } = useAppSelector(currentImageButtonsSelector);
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled; const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled; const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled;
const isFaceRestoreEnabled = useFeatureStatus('faceRestore').isFeatureEnabled; const isFaceRestoreEnabled = useFeatureStatus('faceRestore').isFeatureEnabled;
@ -429,13 +430,15 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
> >
{t('parameters.sendToImg2Img')} {t('parameters.sendToImg2Img')}
</IAIButton> </IAIButton>
<IAIButton {isCanvasEnabled && (
size="sm" <IAIButton
onClick={handleSendToCanvas} size="sm"
leftIcon={<FaShare />} onClick={handleSendToCanvas}
> leftIcon={<FaShare />}
{t('parameters.sendToUnifiedCanvas')} >
</IAIButton> {t('parameters.sendToUnifiedCanvas')}
</IAIButton>
)}
{/* <IAIButton {/* <IAIButton
size="sm" size="sm"

View File

@ -1,4 +1,4 @@
import { Box, Flex, Image, Skeleton, useBoolean } 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/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { useGetUrl } from 'common/util/getUrl'; import { useGetUrl } from 'common/util/getUrl';
@ -11,7 +11,8 @@ import NextPrevImageButtons from './NextPrevImageButtons';
import CurrentImageHidden from './CurrentImageHidden'; import CurrentImageHidden from './CurrentImageHidden';
import { DragEvent, memo, useCallback } from 'react'; import { DragEvent, memo, useCallback } from 'react';
import { systemSelector } from 'features/system/store/systemSelectors'; import { systemSelector } from 'features/system/store/systemSelectors';
import CurrentImageFallback from './CurrentImageFallback'; import ImageFallbackSpinner from './ImageFallbackSpinner';
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
export const imagesSelector = createSelector( export const imagesSelector = createSelector(
[uiSelector, gallerySelector, systemSelector], [uiSelector, gallerySelector, systemSelector],
@ -50,8 +51,6 @@ const CurrentImagePreview = () => {
} = useAppSelector(imagesSelector); } = useAppSelector(imagesSelector);
const { getUrl } = useGetUrl(); const { getUrl } = useGetUrl();
const [isLoaded, { on, off }] = useBoolean();
const handleDragStart = useCallback( const handleDragStart = useCallback(
(e: DragEvent<HTMLDivElement>) => { (e: DragEvent<HTMLDivElement>) => {
if (!image) { if (!image) {
@ -67,11 +66,11 @@ const CurrentImagePreview = () => {
return ( return (
<Flex <Flex
sx={{ sx={{
position: 'relative',
justifyContent: 'center',
alignItems: 'center',
width: '100%', width: '100%',
height: '100%', height: '100%',
position: 'relative',
alignItems: 'center',
justifyContent: 'center',
}} }}
> >
{progressImage && shouldShowProgressInViewer ? ( {progressImage && shouldShowProgressInViewer ? (
@ -91,28 +90,23 @@ const CurrentImagePreview = () => {
/> />
) : ( ) : (
image && ( image && (
<Image <>
onDragStart={handleDragStart} <Image
fallbackStrategy="beforeLoadOrError" src={getUrl(image.url)}
src={shouldHidePreview ? undefined : getUrl(image.url)} fallbackStrategy="beforeLoadOrError"
width={image.metadata.width || 'auto'} fallback={<ImageFallbackSpinner />}
height={image.metadata.height || 'auto'} onDragStart={handleDragStart}
fallback={ sx={{
shouldHidePreview ? ( objectFit: 'contain',
<CurrentImageHidden /> maxWidth: '100%',
) : ( maxHeight: '100%',
<CurrentImageFallback /> height: 'auto',
) position: 'absolute',
} borderRadius: 'base',
sx={{ }}
objectFit: 'contain', />
maxWidth: '100%', <ImageMetadataOverlay image={image} />
maxHeight: '100%', </>
height: 'auto',
position: 'absolute',
borderRadius: 'base',
}}
/>
) )
)} )}
{shouldShowImageDetails && image && 'metadata' in image && ( {shouldShowImageDetails && image && 'metadata' in image && (

View File

@ -1,4 +1,4 @@
import { Box, Flex, Image } from '@chakra-ui/react'; import { Flex, Image, Spinner } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
@ -42,6 +42,7 @@ const GalleryProgressImage = () => {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
aspectRatio: '1/1', aspectRatio: '1/1',
position: 'relative',
}} }}
> >
<Image <Image
@ -61,6 +62,7 @@ const GalleryProgressImage = () => {
imageRendering: shouldAntialiasProgressImage ? 'auto' : 'pixelated', imageRendering: shouldAntialiasProgressImage ? 'auto' : 'pixelated',
}} }}
/> />
<Spinner sx={{ position: 'absolute', top: 1, right: 1, opacity: 0.7 }} />
</Flex> </Flex>
); );
}; };

View File

@ -104,7 +104,10 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const toast = useToast(); const toast = useToast();
const { t } = useTranslation(); const { t } = useTranslation();
const { isFeatureEnabled: isLightboxEnabled } = useFeatureStatus('lightbox');
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
const { recallSeed, recallPrompt, recallInitialImage, recallAllParameters } = const { recallSeed, recallPrompt, recallInitialImage, recallAllParameters } =
useParameters(); useParameters();
@ -250,9 +253,11 @@ const HoverableImage = memo((props: HoverableImageProps) => {
> >
{t('parameters.sendToImg2Img')} {t('parameters.sendToImg2Img')}
</MenuItem> </MenuItem>
<MenuItem icon={<FaShare />} onClickCapture={handleSendToCanvas}> {isCanvasEnabled && (
{t('parameters.sendToUnifiedCanvas')} <MenuItem icon={<FaShare />} onClickCapture={handleSendToCanvas}>
</MenuItem> {t('parameters.sendToUnifiedCanvas')}
</MenuItem>
)}
<MenuItem icon={<FaTrash />} onClickCapture={onDeleteDialogOpen}> <MenuItem icon={<FaTrash />} onClickCapture={onDeleteDialogOpen}>
{t('gallery.deleteImage')} {t('gallery.deleteImage')}
</MenuItem> </MenuItem>
@ -278,6 +283,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
h: 'full', h: 'full',
transition: 'transform 0.2s ease-out', transition: 'transform 0.2s ease-out',
aspectRatio: '1/1', aspectRatio: '1/1',
cursor: 'pointer',
}} }}
> >
<Image <Image

View File

@ -1,8 +1,8 @@
import { Flex, Spinner, SpinnerProps } from '@chakra-ui/react'; import { Flex, Spinner, SpinnerProps } from '@chakra-ui/react';
type CurrentImageFallbackProps = SpinnerProps; type ImageFallbackSpinnerProps = SpinnerProps;
const CurrentImageFallback = (props: CurrentImageFallbackProps) => { const ImageFallbackSpinner = (props: ImageFallbackSpinnerProps) => {
const { size = 'xl', ...rest } = props; const { size = 'xl', ...rest } = props;
return ( return (
@ -21,4 +21,4 @@ const CurrentImageFallback = (props: CurrentImageFallbackProps) => {
); );
}; };
export default CurrentImageFallback; export default ImageFallbackSpinner;

View File

@ -54,11 +54,7 @@ import { uploadsAdapter } from '../store/uploadsSlice';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { Virtuoso, VirtuosoGrid } from 'react-virtuoso'; import { Virtuoso, VirtuosoGrid } from 'react-virtuoso';
import ProgressImagePreview from 'features/parameters/components/_ProgressImagePreview';
import ProgressImage from 'features/parameters/components/ProgressImage';
import { systemSelector } from 'features/system/store/systemSelectors';
import { Image as ImageType } from 'app/types/invokeai'; import { Image as ImageType } from 'app/types/invokeai';
import { ProgressImage as ProgressImageType } from 'services/events/types';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import GalleryProgressImage from './GalleryProgressImage'; import GalleryProgressImage from './GalleryProgressImage';
@ -71,13 +67,13 @@ const selector = createSelector(
const { results, uploads, system, gallery } = state; const { results, uploads, system, gallery } = state;
const { currentCategory } = gallery; const { currentCategory } = gallery;
const tempImages: (ImageType | typeof PROGRESS_IMAGE_PLACEHOLDER)[] = [];
if (system.progressImage) {
tempImages.push(PROGRESS_IMAGE_PLACEHOLDER);
}
if (currentCategory === 'results') { if (currentCategory === 'results') {
const tempImages: (ImageType | typeof PROGRESS_IMAGE_PLACEHOLDER)[] = [];
if (system.progressImage) {
tempImages.push(PROGRESS_IMAGE_PLACEHOLDER);
}
return { return {
images: tempImages.concat( images: tempImages.concat(
resultsAdapter.getSelectors().selectAll(results) resultsAdapter.getSelectors().selectAll(results)
@ -88,9 +84,7 @@ const selector = createSelector(
} }
return { return {
images: tempImages.concat( images: uploadsAdapter.getSelectors().selectAll(uploads),
uploadsAdapter.getSelectors().selectAll(uploads)
),
isLoading: uploads.isLoading, isLoading: uploads.isLoading,
areMoreImagesAvailable: uploads.page < uploads.pages - 1, areMoreImagesAvailable: uploads.page < uploads.pages - 1,
}; };

View File

@ -1,6 +1,11 @@
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import { Image } from 'app/types/invokeai'; import { Image } from 'app/types/invokeai';
import { imageReceived, thumbnailReceived } from 'services/thunks/image';
import {
receivedResultImagesPage,
receivedUploadImagesPage,
} from '../../../services/thunks/gallery';
type GalleryImageObjectFitType = 'contain' | 'cover'; type GalleryImageObjectFitType = 'contain' | 'cover';
@ -63,6 +68,53 @@ export const gallerySlice = createSlice({
state.shouldUseSingleGalleryColumn = action.payload; state.shouldUseSingleGalleryColumn = action.payload;
}, },
}, },
extraReducers(builder) {
builder.addCase(imageReceived.fulfilled, (state, action) => {
// When we get an updated URL for an image, we need to update the selectedImage in gallery,
// which is currently its own object (instead of a reference to an image in results/uploads)
const { imagePath } = action.payload;
const { imageName } = action.meta.arg;
if (state.selectedImage?.name === imageName) {
state.selectedImage.url = imagePath;
}
});
builder.addCase(thumbnailReceived.fulfilled, (state, action) => {
// When we get an updated URL for an image, we need to update the selectedImage in gallery,
// which is currently its own object (instead of a reference to an image in results/uploads)
const { thumbnailPath } = action.payload;
const { thumbnailName } = action.meta.arg;
if (state.selectedImage?.name === thumbnailName) {
state.selectedImage.thumbnail = thumbnailPath;
}
});
builder.addCase(receivedResultImagesPage.fulfilled, (state, action) => {
// rehydrate selectedImage URL when results list comes in
// solves case when outdated URL is in local storage
if (state.selectedImage) {
const selectedImageInResults = action.payload.items.find(
(image) => image.image_name === state.selectedImage!.name
);
if (selectedImageInResults) {
state.selectedImage.url = selectedImageInResults.image_url;
}
}
});
builder.addCase(receivedUploadImagesPage.fulfilled, (state, action) => {
// rehydrate selectedImage URL when results list comes in
// solves case when outdated URL is in local storage
if (state.selectedImage) {
const selectedImageInResults = action.payload.items.find(
(image) => image.image_name === state.selectedImage!.name
);
if (selectedImageInResults) {
state.selectedImage.url = selectedImageInResults.image_url;
}
}
});
},
}); });
export const { export const {

View File

@ -20,7 +20,7 @@ export const iterationGraph = {
model: '', model: '',
progress_images: false, progress_images: false,
prompt: 'dog', prompt: 'dog',
sampler_name: 'k_lms', sampler_name: 'lms',
seamless: false, seamless: false,
steps: 11, steps: 11,
type: 'txt2img', type: 'txt2img',

View File

@ -1,8 +1,12 @@
import { DIFFUSERS_SCHEDULERS } from 'app/constants'; import {
DIFFUSERS_SCHEDULERS,
IMG2IMG_DIFFUSERS_SCHEDULERS,
} from 'app/constants';
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAISelect from 'common/components/IAISelect'; import IAISelect from 'common/components/IAISelect';
import { setSampler } from 'features/parameters/store/generationSlice'; import { setSampler } from 'features/parameters/store/generationSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { ChangeEvent, memo, useCallback } from 'react'; import { ChangeEvent, memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -10,6 +14,9 @@ const ParamSampler = () => {
const sampler = useAppSelector( const sampler = useAppSelector(
(state: RootState) => state.generation.sampler (state: RootState) => state.generation.sampler
); );
const activeTabName = useAppSelector(activeTabNameSelector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
@ -23,7 +30,11 @@ const ParamSampler = () => {
label={t('parameters.sampler')} label={t('parameters.sampler')}
value={sampler} value={sampler}
onChange={handleChange} onChange={handleChange}
validValues={DIFFUSERS_SCHEDULERS} validValues={
activeTabName === 'img2img' || activeTabName == 'unifiedCanvas'
? IMG2IMG_DIFFUSERS_SCHEDULERS
: DIFFUSERS_SCHEDULERS
}
minWidth={36} minWidth={36}
/> />
); );

View File

@ -6,6 +6,7 @@ import IAICollapse from 'common/components/IAICollapse';
import { memo } from 'react'; import { memo } from 'react';
import { ParamHiresStrength } from './ParamHiresStrength'; import { ParamHiresStrength } from './ParamHiresStrength';
import { setHiresFix } from 'features/parameters/store/postprocessingSlice'; import { setHiresFix } from 'features/parameters/store/postprocessingSlice';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
const ParamHiresCollapse = () => { const ParamHiresCollapse = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -13,10 +14,16 @@ const ParamHiresCollapse = () => {
(state: RootState) => state.postprocessing.hiresFix (state: RootState) => state.postprocessing.hiresFix
); );
const isHiresEnabled = useFeatureStatus('hires').isFeatureEnabled;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const handleToggle = () => dispatch(setHiresFix(!hiresFix)); const handleToggle = () => dispatch(setHiresFix(!hiresFix));
if (!isHiresEnabled) {
return null;
}
return ( return (
<IAICollapse <IAICollapse
label={t('parameters.hiresOptim')} label={t('parameters.hiresOptim')}

View File

@ -47,7 +47,7 @@ const ImageToImageStrength = () => {
return ( return (
<IAISlider <IAISlider
label={`${t('parameters.strength')}`} label={`${t('parameters.denoisingStrength')}`}
step={step} step={step}
min={min} min={min}
max={sliderMax} max={sliderMax}

View File

@ -1,17 +1,18 @@
import { Flex, Image, Spinner } from '@chakra-ui/react'; import { Flex, Image } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder'; import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder';
import { useGetUrl } from 'common/util/getUrl'; import { useGetUrl } from 'common/util/getUrl';
import { clearInitialImage } from 'features/parameters/store/generationSlice'; import { clearInitialImage } from 'features/parameters/store/generationSlice';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { DragEvent, useCallback, useState } from 'react'; import { DragEvent, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ImageType } from 'services/api'; import { ImageType } from 'services/api';
import ImageToImageOverlay from 'common/components/ImageToImageOverlay'; import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
import { generationSelector } from 'features/parameters/store/generationSelectors'; import { generationSelector } from 'features/parameters/store/generationSelectors';
import { initialImageSelected } from 'features/parameters/store/actions'; import { initialImageSelected } from 'features/parameters/store/actions';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import ImageFallbackSpinner from 'features/gallery/components/ImageFallbackSpinner';
const selector = createSelector( const selector = createSelector(
[generationSelector], [generationSelector],
@ -30,8 +31,6 @@ const InitialImagePreview = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const [isLoaded, setIsLoaded] = useState(false);
const onError = () => { const onError = () => {
dispatch( dispatch(
addToast({ addToast({
@ -42,13 +41,10 @@ const InitialImagePreview = () => {
}) })
); );
dispatch(clearInitialImage()); dispatch(clearInitialImage());
setIsLoaded(false);
}; };
const handleDrop = useCallback( const handleDrop = useCallback(
(e: DragEvent<HTMLDivElement>) => { (e: DragEvent<HTMLDivElement>) => {
setIsLoaded(false);
const name = e.dataTransfer.getData('invokeai/imageName'); const name = e.dataTransfer.getData('invokeai/imageName');
const type = e.dataTransfer.getData('invokeai/imageType') as ImageType; const type = e.dataTransfer.getData('invokeai/imageType') as ImageType;
@ -62,48 +58,32 @@ const InitialImagePreview = () => {
sx={{ sx={{
width: 'full', width: 'full',
height: 'full', height: 'full',
position: 'relative',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
position: 'relative',
}} }}
onDrop={handleDrop} onDrop={handleDrop}
> >
<Flex {initialImage?.url && (
sx={{ <>
height: 'full', <Image
width: 'full', src={getUrl(initialImage?.url)}
blur: '5px', fallbackStrategy="beforeLoadOrError"
position: 'relative', fallback={<ImageFallbackSpinner />}
alignItems: 'center', onError={onError}
justifyContent: 'center', sx={{
}} objectFit: 'contain',
> maxWidth: '100%',
{initialImage?.url && ( maxHeight: '100%',
<> height: 'auto',
<Image position: 'absolute',
sx={{ borderRadius: 'base',
objectFit: 'contain', }}
borderRadius: 'base', />
maxHeight: 'full', <ImageMetadataOverlay image={initialImage} />
}} </>
src={getUrl(initialImage?.url)} )}
onError={onError} {!initialImage?.url && <SelectImagePlaceholder />}
onLoad={() => {
setIsLoaded(true);
}}
fallback={
<Flex
sx={{ h: 36, alignItems: 'center', justifyContent: 'center' }}
>
<Spinner color="grey" w="5rem" h="5rem" />
</Flex>
}
/>
{isLoaded && <ImageToImageOverlay image={initialImage} />}
</>
)}
{!initialImage?.url && <SelectImagePlaceholder />}
</Flex>
</Flex> </Flex>
); );
}; };

View File

@ -7,9 +7,13 @@ import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { setShouldUseNoiseSettings } from 'features/parameters/store/generationSlice'; import { setShouldUseNoiseSettings } from 'features/parameters/store/generationSlice';
import { memo } from 'react'; import { memo } from 'react';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
const ParamNoiseCollapse = () => { const ParamNoiseCollapse = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const isNoiseEnabled = useFeatureStatus('noise').isFeatureEnabled;
const shouldUseNoiseSettings = useAppSelector( const shouldUseNoiseSettings = useAppSelector(
(state: RootState) => state.generation.shouldUseNoiseSettings (state: RootState) => state.generation.shouldUseNoiseSettings
); );
@ -19,6 +23,10 @@ const ParamNoiseCollapse = () => {
const handleToggle = () => const handleToggle = () =>
dispatch(setShouldUseNoiseSettings(!shouldUseNoiseSettings)); dispatch(setShouldUseNoiseSettings(!shouldUseNoiseSettings));
if (!isNoiseEnabled) {
return null;
}
return ( return (
<IAICollapse <IAICollapse
label={t('parameters.noiseSettings')} label={t('parameters.noiseSettings')}

View File

@ -9,6 +9,7 @@ import { generationSelector } from 'features/parameters/store/generationSelector
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import ParamSeamlessXAxis from './ParamSeamlessXAxis'; import ParamSeamlessXAxis from './ParamSeamlessXAxis';
import ParamSeamlessYAxis from './ParamSeamlessYAxis'; import ParamSeamlessYAxis from './ParamSeamlessYAxis';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
const selector = createSelector( const selector = createSelector(
generationSelector, generationSelector,
@ -24,10 +25,16 @@ const ParamSeamlessCollapse = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { shouldUseSeamless } = useAppSelector(selector); const { shouldUseSeamless } = useAppSelector(selector);
const isSeamlessEnabled = useFeatureStatus('seamless').isFeatureEnabled;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const handleToggle = () => dispatch(setSeamless(!shouldUseSeamless)); const handleToggle = () => dispatch(setSeamless(!shouldUseSeamless));
if (!isSeamlessEnabled) {
return null;
}
return ( return (
<IAICollapse <IAICollapse
label={t('parameters.seamlessTiling')} label={t('parameters.seamlessTiling')}

View File

@ -8,6 +8,7 @@ import IAICollapse from 'common/components/IAICollapse';
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { setShouldUseSymmetry } from 'features/parameters/store/generationSlice'; import { setShouldUseSymmetry } from 'features/parameters/store/generationSlice';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
const ParamSymmetryCollapse = () => { const ParamSymmetryCollapse = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -15,10 +16,16 @@ const ParamSymmetryCollapse = () => {
(state: RootState) => state.generation.shouldUseSymmetry (state: RootState) => state.generation.shouldUseSymmetry
); );
const isSymmetryEnabled = useFeatureStatus('symmetry').isFeatureEnabled;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const handleToggle = () => dispatch(setShouldUseSymmetry(!shouldUseSymmetry)); const handleToggle = () => dispatch(setShouldUseSymmetry(!shouldUseSymmetry));
if (!isSymmetryEnabled) {
return null;
}
return ( return (
<IAICollapse <IAICollapse
label={t('parameters.symmetry')} label={t('parameters.symmetry')}

View File

@ -7,6 +7,7 @@ import { setShouldGenerateVariations } from 'features/parameters/store/generatio
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import IAICollapse from 'common/components/IAICollapse'; import IAICollapse from 'common/components/IAICollapse';
import { memo } from 'react'; import { memo } from 'react';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
const ParamVariationCollapse = () => { const ParamVariationCollapse = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -14,11 +15,17 @@ const ParamVariationCollapse = () => {
(state: RootState) => state.generation.shouldGenerateVariations (state: RootState) => state.generation.shouldGenerateVariations
); );
const isVariationEnabled = useFeatureStatus('variation').isFeatureEnabled;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const handleToggle = () => const handleToggle = () =>
dispatch(setShouldGenerateVariations(!shouldGenerateVariations)); dispatch(setShouldGenerateVariations(!shouldGenerateVariations));
if (!isVariationEnabled) {
return null;
}
return ( return (
<IAICollapse <IAICollapse
label={t('parameters.variations')} label={t('parameters.variations')}

View File

@ -51,7 +51,7 @@ export const initialGenerationState: GenerationState = {
perlin: 0, perlin: 0,
prompt: '', prompt: '',
negativePrompt: '', negativePrompt: '',
sampler: 'k_lms', sampler: 'lms',
seamBlur: 16, seamBlur: 16,
seamSize: 96, seamSize: 96,
seamSteps: 30, seamSteps: 30,

View File

@ -1,73 +1,69 @@
import type { ReactNode } from 'react'; import {
IconButton,
import { VStack } from '@chakra-ui/react'; Menu,
import IAIButton from 'common/components/IAIButton'; MenuButton,
import IAIIconButton from 'common/components/IAIIconButton'; MenuItemOption,
import IAIPopover from 'common/components/IAIPopover'; MenuList,
MenuOptionGroup,
Tooltip,
} from '@chakra-ui/react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaCheck, FaLanguage } from 'react-icons/fa'; import i18n from 'i18n';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { languageSelector } from '../store/systemSelectors';
import { languageChanged } from '../store/systemSlice';
import { map } from 'lodash-es';
import { IoLanguage } from 'react-icons/io5';
export const LANGUAGES = {
ar: i18n.t('common.langArabic', { lng: 'ar' }),
nl: i18n.t('common.langDutch', { lng: 'nl' }),
en: i18n.t('common.langEnglish', { lng: 'en' }),
fr: i18n.t('common.langFrench', { lng: 'fr' }),
de: i18n.t('common.langGerman', { lng: 'de' }),
he: i18n.t('common.langHebrew', { lng: 'he' }),
it: i18n.t('common.langItalian', { lng: 'it' }),
ja: i18n.t('common.langJapanese', { lng: 'ja' }),
ko: i18n.t('common.langKorean', { lng: 'ko' }),
pl: i18n.t('common.langPolish', { lng: 'pl' }),
pt_BR: i18n.t('common.langBrPortuguese', { lng: 'pt_BR' }),
pt: i18n.t('common.langPortuguese', { lng: 'pt' }),
ru: i18n.t('common.langRussian', { lng: 'ru' }),
zh_CN: i18n.t('common.langSimplifiedChinese', { lng: 'zh_CN' }),
es: i18n.t('common.langSpanish', { lng: 'es' }),
uk: i18n.t('common.langUkranian', { lng: 'ua' }),
};
export default function LanguagePicker() { export default function LanguagePicker() {
const { t, i18n } = useTranslation(); const { t } = useTranslation();
const LANGUAGES = { const dispatch = useAppDispatch();
ar: t('common.langArabic', { lng: 'ar' }), const language = useAppSelector(languageSelector);
nl: t('common.langDutch', { lng: 'nl' }),
en: t('common.langEnglish', { lng: 'en' }),
fr: t('common.langFrench', { lng: 'fr' }),
de: t('common.langGerman', { lng: 'de' }),
he: t('common.langHebrew', { lng: 'he' }),
it: t('common.langItalian', { lng: 'it' }),
ja: t('common.langJapanese', { lng: 'ja' }),
ko: t('common.langKorean', { lng: 'ko' }),
pl: t('common.langPolish', { lng: 'pl' }),
pt_BR: t('common.langBrPortuguese', { lng: 'pt_BR' }),
pt: t('common.langPortuguese', { lng: 'pt' }),
ru: t('common.langRussian', { lng: 'ru' }),
zh_CN: t('common.langSimplifiedChinese', { lng: 'zh_CN' }),
es: t('common.langSpanish', { lng: 'es' }),
uk: t('common.langUkranian', { lng: 'ua' }),
};
const renderLanguagePicker = () => {
const languagesToRender: ReactNode[] = [];
Object.keys(LANGUAGES).forEach((lang) => {
languagesToRender.push(
<IAIButton
key={lang}
isChecked={localStorage.getItem('i18nextLng') === lang}
leftIcon={
localStorage.getItem('i18nextLng') === lang ? (
<FaCheck />
) : undefined
}
onClick={() => i18n.changeLanguage(lang)}
aria-label={LANGUAGES[lang as keyof typeof LANGUAGES]}
size="sm"
minWidth="200px"
>
{LANGUAGES[lang as keyof typeof LANGUAGES]}
</IAIButton>
);
});
return languagesToRender;
};
return ( return (
<IAIPopover <Menu closeOnSelect={false}>
triggerComponent={ <Tooltip label={t('common.languagePickerLabel')} hasArrow>
<IAIIconButton <MenuButton
aria-label={t('common.languagePickerLabel')} as={IconButton}
tooltip={t('common.languagePickerLabel')} icon={<IoLanguage />}
icon={<FaLanguage />}
size="sm"
variant="link" variant="link"
data-variant="link" aria-label={t('common.languagePickerLabel')}
fontSize={26} fontSize={22}
minWidth={8}
/> />
} </Tooltip>
> <MenuList>
<VStack>{renderLanguagePicker()}</VStack> <MenuOptionGroup value={language}>
</IAIPopover> {map(LANGUAGES, (languageName, l: keyof typeof LANGUAGES) => (
<MenuItemOption
key={l}
value={l}
onClick={() => dispatch(languageChanged(l))}
>
{languageName}
</MenuItemOption>
))}
</MenuOptionGroup>
</MenuList>
</Menu>
); );
} }

View File

@ -36,7 +36,6 @@ const ProgressBar = () => {
aria-label={t('accessibility.invokeProgressBar')} aria-label={t('accessibility.invokeProgressBar')}
isIndeterminate={isProcessing && !currentStatusHasSteps} isIndeterminate={isProcessing && !currentStatusHasSteps}
height={PROGRESS_BAR_THICKNESS} height={PROGRESS_BAR_THICKNESS}
zIndex={99}
/> />
); );
}; };

View File

@ -1,13 +1,26 @@
import { VStack } from '@chakra-ui/react'; import {
IconButton,
Menu,
MenuButton,
MenuItemOption,
MenuList,
MenuOptionGroup,
Tooltip,
} from '@chakra-ui/react';
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import IAIIconButton from 'common/components/IAIIconButton';
import IAIPopover from 'common/components/IAIPopover';
import { setCurrentTheme } from 'features/ui/store/uiSlice'; import { setCurrentTheme } from 'features/ui/store/uiSlice';
import type { ReactNode } from 'react'; import i18n from 'i18n';
import { map } from 'lodash-es';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaCheck, FaPalette } from 'react-icons/fa'; import { FaPalette } from 'react-icons/fa';
export const THEMES = {
dark: i18n.t('common.darkTheme'),
light: i18n.t('common.lightTheme'),
green: i18n.t('common.greenTheme'),
ocean: i18n.t('common.oceanTheme'),
};
export default function ThemeChanger() { export default function ThemeChanger() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -17,51 +30,31 @@ export default function ThemeChanger() {
(state: RootState) => state.ui.currentTheme (state: RootState) => state.ui.currentTheme
); );
const THEMES = {
dark: t('common.darkTheme'),
light: t('common.lightTheme'),
green: t('common.greenTheme'),
ocean: t('common.oceanTheme'),
};
const handleChangeTheme = (theme: string) => {
dispatch(setCurrentTheme(theme));
};
const renderThemeOptions = () => {
const themesToRender: ReactNode[] = [];
Object.keys(THEMES).forEach((theme) => {
themesToRender.push(
<IAIButton
isChecked={currentTheme === theme}
leftIcon={currentTheme === theme ? <FaCheck /> : undefined}
size="sm"
onClick={() => handleChangeTheme(theme)}
key={theme}
>
{THEMES[theme as keyof typeof THEMES]}
</IAIButton>
);
});
return themesToRender;
};
return ( return (
<IAIPopover <Menu closeOnSelect={false}>
triggerComponent={ <Tooltip label={t('common.themeLabel')} hasArrow>
<IAIIconButton <MenuButton
aria-label={t('common.themeLabel')} as={IconButton}
size="sm"
variant="link"
data-variant="link"
fontSize={20}
icon={<FaPalette />} icon={<FaPalette />}
variant="link"
aria-label={t('common.themeLabel')}
fontSize={20}
minWidth={8}
/> />
} </Tooltip>
> <MenuList>
<VStack align="stretch">{renderThemeOptions()}</VStack> <MenuOptionGroup value={currentTheme}>
</IAIPopover> {map(THEMES, (themeName, themeKey: keyof typeof THEMES) => (
<MenuItemOption
key={themeKey}
value={themeKey}
onClick={() => dispatch(setCurrentTheme(themeKey))}
>
{themeName}
</MenuItemOption>
))}
</MenuOptionGroup>
</MenuList>
</Menu>
); );
} }

View File

@ -1,21 +1,40 @@
import { AppFeature } from 'app/types/invokeai'; import { AppFeature, SDFeature } from 'app/types/invokeai';
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { InvokeTabName } from 'features/ui/store/tabMap';
export const useFeatureStatus = (
feature: AppFeature | SDFeature | InvokeTabName
) => {
const disabledTabs = useAppSelector(
(state: RootState) => state.config.disabledTabs
);
export const useFeatureStatus = (feature: AppFeature) => {
const disabledFeatures = useAppSelector( const disabledFeatures = useAppSelector(
(state: RootState) => state.config.disabledFeatures (state: RootState) => state.config.disabledFeatures
); );
const disabledSDFeatures = useAppSelector(
(state: RootState) => state.config.disabledSDFeatures
);
const isFeatureDisabled = useMemo( const isFeatureDisabled = useMemo(
() => disabledFeatures.includes(feature), () =>
[disabledFeatures, feature] disabledFeatures.includes(feature as AppFeature) ||
disabledSDFeatures.includes(feature as SDFeature) ||
disabledTabs.includes(feature as InvokeTabName),
[disabledFeatures, disabledSDFeatures, disabledTabs, feature]
); );
const isFeatureEnabled = useMemo( const isFeatureEnabled = useMemo(
() => !disabledFeatures.includes(feature), () =>
[disabledFeatures, feature] !(
disabledFeatures.includes(feature as AppFeature) ||
disabledSDFeatures.includes(feature as SDFeature) ||
disabledTabs.includes(feature as InvokeTabName)
),
[disabledFeatures, disabledSDFeatures, disabledTabs, feature]
); );
return { isFeatureDisabled, isFeatureEnabled }; return { isFeatureDisabled, isFeatureEnabled };

View File

@ -8,6 +8,7 @@ export const initialConfigState: AppConfig = {
shouldFetchImages: false, shouldFetchImages: false,
disabledTabs: [], disabledTabs: [],
disabledFeatures: [], disabledFeatures: [],
disabledSDFeatures: [],
canRestoreDeletedImagesFromBin: true, canRestoreDeletedImagesFromBin: true,
sd: { sd: {
iterations: { iterations: {

View File

@ -1,6 +1,7 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { isEqual, reduce, pickBy } from 'lodash-es'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { reduce, pickBy } from 'lodash-es';
export const systemSelector = (state: RootState) => state.system; export const systemSelector = (state: RootState) => state.system;
@ -22,11 +23,7 @@ export const activeModelSelector = createSelector(
); );
return { ...model_list[activeModel], name: activeModel }; return { ...model_list[activeModel], name: activeModel };
}, },
{ defaultSelectorOptions
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
); );
export const diffusersModelsSelector = createSelector( export const diffusersModelsSelector = createSelector(
@ -42,9 +39,11 @@ export const diffusersModelsSelector = createSelector(
return diffusersModels; return diffusersModels;
}, },
{ defaultSelectorOptions
memoizeOptions: { );
resultEqualityCheck: isEqual,
}, export const languageSelector = createSelector(
} systemSelector,
(system) => system.language,
defaultSelectorOptions
); );

View File

@ -24,6 +24,7 @@ import { InvokeLogLevel } from 'app/logging/useLogger';
import { TFuncKey } from 'i18next'; import { TFuncKey } from 'i18next';
import { t } from 'i18next'; import { t } from 'i18next';
import { userInvoked } from 'app/store/actions'; import { userInvoked } from 'app/store/actions';
import { LANGUAGES } from '../components/LanguagePicker';
export type CancelStrategy = 'immediate' | 'scheduled'; export type CancelStrategy = 'immediate' | 'scheduled';
@ -91,6 +92,7 @@ export interface SystemState {
infillMethods: InfillMethod[]; infillMethods: InfillMethod[];
isPersisted: boolean; isPersisted: boolean;
shouldAntialiasProgressImage: boolean; shouldAntialiasProgressImage: boolean;
language: keyof typeof LANGUAGES;
} }
export const initialSystemState: SystemState = { export const initialSystemState: SystemState = {
@ -125,6 +127,7 @@ export const initialSystemState: SystemState = {
canceledSession: '', canceledSession: '',
infillMethods: ['tile', 'patchmatch'], infillMethods: ['tile', 'patchmatch'],
isPersisted: false, isPersisted: false,
language: 'en',
}; };
export const systemSlice = createSlice({ export const systemSlice = createSlice({
@ -272,6 +275,9 @@ export const systemSlice = createSlice({
isPersistedChanged: (state, action: PayloadAction<boolean>) => { isPersistedChanged: (state, action: PayloadAction<boolean>) => {
state.isPersisted = action.payload; state.isPersisted = action.payload;
}, },
languageChanged: (state, action: PayloadAction<keyof typeof LANGUAGES>) => {
state.language = action.payload;
},
}, },
extraReducers(builder) { extraReducers(builder) {
/** /**
@ -418,6 +424,7 @@ export const systemSlice = createSlice({
state.currentStep = 0; state.currentStep = 0;
state.totalSteps = 0; state.totalSteps = 0;
state.statusTranslationKey = 'common.statusConnected'; state.statusTranslationKey = 'common.statusConnected';
state.progressImage = null;
state.toastQueue.push( state.toastQueue.push(
makeToast({ title: t('toast.canceled'), status: 'warning' }) makeToast({ title: t('toast.canceled'), status: 'warning' })
@ -480,6 +487,7 @@ export const {
shouldLogToConsoleChanged, shouldLogToConsoleChanged,
isPersistedChanged, isPersistedChanged,
shouldAntialiasProgressImageChanged, shouldAntialiasProgressImageChanged,
languageChanged,
} = systemSlice.actions; } = systemSlice.actions;
export default systemSlice.reducer; export default systemSlice.reducer;

View File

@ -44,7 +44,6 @@ const FloatingGalleryButton = () => {
pos: 'absolute', pos: 'absolute',
top: '50%', top: '50%',
transform: 'translate(0, -50%)', transform: 'translate(0, -50%)',
zIndex: 31,
p: 0, p: 0,
insetInlineEnd: 0, insetInlineEnd: 0,
px: 3, px: 3,

View File

@ -73,7 +73,6 @@ const FloatingParametersPanelButtons = () => {
<Flex <Flex
pos="absolute" pos="absolute"
transform="translate(0, -50%)" transform="translate(0, -50%)"
zIndex={20}
minW={8} minW={8}
top="50%" top="50%"
insetInlineStart="4.5rem" insetInlineStart="4.5rem"

View File

@ -45,12 +45,12 @@ export interface InvokeTabInfo {
const tabs: InvokeTabInfo[] = [ const tabs: InvokeTabInfo[] = [
{ {
id: 'txt2img', id: 'txt2img',
icon: <Icon as={GoTextSize} sx={{ boxSize: 5 }} />, icon: <Icon as={GoTextSize} sx={{ boxSize: 6 }} />,
content: <TextToImageTab />, content: <TextToImageTab />,
}, },
{ {
id: 'img2img', id: 'img2img',
icon: <Icon as={FaImage} sx={{ boxSize: 5 }} />, icon: <Icon as={FaImage} sx={{ boxSize: 6 }} />,
content: <ImageTab />, content: <ImageTab />,
}, },
{ {

View File

@ -142,7 +142,7 @@ const ResizableDrawer = ({
direction={slideDirection} direction={slideDirection}
in={isOpen} in={isOpen}
motionProps={{ initial: false }} motionProps={{ initial: false }}
style={{ zIndex: 99, width: 'full' }} style={{ width: 'full' }}
> >
<Box <Box
ref={outsideClickRef} ref={outsideClickRef}

View File

@ -64,8 +64,6 @@ const ImageToImageTabCoreParameters = () => {
<ParamSteps /> <ParamSteps />
<ParamCFGScale /> <ParamCFGScale />
</Flex> </Flex>
<ParamWidth isDisabled={!shouldFitToWidthHeight} />
<ParamHeight isDisabled={!shouldFitToWidthHeight} />
<Flex gap={3} w="full"> <Flex gap={3} w="full">
<Box flexGrow={2}> <Box flexGrow={2}>
<ParamSampler /> <ParamSampler />
@ -74,6 +72,8 @@ const ImageToImageTabCoreParameters = () => {
<ModelSelect /> <ModelSelect />
</Box> </Box>
</Flex> </Flex>
<ParamWidth isDisabled={!shouldFitToWidthHeight} />
<ParamHeight isDisabled={!shouldFitToWidthHeight} />
<ImageToImageStrength /> <ImageToImageStrength />
<ImageToImageFit /> <ImageToImageFit />
</Flex> </Flex>

View File

@ -62,8 +62,6 @@ const UnifiedCanvasCoreParameters = () => {
<ParamSteps /> <ParamSteps />
<ParamCFGScale /> <ParamCFGScale />
</Flex> </Flex>
<ParamWidth />
<ParamHeight />
<Flex gap={3} w="full"> <Flex gap={3} w="full">
<Box flexGrow={2}> <Box flexGrow={2}>
<ParamSampler /> <ParamSampler />
@ -72,8 +70,9 @@ const UnifiedCanvasCoreParameters = () => {
<ModelSelect /> <ModelSelect />
</Box> </Box>
</Flex> </Flex>
<ParamWidth />
<ParamHeight />
<ImageToImageStrength /> <ImageToImageStrength />
<ImageToImageFit />
</Flex> </Flex>
)} )}
</Flex> </Flex>

View File

@ -3,7 +3,7 @@ import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend'; import Backend from 'i18next-http-backend';
import { initReactI18next } from 'react-i18next'; import { initReactI18next } from 'react-i18next';
import translationEN from '../dist/locales/en.json'; import translationEN from '../public/locales/en.json';
import { LOCALSTORAGE_PREFIX } from 'app/store/constants'; import { LOCALSTORAGE_PREFIX } from 'app/store/constants';
if (import.meta.env.MODE === 'package') { if (import.meta.env.MODE === 'package') {
@ -21,11 +21,11 @@ if (import.meta.env.MODE === 'package') {
} else { } else {
i18n i18n
.use(Backend) .use(Backend)
.use( // .use(
new LanguageDetector(null, { // new LanguageDetector(null, {
lookupLocalStorage: `${LOCALSTORAGE_PREFIX}lng`, // lookupLocalStorage: `${LOCALSTORAGE_PREFIX}lng`,
}) // })
) // )
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
fallbackLng: 'en', fallbackLng: 'en',

View File

@ -19,7 +19,6 @@ export type { ConditioningField } from './models/ConditioningField';
export type { CreateModelRequest } from './models/CreateModelRequest'; export type { CreateModelRequest } from './models/CreateModelRequest';
export type { CropImageInvocation } from './models/CropImageInvocation'; export type { CropImageInvocation } from './models/CropImageInvocation';
export type { CvInpaintInvocation } from './models/CvInpaintInvocation'; export type { CvInpaintInvocation } from './models/CvInpaintInvocation';
export type { DataURLToImageInvocation } from './models/DataURLToImageInvocation';
export type { DiffusersModelInfo } from './models/DiffusersModelInfo'; export type { DiffusersModelInfo } from './models/DiffusersModelInfo';
export type { DivideInvocation } from './models/DivideInvocation'; export type { DivideInvocation } from './models/DivideInvocation';
export type { Edge } from './models/Edge'; export type { Edge } from './models/Edge';
@ -92,7 +91,6 @@ export { $ConditioningField } from './schemas/$ConditioningField';
export { $CreateModelRequest } from './schemas/$CreateModelRequest'; export { $CreateModelRequest } from './schemas/$CreateModelRequest';
export { $CropImageInvocation } from './schemas/$CropImageInvocation'; export { $CropImageInvocation } from './schemas/$CropImageInvocation';
export { $CvInpaintInvocation } from './schemas/$CvInpaintInvocation'; export { $CvInpaintInvocation } from './schemas/$CvInpaintInvocation';
export { $DataURLToImageInvocation } from './schemas/$DataURLToImageInvocation';
export { $DiffusersModelInfo } from './schemas/$DiffusersModelInfo'; export { $DiffusersModelInfo } from './schemas/$DiffusersModelInfo';
export { $DivideInvocation } from './schemas/$DivideInvocation'; export { $DivideInvocation } from './schemas/$DivideInvocation';
export { $Edge } from './schemas/$Edge'; export { $Edge } from './schemas/$Edge';

View File

@ -1,19 +0,0 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* Outputs an image from a data URL.
*/
export type DataURLToImageInvocation = {
/**
* The id of this node. Must be unique among all nodes.
*/
id: string;
type?: 'dataURL_image';
/**
* The b64 data URL
*/
dataURL: string;
};

View File

@ -8,7 +8,6 @@ import type { CollectInvocation } from './CollectInvocation';
import type { CompelInvocation } from './CompelInvocation'; import type { CompelInvocation } from './CompelInvocation';
import type { CropImageInvocation } from './CropImageInvocation'; import type { CropImageInvocation } from './CropImageInvocation';
import type { CvInpaintInvocation } from './CvInpaintInvocation'; import type { CvInpaintInvocation } from './CvInpaintInvocation';
import type { DataURLToImageInvocation } from './DataURLToImageInvocation';
import type { DivideInvocation } from './DivideInvocation'; import type { DivideInvocation } from './DivideInvocation';
import type { Edge } from './Edge'; import type { Edge } from './Edge';
import type { GraphInvocation } from './GraphInvocation'; import type { GraphInvocation } from './GraphInvocation';
@ -48,7 +47,7 @@ export type Graph = {
/** /**
* The nodes in this graph * The nodes in this graph
*/ */
nodes?: Record<string, (LoadImageInvocation | ShowImageInvocation | DataURLToImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | CompelInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation)>; nodes?: Record<string, (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | CompelInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation)>;
/** /**
* The connections between nodes and their fields in this graph * The connections between nodes and their fields in this graph
*/ */

View File

@ -40,7 +40,7 @@ export type ImageToImageInvocation = {
/** /**
* The scheduler to use * The scheduler to use
*/ */
scheduler?: 'ddim' | 'dpmpp_2' | 'k_dpm_2' | 'k_dpm_2_a' | 'k_dpmpp_2' | 'k_euler' | 'k_euler_a' | 'k_heun' | 'k_lms' | 'plms'; scheduler?: 'ddim' | 'ddpm' | 'deis' | 'lms' | 'pndm' | 'heun' | 'euler' | 'euler_k' | 'euler_a' | 'kdpm_2' | 'kdpm_2_a' | 'dpmpp_2s' | 'dpmpp_2m' | 'dpmpp_2m_k' | 'unipc';
/** /**
* The model to use (currently ignored) * The model to use (currently ignored)
*/ */

View File

@ -6,7 +6,7 @@ import type { ColorField } from './ColorField';
import type { ImageField } from './ImageField'; import type { ImageField } from './ImageField';
/** /**
* Infills transparent areas of an image with a color * Infills transparent areas of an image with a solid color
*/ */
export type InfillColorInvocation = { export type InfillColorInvocation = {
/** /**

View File

@ -5,7 +5,7 @@
import type { ImageField } from './ImageField'; import type { ImageField } from './ImageField';
/** /**
* Infills transparent areas of an image with tiles of the image * Infills transparent areas of an image using the PatchMatch algorithm
*/ */
export type InfillPatchMatchInvocation = { export type InfillPatchMatchInvocation = {
/** /**

View File

@ -41,7 +41,7 @@ export type InpaintInvocation = {
/** /**
* The scheduler to use * The scheduler to use
*/ */
scheduler?: 'ddim' | 'dpmpp_2' | 'k_dpm_2' | 'k_dpm_2_a' | 'k_dpmpp_2' | 'k_euler' | 'k_euler_a' | 'k_heun' | 'k_lms' | 'plms'; scheduler?: 'ddim' | 'ddpm' | 'deis' | 'lms' | 'pndm' | 'heun' | 'euler' | 'euler_k' | 'euler_a' | 'kdpm_2' | 'kdpm_2_a' | 'dpmpp_2s' | 'dpmpp_2m' | 'dpmpp_2m_k' | 'unipc';
/** /**
* The model to use (currently ignored) * The model to use (currently ignored)
*/ */

View File

@ -37,11 +37,19 @@ export type LatentsToLatentsInvocation = {
/** /**
* The scheduler to use * The scheduler to use
*/ */
scheduler?: 'ddim' | 'dpmpp_2' | 'k_dpm_2' | 'k_dpm_2_a' | 'k_dpmpp_2' | 'k_euler' | 'k_euler_a' | 'k_heun' | 'k_lms' | 'plms'; scheduler?: 'ddim' | 'ddpm' | 'deis' | 'lms' | 'pndm' | 'heun' | 'euler' | 'euler_k' | 'euler_a' | 'kdpm_2' | 'kdpm_2_a' | 'dpmpp_2s' | 'dpmpp_2m' | 'dpmpp_2m_k' | 'unipc';
/** /**
* The model to use (currently ignored) * The model to use (currently ignored)
*/ */
model?: string; model?: string;
/**
* Whether or not to generate an image that can tile without seams
*/
seamless?: boolean;
/**
* The axes to tile the image on, 'x' and/or 'y'
*/
seamless_axes?: string;
/** /**
* The latents to use as a base image * The latents to use as a base image
*/ */

View File

@ -38,7 +38,7 @@ export type TextToImageInvocation = {
/** /**
* The scheduler to use * The scheduler to use
*/ */
scheduler?: 'ddim' | 'dpmpp_2' | 'k_dpm_2' | 'k_dpm_2_a' | 'k_dpmpp_2' | 'k_euler' | 'k_euler_a' | 'k_heun' | 'k_lms' | 'plms'; scheduler?: 'ddim' | 'ddpm' | 'deis' | 'lms' | 'pndm' | 'heun' | 'euler' | 'euler_k' | 'euler_a' | 'kdpm_2' | 'kdpm_2_a' | 'dpmpp_2s' | 'dpmpp_2m' | 'dpmpp_2m_k' | 'unipc';
/** /**
* The model to use (currently ignored) * The model to use (currently ignored)
*/ */

View File

@ -37,10 +37,18 @@ export type TextToLatentsInvocation = {
/** /**
* The scheduler to use * The scheduler to use
*/ */
scheduler?: 'ddim' | 'dpmpp_2' | 'k_dpm_2' | 'k_dpm_2_a' | 'k_dpmpp_2' | 'k_euler' | 'k_euler_a' | 'k_heun' | 'k_lms' | 'plms'; scheduler?: 'ddim' | 'ddpm' | 'deis' | 'lms' | 'pndm' | 'heun' | 'euler' | 'euler_k' | 'euler_a' | 'kdpm_2' | 'kdpm_2_a' | 'dpmpp_2s' | 'dpmpp_2m' | 'dpmpp_2m_k' | 'unipc';
/** /**
* The model to use (currently ignored) * The model to use (currently ignored)
*/ */
model?: string; model?: string;
/**
* Whether or not to generate an image that can tile without seams
*/
seamless?: boolean;
/**
* The axes to tile the image on, 'x' and/or 'y'
*/
seamless_axes?: string;
}; };

View File

@ -1,21 +0,0 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $DataURLToImageInvocation = {
description: `Outputs an image from a data URL.`,
properties: {
id: {
type: 'string',
description: `The id of this node. Must be unique among all nodes.`,
isRequired: true,
},
type: {
type: 'Enum',
},
dataURL: {
type: 'string',
description: `The b64 data URL`,
isRequired: true,
},
},
} as const;

View File

@ -15,8 +15,6 @@ export const $Graph = {
type: 'LoadImageInvocation', type: 'LoadImageInvocation',
}, { }, {
type: 'ShowImageInvocation', type: 'ShowImageInvocation',
}, {
type: 'DataURLToImageInvocation',
}, { }, {
type: 'CropImageInvocation', type: 'CropImageInvocation',
}, { }, {

View File

@ -2,7 +2,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export const $InfillColorInvocation = { export const $InfillColorInvocation = {
description: `Infills transparent areas of an image with a color`, description: `Infills transparent areas of an image with a solid color`,
properties: { properties: {
id: { id: {
type: 'string', type: 'string',

View File

@ -2,7 +2,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export const $InfillPatchMatchInvocation = { export const $InfillPatchMatchInvocation = {
description: `Infills transparent areas of an image with tiles of the image`, description: `Infills transparent areas of an image using the PatchMatch algorithm`,
properties: { properties: {
id: { id: {
type: 'string', type: 'string',

View File

@ -48,6 +48,14 @@ export const $LatentsToLatentsInvocation = {
type: 'string', type: 'string',
description: `The model to use (currently ignored)`, description: `The model to use (currently ignored)`,
}, },
seamless: {
type: 'boolean',
description: `Whether or not to generate an image that can tile without seams`,
},
seamless_axes: {
type: 'string',
description: `The axes to tile the image on, 'x' and/or 'y'`,
},
latents: { latents: {
type: 'all-of', type: 'all-of',
description: `The latents to use as a base image`, description: `The latents to use as a base image`,

View File

@ -48,5 +48,13 @@ export const $TextToLatentsInvocation = {
type: 'string', type: 'string',
description: `The model to use (currently ignored)`, description: `The model to use (currently ignored)`,
}, },
seamless: {
type: 'boolean',
description: `Whether or not to generate an image that can tile without seams`,
},
seamless_axes: {
type: 'string',
description: `The axes to tile the image on, 'x' and/or 'y'`,
},
}, },
} as const; } as const;

View File

@ -7,7 +7,6 @@ import type { CollectInvocation } from '../models/CollectInvocation';
import type { CompelInvocation } from '../models/CompelInvocation'; import type { CompelInvocation } from '../models/CompelInvocation';
import type { CropImageInvocation } from '../models/CropImageInvocation'; import type { CropImageInvocation } from '../models/CropImageInvocation';
import type { CvInpaintInvocation } from '../models/CvInpaintInvocation'; import type { CvInpaintInvocation } from '../models/CvInpaintInvocation';
import type { DataURLToImageInvocation } from '../models/DataURLToImageInvocation';
import type { DivideInvocation } from '../models/DivideInvocation'; import type { DivideInvocation } from '../models/DivideInvocation';
import type { Edge } from '../models/Edge'; import type { Edge } from '../models/Edge';
import type { Graph } from '../models/Graph'; import type { Graph } from '../models/Graph';
@ -150,7 +149,7 @@ export class SessionsService {
* The id of the session * The id of the session
*/ */
sessionId: string, sessionId: string,
requestBody: (LoadImageInvocation | ShowImageInvocation | DataURLToImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | CompelInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | CompelInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation),
}): CancelablePromise<string> { }): CancelablePromise<string> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'POST', method: 'POST',
@ -187,7 +186,7 @@ export class SessionsService {
* The path to the node in the graph * The path to the node in the graph
*/ */
nodePath: string, nodePath: string,
requestBody: (LoadImageInvocation | ShowImageInvocation | DataURLToImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | CompelInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | CompelInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation),
}): CancelablePromise<GraphExecutionState> { }): CancelablePromise<GraphExecutionState> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'PUT', method: 'PUT',

View File

@ -23,6 +23,8 @@ import { textareaTheme } from './components/textarea';
export const theme: ThemeOverride = { export const theme: ThemeOverride = {
config: { config: {
cssVarPrefix: 'invokeai', cssVarPrefix: 'invokeai',
initialColorMode: 'dark',
useSystemColorMode: false,
}, },
styles: { styles: {
global: (_props: StyleFunctionProps) => ({ global: (_props: StyleFunctionProps) => ({
@ -39,7 +41,7 @@ export const theme: ThemeOverride = {
}, },
direction: 'ltr', direction: 'ltr',
fonts: { fonts: {
body: `'Inter', sans-serif`, body: `'InterVariable', sans-serif`,
}, },
breakpoints: { breakpoints: {
base: '0em', // 0px and onwards base: '0em', // 0px and onwards

View File

@ -20,13 +20,7 @@
"*": ["./src/*"] "*": ["./src/*"]
} }
}, },
"include": [ "include": ["src/**/*.ts", "src/**/*.tsx", "*.d.ts"],
"src/**/*.ts",
"src/**/*.tsx",
"*.d.ts",
"src/app/store/middleware/listenerMiddleware",
"src/features/nodes/util/edgeBuilders"
],
"exclude": ["src/services/fixtures/*", "node_modules", "dist"], "exclude": ["src/services/fixtures/*", "node_modules", "dist"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
} }

View File

@ -6673,6 +6673,11 @@ validator@^13.7.0:
resolved "https://registry.yarnpkg.com/validator/-/validator-13.9.0.tgz#33e7b85b604f3bbce9bb1a05d5c3e22e1c2ff855" resolved "https://registry.yarnpkg.com/validator/-/validator-13.9.0.tgz#33e7b85b604f3bbce9bb1a05d5c3e22e1c2ff855"
integrity sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA== integrity sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==
vite-plugin-css-injected-by-js@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.1.1.tgz#8324412636cf6fdada1a86f595aa2e78458e5ddb"
integrity sha512-mwrFvEEy0TuH8Ul0cb2HgjmNboQ/JnEFy+kHCWqAJph3ikMOiIuyYVdx0JO4nEIWJyzSnc4TTdmoTulsikvJEg==
vite-plugin-dts@^2.3.0: vite-plugin-dts@^2.3.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/vite-plugin-dts/-/vite-plugin-dts-2.3.0.tgz#6ab2edf56f48261bfede03958704bfaee2fca3e4" resolved "https://registry.yarnpkg.com/vite-plugin-dts/-/vite-plugin-dts-2.3.0.tgz#6ab2edf56f48261bfede03958704bfaee2fca3e4"

View File

@ -23,7 +23,7 @@
], ],
"threshold": 0, "threshold": 0,
"postprocessing": null, "postprocessing": null,
"sampler": "k_lms", "sampler": "lms",
"variations": [], "variations": [],
"type": "txt2img" "type": "txt2img"
} }

View File

@ -17,7 +17,7 @@ valid_metadata = {
"width": 512, "width": 512,
"height": 512, "height": 512,
"cfg_scale": 7.5, "cfg_scale": 7.5,
"scheduler": "k_lms", "scheduler": "lms",
"model": "stable-diffusion-1.5", "model": "stable-diffusion-1.5",
}, },
} }