From 281662a6e1854b7af11c86cbb227f04780a8c4bd Mon Sep 17 00:00:00 2001 From: Sammy Date: Sat, 15 Apr 2023 21:46:47 +0200 Subject: [PATCH 01/13] chore: add ".version" and ".last_model" to gitignore Mistakenly closed the previous pr --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 86f102dd7f..e9918d4fb5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ models/ldm/stable-diffusion-v1/model.ckpt configs/models.user.yaml config/models.user.yml invokeai.init +.version +.last_model # ignore the Anaconda/Miniconda installer used while building Docker image anaconda.sh From 6f6de402adc5b1dcc677d9aef23d2f0ada87a0a3 Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Thu, 13 Apr 2023 00:23:15 -0400 Subject: [PATCH 02/13] make InvocationQueueItem serializable --- invokeai/app/services/invocation_queue.py | 24 +++++------------------ 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/invokeai/app/services/invocation_queue.py b/invokeai/app/services/invocation_queue.py index 4a42789b12..1a624391c9 100644 --- a/invokeai/app/services/invocation_queue.py +++ b/invokeai/app/services/invocation_queue.py @@ -1,31 +1,17 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) -from abc import ABC, abstractmethod -from queue import Queue import time +from abc import ABC, abstractmethod +from dataclasses import dataclass +from queue import Queue -# TODO: make this serializable +@dataclass class InvocationQueueItem: - # session_id: str graph_execution_state_id: str invocation_id: str invoke_all: bool - timestamp: float - - def __init__( - self, - # session_id: str, - graph_execution_state_id: str, - invocation_id: str, - invoke_all: bool = False, - ): - # self.session_id = session_id - self.graph_execution_state_id = graph_execution_state_id - self.invocation_id = invocation_id - self.invoke_all = invoke_all - self.timestamp = time.time() - + timestamp: float = time.time() class InvocationQueueABC(ABC): """Abstract base class for all invocation queues""" From 7fc5fbd4ce5e99094eb35f76eaa60f3c98333269 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 13 Apr 2023 03:44:44 -0400 Subject: [PATCH 03/13] nodes: convert InvocationQueueItem to Pydantic class --- invokeai/app/services/invocation_queue.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/invokeai/app/services/invocation_queue.py b/invokeai/app/services/invocation_queue.py index 1a624391c9..008a2e0823 100644 --- a/invokeai/app/services/invocation_queue.py +++ b/invokeai/app/services/invocation_queue.py @@ -2,16 +2,16 @@ import time from abc import ABC, abstractmethod -from dataclasses import dataclass +from pydantic import BaseModel, Field from queue import Queue -@dataclass -class InvocationQueueItem: +class InvocationQueueItem(BaseModel): graph_execution_state_id: str invocation_id: str invoke_all: bool - timestamp: float = time.time() + timestamp: float = Field(default_factory=time.time) + class InvocationQueueABC(ABC): """Abstract base class for all invocation queues""" From cbd1a7263a1c8ca880a7041121211662b8d68d66 Mon Sep 17 00:00:00 2001 From: Eugene Date: Fri, 14 Apr 2023 00:56:17 -0400 Subject: [PATCH 04/13] nodes: fix typing of GraphExecutionState.id --- invokeai/app/services/graph.py | 5 ++--- invokeai/app/services/invocation_queue.py | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/invokeai/app/services/graph.py b/invokeai/app/services/graph.py index 44f6a3d69e..d18329f49d 100644 --- a/invokeai/app/services/graph.py +++ b/invokeai/app/services/graph.py @@ -2,7 +2,6 @@ import copy import itertools -import traceback import uuid from types import NoneType from typing import ( @@ -751,7 +750,7 @@ class GraphExecutionState(BaseModel): """Tracks the state of a graph execution""" id: str = Field( - description="The id of the execution state", default_factory=uuid.uuid4 + description="The id of the execution state", default_factory=lambda: uuid.uuid4().__str__() ) # TODO: Store a reference to the graph instead of the actual graph? @@ -1171,7 +1170,7 @@ class LibraryGraph(BaseModel): if len(v) != len(set(i.alias for i in v)): raise ValueError("Duplicate exposed alias") return v - + @root_validator def validate_exposed_nodes(cls, values): graph = values['graph'] diff --git a/invokeai/app/services/invocation_queue.py b/invokeai/app/services/invocation_queue.py index 008a2e0823..9ffaa5f898 100644 --- a/invokeai/app/services/invocation_queue.py +++ b/invokeai/app/services/invocation_queue.py @@ -2,12 +2,14 @@ import time from abc import ABC, abstractmethod -from pydantic import BaseModel, Field from queue import Queue +from uuid import UUID + +from pydantic import BaseModel, Field class InvocationQueueItem(BaseModel): - graph_execution_state_id: str + graph_execution_state_id: UUID invocation_id: str invoke_all: bool timestamp: float = Field(default_factory=time.time) From 570c3fe690ee36f8eecd982f6ddf9c7c50f7f2b3 Mon Sep 17 00:00:00 2001 From: Eugene Date: Fri, 14 Apr 2023 11:17:40 -0400 Subject: [PATCH 05/13] nodes: ensure Graph and GraphExecutionState ids are cast to str on instantiation --- invokeai/app/services/graph.py | 7 ++----- invokeai/app/services/invocation_queue.py | 3 +-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/invokeai/app/services/graph.py b/invokeai/app/services/graph.py index d18329f49d..7ed65015d0 100644 --- a/invokeai/app/services/graph.py +++ b/invokeai/app/services/graph.py @@ -25,7 +25,6 @@ from ..invocations.baseinvocation import ( BaseInvocationOutput, InvocationContext, ) -from .invocation_services import InvocationServices class EdgeConnection(BaseModel): @@ -214,7 +213,7 @@ InvocationOutputsUnion = Union[BaseInvocationOutput.get_all_subclasses_tuple()] class Graph(BaseModel): - id: str = Field(description="The id of this graph", default_factory=uuid.uuid4) + id: str = Field(description="The id of this graph", default_factory=lambda: uuid.uuid4().__str__()) # TODO: use a list (and never use dict in a BaseModel) because pydantic/fastapi hates me nodes: dict[str, Annotated[InvocationsUnion, Field(discriminator="type")]] = Field( description="The nodes in this graph", default_factory=dict @@ -749,9 +748,7 @@ class Graph(BaseModel): class GraphExecutionState(BaseModel): """Tracks the state of a graph execution""" - id: str = Field( - description="The id of the execution state", default_factory=lambda: uuid.uuid4().__str__() - ) + id: str = Field(description="The id of the execution state", default_factory=lambda: uuid.uuid4().__str__()) # TODO: Store a reference to the graph instead of the actual graph? graph: Graph = Field(description="The graph being executed") diff --git a/invokeai/app/services/invocation_queue.py b/invokeai/app/services/invocation_queue.py index 9ffaa5f898..0570d24515 100644 --- a/invokeai/app/services/invocation_queue.py +++ b/invokeai/app/services/invocation_queue.py @@ -3,13 +3,12 @@ import time from abc import ABC, abstractmethod from queue import Queue -from uuid import UUID from pydantic import BaseModel, Field class InvocationQueueItem(BaseModel): - graph_execution_state_id: UUID + graph_execution_state_id: str invocation_id: str invoke_all: bool timestamp: float = Field(default_factory=time.time) From 3daaddf15bbd836a8c56b8f4120c014bfc1faa08 Mon Sep 17 00:00:00 2001 From: Eugene Date: Fri, 14 Apr 2023 12:29:52 -0400 Subject: [PATCH 06/13] nodes: remove duplicate LatentsToLatentsInvocation --- invokeai/app/invocations/latent.py | 56 ++---------------------------- 1 file changed, 3 insertions(+), 53 deletions(-) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index ef17962f89..7593b34142 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -171,7 +171,7 @@ class TextToLatentsInvocation(BaseInvocation): # TODO: pass this an emitter method or something? or a session for dispatching? def dispatch_progress( self, context: InvocationContext, intermediate_state: PipelineIntermediateState - ) -> None: + ) -> None: if (context.services.queue.is_canceled(context.graph_execution_state_id)): raise CanceledException @@ -185,7 +185,7 @@ class TextToLatentsInvocation(BaseInvocation): diffusers_step_callback_adapter(sample, step, steps=self.steps, id=self.id, context=context) - + def get_model(self, model_manager: ModelManager) -> StableDiffusionGeneratorPipeline: model_info = choose_model(model_manager, self.model) model_name = model_info['model_name'] @@ -195,7 +195,7 @@ class TextToLatentsInvocation(BaseInvocation): model=model, scheduler_name=self.scheduler ) - + if isinstance(model, DiffusionPipeline): for component in [model.unet, model.vae]: configure_model_padding(component, @@ -292,57 +292,7 @@ class LatentsToLatentsInvocation(TextToLatentsInvocation): initial_latents = latent if self.strength < 1.0 else torch.zeros_like( latent, device=model.device, dtype=latent.dtype ) - - timesteps, _ = model.get_img2img_timesteps( - self.steps, - self.strength, - device=model.device, - ) - result_latents, result_attention_map_saver = model.latents_from_embeddings( - latents=initial_latents, - timesteps=timesteps, - noise=noise, - num_inference_steps=self.steps, - conditioning_data=conditioning_data, - callback=step_callback - ) - - # https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699 - torch.cuda.empty_cache() - - name = f'{context.graph_execution_state_id}__{self.id}' - context.services.latents.set(name, result_latents) - return LatentsOutput( - latents=LatentsField(latents_name=name) - ) - - -class LatentsToLatentsInvocation(TextToLatentsInvocation): - """Generates latents using latents as base image.""" - - type: Literal["l2l"] = "l2l" - - # Inputs - latents: Optional[LatentsField] = Field(description="The latents to use as a base image") - strength: float = Field(default=0.5, description="The strength of the latents to use") - - def invoke(self, context: InvocationContext) -> LatentsOutput: - noise = context.services.latents.get(self.noise.latents_name) - latent = context.services.latents.get(self.latents.latents_name) - - def step_callback(state: PipelineIntermediateState): - self.dispatch_progress(context, state) - - model = self.get_model(context.services.model_manager) - conditioning_data = self.get_conditioning_data(model) - - # TODO: Verify the noise is the right size - - initial_latents = latent if self.strength < 1.0 else torch.zeros_like( - latent, device=model.device, dtype=latent.dtype - ) - timesteps, _ = model.get_img2img_timesteps( self.steps, self.strength, From ef0773b8a34a70221a9f9b2ec6345575a56613c9 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sat, 15 Apr 2023 23:19:30 -0400 Subject: [PATCH 07/13] nodes: set default for InvocationQueueItem.invoke_all --- invokeai/app/services/invocation_queue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/services/invocation_queue.py b/invokeai/app/services/invocation_queue.py index 0570d24515..d049377e7e 100644 --- a/invokeai/app/services/invocation_queue.py +++ b/invokeai/app/services/invocation_queue.py @@ -10,7 +10,7 @@ from pydantic import BaseModel, Field class InvocationQueueItem(BaseModel): graph_execution_state_id: str invocation_id: str - invoke_all: bool + invoke_all: bool = Field(default=False) timestamp: float = Field(default_factory=time.time) From 63d10027a4996f810057be03ae4b50f6444e7a26 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sat, 15 Apr 2023 23:29:18 -0400 Subject: [PATCH 08/13] nodes: invocation queue item - make more pydantic --- invokeai/app/services/invocation_queue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/app/services/invocation_queue.py b/invokeai/app/services/invocation_queue.py index d049377e7e..acfda6b90b 100644 --- a/invokeai/app/services/invocation_queue.py +++ b/invokeai/app/services/invocation_queue.py @@ -8,8 +8,8 @@ from pydantic import BaseModel, Field class InvocationQueueItem(BaseModel): - graph_execution_state_id: str - invocation_id: str + graph_execution_state_id: str = Field(description="The ID of the graph execution state") + invocation_id: str = Field(description="The ID of the node being invoked") invoke_all: bool = Field(default=False) timestamp: float = Field(default_factory=time.time) From f6cdff2c5b2293ec4684b0e36a1e64c25267b305 Mon Sep 17 00:00:00 2001 From: Tim Cabbage Date: Mon, 17 Apr 2023 16:53:31 +0200 Subject: [PATCH 09/13] [bug] #3218 HuggingFace API off when --no-internet set https://github.com/invoke-ai/InvokeAI/issues/3218 Huggingface API will not be queried if --no-internet flag is set --- invokeai/backend/stable_diffusion/concepts_lib.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/invokeai/backend/stable_diffusion/concepts_lib.py b/invokeai/backend/stable_diffusion/concepts_lib.py index 28167a0bd5..129dd430f4 100644 --- a/invokeai/backend/stable_diffusion/concepts_lib.py +++ b/invokeai/backend/stable_diffusion/concepts_lib.py @@ -57,7 +57,7 @@ class HuggingFaceConceptsLibrary(object): self.concept_list.extend(list(local_concepts_to_add)) return self.concept_list return self.concept_list - else: + elif Globals.internet_available is True: try: models = self.hf_api.list_models( filter=ModelFilter(model_name="sd-concepts-library/") @@ -73,6 +73,8 @@ class HuggingFaceConceptsLibrary(object): " ** You may load .bin and .pt file(s) manually using the --embedding_directory argument." ) return self.concept_list + else: + return self.concept_list def get_concept_model_path(self, concept_name: str) -> str: """ From 2c9a05eb590d3bc4ce7f9b64678b9d2f13b46013 Mon Sep 17 00:00:00 2001 From: Leo Pasanen Date: Tue, 18 Apr 2023 18:46:55 +0300 Subject: [PATCH 10/13] Added CPU instruction for README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 56946df433..dda18e9669 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,11 @@ not supported. pip install InvokeAI --use-pep517 --extra-index-url https://download.pytorch.org/whl/rocm5.4.2 ``` + _For non-GPU systems:_ + ```terminal + pip install InvokeAI --use-pep517 --extra-index-url https://download.pytorch.org/whl/cpu + ``` + _For Macintoshes, either Intel or M1/M2:_ ```sh From 3a968e5072b86cc38d88091a084fb7aa52af659c Mon Sep 17 00:00:00 2001 From: "Alexandre D. Roberge" <4276275+AldeRoberge@users.noreply.github.com> Date: Tue, 18 Apr 2023 21:16:00 -0400 Subject: [PATCH 11/13] Update NSFW.md Outdated doc said to change the '.invokeai' file, but it's now named 'invokeai.init' afaik. --- docs/features/NSFW.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/NSFW.md b/docs/features/NSFW.md index 9a39fd09c3..06d382f47d 100644 --- a/docs/features/NSFW.md +++ b/docs/features/NSFW.md @@ -32,7 +32,7 @@ turned on and off on the command line using `--nsfw_checker` and At installation time, InvokeAI will ask whether the checker should be activated by default (neither argument given on the command line). The response is stored in the InvokeAI initialization file (usually -`.invokeai` in your home directory). You can change the default at any +`invokeai.init` in your home directory). You can change the default at any time by opening this file in a text editor and commenting or uncommenting the line `--nsfw_checker`. From b8432552365d716fa7a2d7f0a895cf729a334211 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Wed, 19 Apr 2023 17:37:48 -0400 Subject: [PATCH 12/13] update CODEOWNERS for changed team composition --- .github/CODEOWNERS | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 17facf4155..046e8f0c57 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,16 +1,16 @@ # continuous integration -/.github/workflows/ @mauwii @lstein @blessedcoolant +/.github/workflows/ @lstein @blessedcoolant # documentation -/docs/ @lstein @mauwii @tildebyte @blessedcoolant -/mkdocs.yml @lstein @mauwii @blessedcoolant +/docs/ @lstein @tildebyte @blessedcoolant +/mkdocs.yml @lstein @blessedcoolant # nodes /invokeai/app/ @Kyle0654 @blessedcoolant # installation and configuration -/pyproject.toml @mauwii @lstein @blessedcoolant -/docker/ @mauwii @lstein @blessedcoolant +/pyproject.toml @lstein @blessedcoolant +/docker/ @lstein @blessedcoolant /scripts/ @ebr @lstein /installer/ @lstein @ebr /invokeai/assets @lstein @ebr @@ -22,11 +22,11 @@ /invokeai/backend @blessedcoolant @psychedelicious @lstein # generation, model management, postprocessing -/invokeai/backend @keturn @damian0815 @lstein @blessedcoolant @jpphoto +/invokeai/backend @damian0815 @lstein @blessedcoolant @jpphoto @gregghelt2 # front ends /invokeai/frontend/CLI @lstein -/invokeai/frontend/install @lstein @ebr @mauwii +/invokeai/frontend/install @lstein @ebr /invokeai/frontend/merge @lstein @blessedcoolant @hipsterusername /invokeai/frontend/training @lstein @blessedcoolant @hipsterusername /invokeai/frontend/web @psychedelicious @blessedcoolant From 5f498e10bd69247dbccc2a938d369f8fc5633e0f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 22 Apr 2023 13:10:20 +1000 Subject: [PATCH 13/13] Partial migration of UI to nodes API (#3195) * feat(ui): add axios client generator and simple example * fix(ui): update client & nodes test code w/ new Edge type * chore(ui): organize generated files * chore(ui): update .eslintignore, .prettierignore * chore(ui): update openapi.json * feat(backend): fixes for nodes/generator * feat(ui): generate object args for api client * feat(ui): more nodes api prototyping * feat(ui): nodes cancel * chore(ui): regenerate api client * fix(ui): disable OG web server socket connection * fix(ui): fix scrollbar styles typing and prop just noticed the typo, and made the types stronger. * feat(ui): add socketio types * feat(ui): wip nodes - extract api client method arg types instead of manually declaring them - update example to display images - general tidy up * start building out node translations from frontend state and add notes about missing features * use reference to sampler_name * use reference to sampler_name * add optional apiUrl prop * feat(ui): start hooking up dynamic txt2img node generation, create middleware for session invocation * feat(ui): write separate nodes socket layer, txt2img generating and rendering w single node * feat(ui): img2img implementation * feat(ui): get intermediate images working but types are stubbed out * chore(ui): add support for package mode * feat(ui): add nodes mode script * feat(ui): handle random seeds * fix(ui): fix middleware types * feat(ui): add rtk action type guard * feat(ui): disable NodeAPITest This was polluting the network/socket logs. * feat(ui): fix parameters panel border color This commit should be elsewhere but I don't want to break my flow * feat(ui): make thunk types more consistent * feat(ui): add type guards for outputs * feat(ui): load images on socket connect Rudimentary * chore(ui): bump redux-toolkit * docs(ui): update readme * chore(ui): regenerate api client * chore(ui): add typescript as dev dependency I am having trouble with TS versions after vscode updated and now uses TS 5. `madge` has installed 3.9.10 and for whatever reason my vscode wants to use that. Manually specifying 4.9.5 and then setting vscode to use that as the workspace TS fixes the issue. * feat(ui): begin migrating gallery to nodes Along the way, migrate to use RTK `createEntityAdapter` for gallery images, and separate `results` and `uploads` into separate slices. Much cleaner this way. * feat(ui): clean up & comment results slice * fix(ui): separate thunk for initial gallery load so it properly gets index 0 * feat(ui): POST upload working * fix(ui): restore removed type * feat(ui): patch api generation for headers access * chore(ui): regenerate api * feat(ui): wip gallery migration * feat(ui): wip gallery migration * chore(ui): regenerate api * feat(ui): wip refactor socket events * feat(ui): disable panels based on app props * feat(ui): invert logic to be disabled * disable panels when app mounts * feat(ui): add support to disableTabs * docs(ui): organise and update docs * lang(ui): add toast strings * feat(ui): wip events, comments, and general refactoring * feat(ui): add optional token for auth * feat(ui): export StatusIndicator and ModelSelect for header use * feat(ui) working on making socket URL dynamic * feat(ui): dynamic middleware loading * feat(ui): prep for socket jwt * feat(ui): migrate cancelation also updated action names to be event-like instead of declaration-like sorry, i was scattered and this commit has a lot of unrelated stuff in it. * fix(ui): fix img2img type * chore(ui): regenerate api client * feat(ui): improve InvocationCompleteEvent types * feat(ui): increase StatusIndicator font size * fix(ui): fix middleware order for multi-node graphs * feat(ui): add exampleGraphs object w/ iterations example * feat(ui): generate iterations graph * feat(ui): update ModelSelect for nodes API * feat(ui): add hi-res functionality for txt2img generations * feat(ui): "subscribe" to particular nodes feels like a dirty hack but oh well it works * feat(ui): first steps to node editor ui * fix(ui): disable event subscription it is not fully baked just yet * feat(ui): wip node editor * feat(ui): remove extraneous field types * feat(ui): nodes before deleting stuff * feat(ui): cleanup nodes ui stuff * feat(ui): hook up nodes to redux * fix(ui): fix handle * fix(ui): add basic node edges & connection validation * feat(ui): add connection validation styling * feat(ui): increase edge width * feat(ui): it blends * feat(ui): wip model handling and graph topology validation * feat(ui): validation connections w/ graphlib * docs(ui): update nodes doc * feat(ui): wip node editor * chore(ui): rebuild api, update types * add redux-dynamic-middlewares as a dependency * feat(ui): add url host transformation * feat(ui): handle already-connected fields * feat(ui): rewrite SqliteItemStore in sqlalchemy * fix(ui): fix sqlalchemy dynamic model instantiation * feat(ui, nodes): metadata wip * feat(ui, nodes): models * feat(ui, nodes): more metadata wip * feat(ui): wip range/iterate * fix(nodes): fix sqlite typing * feat(ui): export new type for invoke component * tests(nodes): fix test instantiation of ImageField * feat(nodes): fix LoadImageInvocation * feat(nodes): add `title` ui hint * feat(nodes): make ImageField attrs optional * feat(ui): wip nodes etc * feat(nodes): roll back sqlalchemy * fix(nodes): partially address feedback * fix(backend): roll back changes to pngwriter * feat(nodes): wip address metadata feedback * feat(nodes): add seeded rng to RandomRange * feat(nodes): address feedback * feat(nodes): move GET images error handling to DiskImageStorage * feat(nodes): move GET images error handling to DiskImageStorage * fix(nodes): fix image output schema customization * feat(ui): img2img/txt2img -> linear - remove txt2img and img2img tabs - add linear tab - add initial image selection to linear parameters accordion * feat(ui): tidy graph builders * feat(ui): tidy misc * feat(ui): improve invocation union types * feat(ui): wip metadata viewer recall * feat(ui): move fonts to normal deps * feat(nodes): fix broken upload * feat(nodes): add metadata module + tests, thumbnails - `MetadataModule` is stateless and needed in places where the `InvocationContext` is not available, so have not made it a `service` - Handles loading/parsing/building metadata, and creating png info objects - added tests for MetadataModule - Lifted thumbnail stuff to util * fix(nodes): revert change to RandomRangeInvocation * feat(nodes): address feedback - make metadata a service - rip out pydantic validation, implement metadata parsing as simple functions - update tests - address other minor feedback items * fix(nodes): fix other tests * fix(nodes): add metadata service to cli * fix(nodes): fix latents/image field parsing * feat(nodes): customise LatentsField schema * feat(nodes): move metadata parsing to frontend * fix(nodes): fix metadata test --------- Co-authored-by: maryhipp Co-authored-by: Mary Hipp --- invokeai/app/api/dependencies.py | 7 +- invokeai/app/api/models/images.py | 24 +- invokeai/app/api/routers/images.py | 104 +++- invokeai/app/cli_app.py | 7 +- invokeai/app/invocations/baseinvocation.py | 2 +- invokeai/app/invocations/collections.py | 32 +- invokeai/app/invocations/cv.py | 15 +- invokeai/app/invocations/generate.py | 176 +++--- invokeai/app/invocations/image.py | 175 ++++-- invokeai/app/invocations/latent.py | 56 +- invokeai/app/invocations/reconstruct.py | 18 +- invokeai/app/invocations/upscale.py | 17 +- .../util/{get_model.py => choose_model.py} | 11 +- invokeai/app/models/image.py | 15 +- invokeai/app/models/metadata.py | 11 - invokeai/app/services/events.py | 45 +- invokeai/app/services/image_storage.py | 106 +++- invokeai/app/services/invocation_services.py | 4 + invokeai/app/services/metadata.py | 96 ++++ invokeai/app/services/processor.py | 12 +- invokeai/app/services/sqlite.py | 31 +- invokeai/app/util/misc.py | 5 + invokeai/app/util/save_thumbnail.py | 25 - invokeai/app/util/step_callback.py | 62 +- invokeai/app/util/thumbnails.py | 15 + invokeai/frontend/web/.eslintignore | 2 + invokeai/frontend/web/.prettierignore | 4 + invokeai/frontend/web/docs/API_CLIENT.md | 87 +++ invokeai/frontend/web/docs/EVENTS.md | 21 + invokeai/frontend/web/docs/NODE_EDITOR.md | 17 + invokeai/frontend/web/docs/PACKAGE_SCRIPTS.md | 29 + invokeai/frontend/web/{ => docs}/README.md | 10 +- invokeai/frontend/web/index.d.ts | 21 +- invokeai/frontend/web/package.json | 16 +- invokeai/frontend/web/public/locales/en.json | 5 + invokeai/frontend/web/src/app/App.tsx | 30 +- invokeai/frontend/web/src/app/invokeai.d.ts | 22 +- .../src/app/selectors/readinessSelector.ts | 3 +- .../frontend/web/src/app/socketio/actions.ts | 10 +- .../frontend/web/src/app/socketio/emitters.ts | 6 +- .../web/src/app/socketio/listeners.ts | 27 +- .../web/src/app/socketio/middleware.ts | 2 + invokeai/frontend/web/src/app/store.ts | 88 +-- invokeai/frontend/web/src/app/storeUtils.ts | 8 + .../web/src/common/components/IAISlider.tsx | 17 +- .../common/components/ImageToImageOverlay.tsx | 79 +++ .../src/common/components/ImageUploader.tsx | 6 +- .../components/SelectImagePlaceholder.tsx | 12 + .../components/WorkInProgress/NodesWIP.tsx | 185 +++++- .../WorkInProgress/WorkInProgress.tsx | 2 + .../web/src/common/util/_parseMetadataZod.ts | 119 ++++ .../web/src/common/util/getTimestamp.ts | 6 + .../frontend/web/src/common/util/getUrl.ts | 28 + .../web/src/common/util/parseMetadata.ts | 169 ++++++ invokeai/frontend/web/src/component.tsx | 53 +- invokeai/frontend/web/src/exports.tsx | 4 + .../components/IAICanvasIntermediateImage.tsx | 7 +- .../components/IAICanvasObjectRenderer.tsx | 9 +- .../components/IAICanvasStagingArea.tsx | 8 +- .../canvas/store/canvasPersistBlacklist.ts | 14 + .../src/features/canvas/store/canvasSlice.ts | 4 +- .../src/features/canvas/store/canvasTypes.ts | 4 +- .../store/thunks/mergeAndUploadCanvas.ts | 2 +- .../components/CurrentImageButtons.tsx | 133 +++-- .../components/CurrentImageDisplay.tsx | 11 +- .../components/CurrentImagePreview.tsx | 89 ++- .../gallery/components/DeleteImageModal.tsx | 2 +- .../gallery/components/HoverableImage.tsx | 78 +-- .../components/ImageGalleryContent.tsx | 62 +- .../gallery/components/ImageGalleryPanel.tsx | 9 +- .../ImageMetadataViewer.tsx | 299 +++------- .../OLD_ImageMetadataViewer.tsx | 470 +++++++++++++++ .../gallery/hooks/useGetImageByName.ts | 35 ++ .../gallery/store/galleryPersistBlacklist.ts | 17 + .../gallery/store/gallerySelectors.ts | 25 + .../features/gallery/store/gallerySlice.ts | 63 ++- .../gallery/store/resultsPersistBlacklist.ts | 12 + .../features/gallery/store/resultsSlice.ts | 139 +++++ .../gallery/store/thunks/uploadImage.ts | 54 -- .../gallery/store/uploadsPersistBlacklist.ts | 12 + .../features/gallery/store/uploadsSlice.ts | 87 +++ .../lightbox/components/ReactPanZoomImage.tsx | 6 +- .../store/lightboxPersistBlacklist.ts | 10 + .../features/nodes/components/AddNodeMenu.tsx | 63 +++ .../features/nodes/components/FieldHandle.tsx | 69 +++ .../nodes/components/FieldTypeLegend.tsx | 18 + .../src/features/nodes/components/Flow.tsx | 104 ++++ .../nodes/components/InputFieldComponent.tsx | 107 ++++ .../nodes/components/InvocationComponent.tsx | 243 ++++++++ .../features/nodes/components/NodeEditor.tsx | 46 ++ .../components/fields/ArrayInputField.tsx.tsx | 14 + .../fields/BooleanInputFieldComponent.tsx | 31 + .../fields/EnumInputFieldComponent.tsx | 35 ++ .../fields/ImageInputFieldComponent.tsx | 64 +++ .../fields/LatentsInputFieldComponent.tsx | 13 + .../fields/ModelInputFieldComponent.tsx | 57 ++ .../fields/NumberInputFieldComponent.tsx | 41 ++ .../fields/StringInputFieldComponent.tsx | 29 + .../features/nodes/components/fields/types.ts | 10 + .../features/nodes/examples/iterationGraph.ts | 52 ++ .../nodes/hooks/useBuildInvocation.ts | 78 +++ .../nodes/hooks/useInvocationTemplate.ts | 16 + .../nodes/hooks/useIsValidConnection.ts | 93 +++ .../nodes/store/nodesPersistBlacklist.ts | 10 + .../src/features/nodes/store/nodesSlice.ts | 103 ++++ .../selectors/invocationTemplatesSelector.ts | 7 + .../web/src/features/nodes/types/constants.ts | 69 +++ .../src/features/nodes/types/typeGuards.ts | 9 + .../web/src/features/nodes/types/types.ts | 296 ++++++++++ .../nodes/util/fieldTemplateBuilders.ts | 336 +++++++++++ .../features/nodes/util/fieldValueBuilders.ts | 57 ++ .../util/linearGraphBuilder/buildEdges.ts | 39 ++ .../buildImageToImageNode.ts | 99 ++++ .../linearGraphBuilder/buildIterateNode.ts | 13 + .../linearGraphBuilder/buildLinearGraph.ts | 39 ++ .../util/linearGraphBuilder/buildRangeNode.ts | 26 + .../buildTextToImageNode.ts | 42 ++ .../util/nodesGraphBuilder/buildNodesGraph.ts | 77 +++ .../src/features/nodes/util/parseSchema.ts | 120 ++++ .../AccordionItems/InvokeAccordionItem.tsx | 21 +- .../Canvas/InfillAndScalingSettings.tsx | 12 +- .../FaceRestore/CodeformerFidelity.tsx | 4 +- .../FaceRestore/FaceRestoreStrength.tsx | 4 +- .../ImageToImage/ImageFit.tsx | 5 + .../ImageToImage/ImageToImageSettings.tsx | 4 +- .../ImageToImage/ImageToImageStrength.tsx | 4 + .../ImageToImage/ImageToImageToggle.tsx | 24 + .../ImageToImage/InitialImagePreview.tsx | 155 +++++ .../Output/HiresSettings.tsx | 4 +- .../Upscale/UpscaleDenoisingStrength.tsx | 4 +- .../Upscale/UpscaleStrength.tsx | 4 +- .../Variations/VariationAmount.tsx | 4 +- .../components/MainParameters/MainHeight.tsx | 4 +- .../components/MainParameters/MainWidth.tsx | 4 +- .../components/ParametersAccordion.tsx | 111 ++-- .../ProcessButtons/CancelButton.tsx | 139 +++-- .../ProcessButtons/InvokeButton.tsx | 4 +- .../store/generationPersistBlacklist.ts | 10 + .../parameters/store/generationSelectors.ts | 18 + .../parameters/store/generationSlice.ts | 27 +- .../store/postprocessingPersistBlacklist.ts | 10 + .../system/components/ModelSelect.tsx | 41 +- .../system/components/StatusIndicator.tsx | 2 +- .../features/system/hooks/useToastWatcher.ts | 17 +- .../features/system/store/modelSelectors.ts | 5 + .../src/features/system/store/modelSlice.ts | 80 +++ .../system/store/modelsPersistBlacklist.ts | 10 + .../system/store/systemPersistsBlacklist.ts | 26 + .../src/features/system/store/systemSlice.ts | 248 ++++++++ .../ui/components/FloatingGalleryButton.tsx | 4 +- .../FloatingParametersPanelButtons.tsx | 2 +- .../src/features/ui/components/InvokeTabs.tsx | 103 ++-- .../features/ui/components/InvokeWorkarea.tsx | 6 +- .../ui/components/ParametersPanel.tsx | 1 - .../tabs/ImageToImage/ImageToImageContent.tsx | 50 -- .../ImageToImage/ImageToImageParameters.tsx | 81 --- .../ImageToImage/ImageToImageWorkarea.tsx | 11 - .../tabs/ImageToImage/InitImagePreview.tsx | 85 --- .../LinearContent.tsx} | 4 +- .../LinearParameters.tsx} | 42 +- .../components/tabs/Linear/LinearWorkarea.tsx | 11 + .../tabs/TextToImage/TextToImageWorkarea.tsx | 11 - .../UnifiedCanvas/UnifiedCanvasParameters.tsx | 16 +- .../src/features/ui/store/extraReducers.ts | 13 + .../web/src/features/ui/store/tabMap.ts | 9 +- .../features/ui/store/uiPersistBlacklist.ts | 10 + .../web/src/features/ui/store/uiSlice.ts | 30 +- .../web/src/features/ui/store/uiTypes.ts | 6 + .../web/src/services/api/core/ApiError.ts | 24 + .../services/api/core/ApiRequestOptions.ts | 16 + .../web/src/services/api/core/ApiResult.ts | 10 + .../services/api/core/CancelablePromise.ts | 128 +++++ .../web/src/services/api/core/OpenAPI.ts | 31 + .../web/src/services/api/core/request.ts | 351 ++++++++++++ .../frontend/web/src/services/api/index.ts | 133 +++++ .../src/services/api/models/AddInvocation.ts | 23 + .../src/services/api/models/BlurInvocation.ts | 29 + .../services/api/models/Body_upload_image.ts | 8 + .../src/services/api/models/CkptModelInfo.ts | 32 ++ .../services/api/models/CollectInvocation.ts | 23 + .../api/models/CollectInvocationOutput.ts | 15 + .../services/api/models/CreateModelRequest.ts | 18 + .../api/models/CropImageInvocation.ts | 37 ++ .../api/models/CvInpaintInvocation.ts | 25 + .../services/api/models/DiffusersModelInfo.ts | 26 + .../services/api/models/DivideInvocation.ts | 23 + .../web/src/services/api/models/Edge.ts | 17 + .../src/services/api/models/EdgeConnection.ts | 15 + .../web/src/services/api/models/Graph.ts | 49 ++ .../api/models/GraphExecutionState.ts | 58 ++ .../services/api/models/GraphInvocation.ts | 22 + .../api/models/GraphInvocationOutput.ts | 11 + .../api/models/HTTPValidationError.ts | 10 + .../web/src/services/api/models/ImageField.ts | 20 + .../src/services/api/models/ImageOutput.ts | 25 + .../src/services/api/models/ImageResponse.ts | 33 ++ .../api/models/ImageResponseMetadata.ts | 28 + .../api/models/ImageToImageInvocation.ts | 69 +++ .../web/src/services/api/models/ImageType.ts | 8 + .../services/api/models/InpaintInvocation.ts | 77 +++ .../api/models/IntCollectionOutput.ts | 15 + .../web/src/services/api/models/IntOutput.ts | 15 + .../api/models/InverseLerpInvocation.ts | 29 + .../services/api/models/InvokeAIMetadata.ts | 12 + .../services/api/models/IterateInvocation.ts | 24 + .../api/models/IterateInvocationOutput.ts | 15 + .../src/services/api/models/LatentsField.ts | 14 + .../src/services/api/models/LatentsOutput.ts | 17 + .../api/models/LatentsToImageInvocation.ts | 25 + .../api/models/LatentsToLatentsInvocation.ts | 73 +++ .../src/services/api/models/LerpInvocation.ts | 29 + .../api/models/LoadImageInvocation.ts | 25 + .../api/models/MaskFromAlphaInvocation.ts | 25 + .../web/src/services/api/models/MaskOutput.ts | 17 + .../services/api/models/MetadataImageField.ts | 11 + .../api/models/MetadataLatentsField.ts | 8 + .../web/src/services/api/models/ModelsList.ts | 11 + .../services/api/models/MultiplyInvocation.ts | 23 + .../services/api/models/NoiseInvocation.ts | 27 + .../src/services/api/models/NoiseOutput.ts | 17 + .../PaginatedResults_GraphExecutionState_.ts | 32 ++ .../models/PaginatedResults_ImageResponse_.ts | 32 ++ .../services/api/models/ParamIntInvocation.ts | 19 + .../api/models/PasteImageInvocation.ts | 37 ++ .../src/services/api/models/PromptOutput.ts | 15 + .../api/models/RandomRangeInvocation.ts | 31 + .../services/api/models/RangeInvocation.ts | 27 + .../api/models/RestoreFaceInvocation.ts | 25 + .../api/models/ShowImageInvocation.ts | 21 + .../services/api/models/SubtractInvocation.ts | 23 + .../api/models/TextToImageInvocation.ts | 55 ++ .../api/models/TextToLatentsInvocation.ts | 65 +++ .../services/api/models/UpscaleInvocation.ts | 29 + .../web/src/services/api/models/VaeRepo.ts | 19 + .../services/api/models/ValidationError.ts | 10 + .../services/api/schemas/$AddInvocation.ts | 24 + .../services/api/schemas/$BlurInvocation.ts | 30 + .../api/schemas/$Body_upload_image.ts | 12 + .../services/api/schemas/$CkptModelInfo.ts | 37 ++ .../api/schemas/$CollectInvocation.ts | 28 + .../api/schemas/$CollectInvocationOutput.ts | 20 + .../api/schemas/$CreateModelRequest.ts | 22 + .../api/schemas/$CropImageInvocation.ts | 39 ++ .../api/schemas/$CvInpaintInvocation.ts | 30 + .../api/schemas/$DiffusersModelInfo.ts | 29 + .../services/api/schemas/$DivideInvocation.ts | 24 + .../web/src/services/api/schemas/$Edge.ts | 23 + .../services/api/schemas/$EdgeConnection.ts | 17 + .../web/src/services/api/schemas/$Graph.ts | 80 +++ .../api/schemas/$GraphExecutionState.ts | 95 ++++ .../services/api/schemas/$GraphInvocation.ts | 24 + .../api/schemas/$GraphInvocationOutput.ts | 12 + .../api/schemas/$HTTPValidationError.ts | 13 + .../src/services/api/schemas/$ImageField.ts | 21 + .../src/services/api/schemas/$ImageOutput.ts | 30 + .../services/api/schemas/$ImageResponse.ts | 39 ++ .../api/schemas/$ImageResponseMetadata.ts | 30 + .../api/schemas/$ImageToImageInvocation.ts | 75 +++ .../src/services/api/schemas/$ImageType.ts | 6 + .../api/schemas/$InpaintInvocation.ts | 87 +++ .../api/schemas/$IntCollectionOutput.ts | 17 + .../src/services/api/schemas/$IntOutput.ts | 15 + .../api/schemas/$InverseLerpInvocation.ts | 33 ++ .../services/api/schemas/$InvokeAIMetadata.ts | 29 + .../api/schemas/$IterateInvocation.ts | 28 + .../api/schemas/$IterateInvocationOutput.ts | 18 + .../src/services/api/schemas/$LatentsField.ts | 13 + .../services/api/schemas/$LatentsOutput.ts | 18 + .../api/schemas/$LatentsToImageInvocation.ts | 27 + .../schemas/$LatentsToLatentsInvocation.ts | 81 +++ .../services/api/schemas/$LerpInvocation.ts | 33 ++ .../api/schemas/$LoadImageInvocation.ts | 29 + .../api/schemas/$MaskFromAlphaInvocation.ts | 27 + .../src/services/api/schemas/$MaskOutput.ts | 20 + .../api/schemas/$MetadataImageField.ts | 15 + .../api/schemas/$MetadataLatentsField.ts | 11 + .../src/services/api/schemas/$ModelsList.ts | 19 + .../api/schemas/$MultiplyInvocation.ts | 24 + .../services/api/schemas/$NoiseInvocation.ts | 31 + .../src/services/api/schemas/$NoiseOutput.ts | 18 + .../$PaginatedResults_GraphExecutionState_.ts | 35 ++ .../$PaginatedResults_ImageResponse_.ts | 35 ++ .../api/schemas/$ParamIntInvocation.ts | 20 + .../api/schemas/$PasteImageInvocation.ts | 45 ++ .../src/services/api/schemas/$PromptOutput.ts | 17 + .../api/schemas/$RandomRangeInvocation.ts | 33 ++ .../services/api/schemas/$RangeInvocation.ts | 28 + .../api/schemas/$RestoreFaceInvocation.ts | 28 + .../api/schemas/$ShowImageInvocation.ts | 23 + .../api/schemas/$SubtractInvocation.ts | 24 + .../api/schemas/$TextToImageInvocation.ts | 59 ++ .../api/schemas/$TextToLatentsInvocation.ts | 70 +++ .../api/schemas/$UpscaleInvocation.ts | 31 + .../web/src/services/api/schemas/$VaeRepo.ts | 20 + .../services/api/schemas/$ValidationError.ts | 27 + .../services/api/services/ImagesService.ts | 139 +++++ .../services/api/services/ModelsService.ts | 72 +++ .../services/api/services/SessionsService.ts | 381 +++++++++++++ .../web/src/services/events/actions.ts | 50 ++ .../web/src/services/events/middleware.ts | 221 ++++++++ .../frontend/web/src/services/events/types.ts | 109 ++++ .../web/src/services/fixtures/openapi.json | 1 + .../web/src/services/fixtures/request.ts | 351 ++++++++++++ .../web/src/services/thunks/gallery.ts | 30 + .../frontend/web/src/services/thunks/image.ts | 36 ++ .../frontend/web/src/services/thunks/model.ts | 24 + .../web/src/services/thunks/schema.ts | 14 + .../web/src/services/thunks/session.ts | 132 +++++ .../frontend/web/src/services/types/guards.ts | 33 ++ .../services/util/deserializeImageField.ts | 29 + .../services/util/deserializeImageResponse.ts | 29 + .../web/src/services/util/getHeaders.ts | 12 + .../src/services/util/makeGraphOfXImages.ts | 24 + .../web/src/theme/components/progress.ts | 4 +- .../web/src/theme/components/scrollbar.ts | 8 +- .../web/src/theme/components/slider.ts | 7 + invokeai/frontend/web/tests/metadata.ts | 174 ++++++ invokeai/frontend/web/tsconfig.json | 1 + invokeai/frontend/web/vite.config.ts | 17 + invokeai/frontend/web/yarn.lock | 535 +++++++++++++++++- tests/nodes/test_graph_execution_state.py | 1 + tests/nodes/test_invoker.py | 1 + tests/nodes/test_nodes.py | 2 +- tests/nodes/test_png_metadata_service.py | 55 ++ 324 files changed, 13051 insertions(+), 1400 deletions(-) rename invokeai/app/invocations/util/{get_model.py => choose_model.py} (53%) delete mode 100644 invokeai/app/models/metadata.py create mode 100644 invokeai/app/services/metadata.py create mode 100644 invokeai/app/util/misc.py delete mode 100644 invokeai/app/util/save_thumbnail.py create mode 100644 invokeai/app/util/thumbnails.py create mode 100644 invokeai/frontend/web/docs/API_CLIENT.md create mode 100644 invokeai/frontend/web/docs/EVENTS.md create mode 100644 invokeai/frontend/web/docs/NODE_EDITOR.md create mode 100644 invokeai/frontend/web/docs/PACKAGE_SCRIPTS.md rename invokeai/frontend/web/{ => docs}/README.md (88%) create mode 100644 invokeai/frontend/web/src/app/storeUtils.ts create mode 100644 invokeai/frontend/web/src/common/components/ImageToImageOverlay.tsx create mode 100644 invokeai/frontend/web/src/common/components/SelectImagePlaceholder.tsx create mode 100644 invokeai/frontend/web/src/common/util/_parseMetadataZod.ts create mode 100644 invokeai/frontend/web/src/common/util/getTimestamp.ts create mode 100644 invokeai/frontend/web/src/common/util/getUrl.ts create mode 100644 invokeai/frontend/web/src/common/util/parseMetadata.ts create mode 100644 invokeai/frontend/web/src/features/canvas/store/canvasPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/OLD_ImageMetadataViewer.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts create mode 100644 invokeai/frontend/web/src/features/gallery/store/galleryPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/features/gallery/store/resultsPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts delete mode 100644 invokeai/frontend/web/src/features/gallery/store/thunks/uploadImage.ts create mode 100644 invokeai/frontend/web/src/features/gallery/store/uploadsPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts create mode 100644 invokeai/frontend/web/src/features/lightbox/store/lightboxPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/Flow.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputField.tsx.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/types.ts create mode 100644 invokeai/frontend/web/src/features/nodes/examples/iterationGraph.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useInvocationTemplate.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/nodesPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/selectors/invocationTemplatesSelector.ts create mode 100644 invokeai/frontend/web/src/features/nodes/types/constants.ts create mode 100644 invokeai/frontend/web/src/features/nodes/types/typeGuards.ts create mode 100644 invokeai/frontend/web/src/features/nodes/types/types.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildEdges.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildImageToImageNode.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildIterateNode.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildLinearGraph.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildRangeNode.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildTextToImageNode.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/nodesGraphBuilder/buildNodesGraph.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/parseSchema.ts create mode 100644 invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageToggle.tsx create mode 100644 invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/InitialImagePreview.tsx create mode 100644 invokeai/frontend/web/src/features/parameters/store/generationPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/features/parameters/store/postprocessingPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/features/system/store/modelSelectors.ts create mode 100644 invokeai/frontend/web/src/features/system/store/modelSlice.ts create mode 100644 invokeai/frontend/web/src/features/system/store/modelsPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/features/system/store/systemPersistsBlacklist.ts delete mode 100644 invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageContent.tsx delete mode 100644 invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageParameters.tsx delete mode 100644 invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageWorkarea.tsx delete mode 100644 invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/InitImagePreview.tsx rename invokeai/frontend/web/src/features/ui/components/tabs/{TextToImage/TextToImageContent.tsx => Linear/LinearContent.tsx} (85%) rename invokeai/frontend/web/src/features/ui/components/tabs/{TextToImage/TextToImageParameters.tsx => Linear/LinearParameters.tsx} (65%) create mode 100644 invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearWorkarea.tsx delete mode 100644 invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageWorkarea.tsx create mode 100644 invokeai/frontend/web/src/features/ui/store/extraReducers.ts create mode 100644 invokeai/frontend/web/src/features/ui/store/uiPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/services/api/core/ApiError.ts create mode 100644 invokeai/frontend/web/src/services/api/core/ApiRequestOptions.ts create mode 100644 invokeai/frontend/web/src/services/api/core/ApiResult.ts create mode 100644 invokeai/frontend/web/src/services/api/core/CancelablePromise.ts create mode 100644 invokeai/frontend/web/src/services/api/core/OpenAPI.ts create mode 100644 invokeai/frontend/web/src/services/api/core/request.ts create mode 100644 invokeai/frontend/web/src/services/api/index.ts create mode 100644 invokeai/frontend/web/src/services/api/models/AddInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/BlurInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/Body_upload_image.ts create mode 100644 invokeai/frontend/web/src/services/api/models/CkptModelInfo.ts create mode 100644 invokeai/frontend/web/src/services/api/models/CollectInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/CollectInvocationOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/CreateModelRequest.ts create mode 100644 invokeai/frontend/web/src/services/api/models/CropImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/CvInpaintInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/DiffusersModelInfo.ts create mode 100644 invokeai/frontend/web/src/services/api/models/DivideInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/Edge.ts create mode 100644 invokeai/frontend/web/src/services/api/models/EdgeConnection.ts create mode 100644 invokeai/frontend/web/src/services/api/models/Graph.ts create mode 100644 invokeai/frontend/web/src/services/api/models/GraphExecutionState.ts create mode 100644 invokeai/frontend/web/src/services/api/models/GraphInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/GraphInvocationOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/HTTPValidationError.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ImageField.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ImageOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ImageResponse.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ImageResponseMetadata.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ImageToImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ImageType.ts create mode 100644 invokeai/frontend/web/src/services/api/models/InpaintInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/IntCollectionOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/IntOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/InverseLerpInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/InvokeAIMetadata.ts create mode 100644 invokeai/frontend/web/src/services/api/models/IterateInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/IterateInvocationOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/LatentsField.ts create mode 100644 invokeai/frontend/web/src/services/api/models/LatentsOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/LatentsToImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/LerpInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/LoadImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/MaskFromAlphaInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/MaskOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/MetadataImageField.ts create mode 100644 invokeai/frontend/web/src/services/api/models/MetadataLatentsField.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ModelsList.ts create mode 100644 invokeai/frontend/web/src/services/api/models/MultiplyInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/NoiseInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/NoiseOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/PaginatedResults_GraphExecutionState_.ts create mode 100644 invokeai/frontend/web/src/services/api/models/PaginatedResults_ImageResponse_.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ParamIntInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/PasteImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/PromptOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/RandomRangeInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/RangeInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/RestoreFaceInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ShowImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/SubtractInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/TextToImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/UpscaleInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/VaeRepo.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ValidationError.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$AddInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$BlurInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$Body_upload_image.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$CkptModelInfo.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$CollectInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$CollectInvocationOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$CreateModelRequest.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$CropImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$CvInpaintInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$DiffusersModelInfo.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$DivideInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$Edge.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$EdgeConnection.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$Graph.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$GraphExecutionState.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$GraphInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$GraphInvocationOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$HTTPValidationError.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ImageField.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ImageOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ImageResponse.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ImageResponseMetadata.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ImageToImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ImageType.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$InpaintInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$IntCollectionOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$IntOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$InverseLerpInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$InvokeAIMetadata.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$IterateInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$IterateInvocationOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$LatentsField.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$LatentsOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$LatentsToImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$LerpInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$LoadImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$MaskFromAlphaInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$MaskOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$MetadataImageField.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$MetadataLatentsField.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ModelsList.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$MultiplyInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$NoiseInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$NoiseOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_GraphExecutionState_.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_ImageResponse_.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ParamIntInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$PasteImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$PromptOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$RandomRangeInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$RangeInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$RestoreFaceInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ShowImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$SubtractInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$TextToImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$UpscaleInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$VaeRepo.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ValidationError.ts create mode 100644 invokeai/frontend/web/src/services/api/services/ImagesService.ts create mode 100644 invokeai/frontend/web/src/services/api/services/ModelsService.ts create mode 100644 invokeai/frontend/web/src/services/api/services/SessionsService.ts create mode 100644 invokeai/frontend/web/src/services/events/actions.ts create mode 100644 invokeai/frontend/web/src/services/events/middleware.ts create mode 100644 invokeai/frontend/web/src/services/events/types.ts create mode 100644 invokeai/frontend/web/src/services/fixtures/openapi.json create mode 100644 invokeai/frontend/web/src/services/fixtures/request.ts create mode 100644 invokeai/frontend/web/src/services/thunks/gallery.ts create mode 100644 invokeai/frontend/web/src/services/thunks/image.ts create mode 100644 invokeai/frontend/web/src/services/thunks/model.ts create mode 100644 invokeai/frontend/web/src/services/thunks/schema.ts create mode 100644 invokeai/frontend/web/src/services/thunks/session.ts create mode 100644 invokeai/frontend/web/src/services/types/guards.ts create mode 100644 invokeai/frontend/web/src/services/util/deserializeImageField.ts create mode 100644 invokeai/frontend/web/src/services/util/deserializeImageResponse.ts create mode 100644 invokeai/frontend/web/src/services/util/getHeaders.ts create mode 100644 invokeai/frontend/web/src/services/util/makeGraphOfXImages.ts create mode 100644 invokeai/frontend/web/tests/metadata.ts create mode 100644 tests/nodes/test_png_metadata_service.py diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index cd5d8a61b2..f33bfff26e 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -3,6 +3,8 @@ import os from argparse import Namespace +from invokeai.app.services.metadata import PngMetadataService, MetadataServiceBase + from ..services.default_graphs import create_system_graphs from ..services.latent_storage import DiskLatentsStorage, ForwardCacheLatentsStorage @@ -60,7 +62,9 @@ class ApiDependencies: latents = ForwardCacheLatentsStorage(DiskLatentsStorage(f'{output_folder}/latents')) - images = DiskImageStorage(f'{output_folder}/images') + metadata = PngMetadataService() + + images = DiskImageStorage(f'{output_folder}/images', metadata_service=metadata) # TODO: build a file/path manager? db_location = os.path.join(output_folder, "invokeai.db") @@ -70,6 +74,7 @@ class ApiDependencies: events=events, latents=latents, images=images, + metadata=metadata, queue=MemoryInvocationQueue(), graph_library=SqliteItemStorage[LibraryGraph]( filename=db_location, table_name="graphs" diff --git a/invokeai/app/api/models/images.py b/invokeai/app/api/models/images.py index 5ff0a48a44..53caca63f5 100644 --- a/invokeai/app/api/models/images.py +++ b/invokeai/app/api/models/images.py @@ -1,7 +1,19 @@ +from typing import Optional from pydantic import BaseModel, Field from invokeai.app.models.image import ImageType -from invokeai.app.models.metadata import ImageMetadata +from invokeai.app.services.metadata import InvokeAIMetadata + + +class ImageResponseMetadata(BaseModel): + """An image's metadata. Used only in HTTP responses.""" + + created: int = Field(description="The creation timestamp of the image") + width: int = Field(description="The width of the image in pixels") + height: int = Field(description="The height of the image in pixels") + invokeai: Optional[InvokeAIMetadata] = Field( + description="The image's InvokeAI-specific metadata" + ) class ImageResponse(BaseModel): @@ -11,4 +23,12 @@ class ImageResponse(BaseModel): image_name: str = Field(description="The name of the image") image_url: str = Field(description="The url of the image") thumbnail_url: str = Field(description="The url of the image's thumbnail") - metadata: ImageMetadata = Field(description="The image's metadata") + metadata: ImageResponseMetadata = Field(description="The image's metadata") + + +class ProgressImage(BaseModel): + """The progress image sent intermittently during processing""" + + width: int = Field(description="The effective width of the image in pixels") + height: int = Field(description="The effective height of the image in pixels") + dataURL: str = Field(description="The image data as a b64 data URL") diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index bb3aabae6d..14a84a5dd4 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -1,13 +1,17 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) - +import io from datetime import datetime, timezone +import json +import os +from typing import Any import uuid -from fastapi import Path, Query, Request, UploadFile +from fastapi import HTTPException, Path, Query, Request, UploadFile from fastapi.responses import FileResponse, Response from fastapi.routing import APIRouter from PIL import Image -from invokeai.app.api.models.images import ImageResponse +from invokeai.app.api.models.images import ImageResponse, ImageResponseMetadata +from invokeai.app.services.metadata import InvokeAIMetadata from invokeai.app.services.item_storage import PaginatedResults from ...services.image_storage import ImageType @@ -15,70 +19,110 @@ from ..dependencies import ApiDependencies images_router = APIRouter(prefix="/v1/images", tags=["images"]) + @images_router.get("/{image_type}/{image_name}", operation_id="get_image") async def get_image( image_type: ImageType = Path(description="The type of image to get"), image_name: str = Path(description="The name of the image to get"), -): +) -> FileResponse | Response: """Gets a result""" - # TODO: This is not really secure at all. At least make sure only output results are served - filename = ApiDependencies.invoker.services.images.get_path(image_type, image_name) - return FileResponse(filename) -@images_router.get("/{image_type}/thumbnails/{image_name}", operation_id="get_thumbnail") + path = ApiDependencies.invoker.services.images.get_path( + image_type=image_type, image_name=image_name + ) + + if ApiDependencies.invoker.services.images.validate_path(path): + return FileResponse(path) + else: + raise HTTPException(status_code=404) + + +@images_router.get( + "/{image_type}/thumbnails/{image_name}", operation_id="get_thumbnail" +) async def get_thumbnail( image_type: ImageType = Path(description="The type of image to get"), image_name: str = Path(description="The name of the image to get"), -): +) -> FileResponse | Response: """Gets a thumbnail""" - # TODO: This is not really secure at all. At least make sure only output results are served - filename = ApiDependencies.invoker.services.images.get_path(image_type, 'thumbnails/' + image_name) - return FileResponse(filename) + + path = ApiDependencies.invoker.services.images.get_path( + image_type=image_type, image_name=image_name, is_thumbnail=True + ) + + if ApiDependencies.invoker.services.images.validate_path(path): + return FileResponse(path) + else: + raise HTTPException(status_code=404) @images_router.post( "/uploads/", operation_id="upload_image", responses={ - 201: {"description": "The image was uploaded successfully"}, - 404: {"description": "Session not found"}, + 201: { + "description": "The image was uploaded successfully", + "model": ImageResponse, + }, + 415: {"description": "Image upload failed"}, }, + status_code=201, ) -async def upload_image(file: UploadFile, request: Request): +async def upload_image( + file: UploadFile, request: Request, response: Response +) -> ImageResponse: if not file.content_type.startswith("image"): - return Response(status_code=415) + raise HTTPException(status_code=415, detail="Not an image") contents = await file.read() + try: - im = Image.open(contents) + img = Image.open(io.BytesIO(contents)) except: # Error opening the image - return Response(status_code=415) + raise HTTPException(status_code=415, detail="Failed to read image") filename = f"{uuid.uuid4()}_{str(int(datetime.now(timezone.utc).timestamp()))}.png" - ApiDependencies.invoker.services.images.save(ImageType.UPLOAD, filename, im) - return Response( - status_code=201, - headers={ - "Location": request.url_for( - "get_image", image_type=ImageType.UPLOAD.value, image_name=filename - ) - }, + (image_path, thumbnail_path, ctime) = ApiDependencies.invoker.services.images.save( + ImageType.UPLOAD, filename, img ) + invokeai_metadata = ApiDependencies.invoker.services.metadata.get_metadata(img) + + res = ImageResponse( + image_type=ImageType.UPLOAD, + image_name=filename, + image_url=f"api/v1/images/{ImageType.UPLOAD.value}/{filename}", + thumbnail_url=f"api/v1/images/{ImageType.UPLOAD.value}/thumbnails/{os.path.splitext(filename)[0]}.webp", + metadata=ImageResponseMetadata( + created=ctime, + width=img.width, + height=img.height, + invokeai=invokeai_metadata, + ), + ) + + response.status_code = 201 + response.headers["Location"] = request.url_for( + "get_image", image_type=ImageType.UPLOAD.value, image_name=filename + ) + + return res + + @images_router.get( "/", operation_id="list_images", responses={200: {"model": PaginatedResults[ImageResponse]}}, ) async def list_images( - image_type: ImageType = Query(default=ImageType.RESULT, description="The type of images to get"), + image_type: ImageType = Query( + default=ImageType.RESULT, description="The type of images to get" + ), page: int = Query(default=0, description="The page of images to get"), per_page: int = Query(default=10, description="The number of images per page"), ) -> PaginatedResults[ImageResponse]: """Gets a list of images""" - result = ApiDependencies.invoker.services.images.list( - image_type, page, per_page - ) + result = ApiDependencies.invoker.services.images.list(image_type, page, per_page) return result diff --git a/invokeai/app/cli_app.py b/invokeai/app/cli_app.py index 86fd18ca60..9ac156916b 100644 --- a/invokeai/app/cli_app.py +++ b/invokeai/app/cli_app.py @@ -13,6 +13,8 @@ from typing import ( from pydantic import BaseModel from pydantic.fields import Field +from invokeai.app.services.metadata import PngMetadataService + from .services.default_graphs import create_system_graphs from .services.latent_storage import DiskLatentsStorage, ForwardCacheLatentsStorage @@ -200,6 +202,8 @@ def invoke_cli(): events = EventServiceBase() + metadata = PngMetadataService() + output_folder = os.path.abspath( os.path.join(os.path.dirname(__file__), "../../../outputs") ) @@ -211,7 +215,8 @@ def invoke_cli(): model_manager=model_manager, events=events, latents = ForwardCacheLatentsStorage(DiskLatentsStorage(f'{output_folder}/latents')), - images=DiskImageStorage(f'{output_folder}/images'), + images=DiskImageStorage(f'{output_folder}/images', metadata_service=metadata), + metadata=metadata, queue=MemoryInvocationQueue(), graph_library=SqliteItemStorage[LibraryGraph]( filename=db_location, table_name="graphs" diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index 3590129b96..7daaa588b1 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -95,7 +95,7 @@ class UIConfig(TypedDict, total=False): ], ] tags: List[str] - + title: str class CustomisedSchemaExtra(TypedDict): ui: UIConfig diff --git a/invokeai/app/invocations/collections.py b/invokeai/app/invocations/collections.py index c68b7449cc..24a89c2cf4 100644 --- a/invokeai/app/invocations/collections.py +++ b/invokeai/app/invocations/collections.py @@ -1,16 +1,17 @@ # Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654) -from typing import Literal +from typing import Literal, Optional -import cv2 as cv import numpy as np import numpy.random -from PIL import Image, ImageOps from pydantic import Field -from ..services.image_storage import ImageType -from .baseinvocation import BaseInvocation, InvocationContext, BaseInvocationOutput -from .image import ImageField, ImageOutput +from .baseinvocation import ( + BaseInvocation, + InvocationConfig, + InvocationContext, + BaseInvocationOutput, +) class IntCollectionOutput(BaseInvocationOutput): @@ -33,7 +34,9 @@ class RangeInvocation(BaseInvocation): step: int = Field(default=1, description="The step of the range") def invoke(self, context: InvocationContext) -> IntCollectionOutput: - return IntCollectionOutput(collection=list(range(self.start, self.stop, self.step))) + return IntCollectionOutput( + collection=list(range(self.start, self.stop, self.step)) + ) class RandomRangeInvocation(BaseInvocation): @@ -43,8 +46,19 @@ class RandomRangeInvocation(BaseInvocation): # Inputs low: int = Field(default=0, description="The inclusive low value") - high: int = Field(default=np.iinfo(np.int32).max, description="The exclusive high value") + high: int = Field( + default=np.iinfo(np.int32).max, description="The exclusive high value" + ) size: int = Field(default=1, description="The number of values to generate") + seed: Optional[int] = Field( + ge=0, + le=np.iinfo(np.int32).max, + description="The seed for the RNG", + default_factory=lambda: numpy.random.randint(0, np.iinfo(np.int32).max), + ) def invoke(self, context: InvocationContext) -> IntCollectionOutput: - return IntCollectionOutput(collection=list(numpy.random.randint(self.low, self.high, size=self.size))) + rng = np.random.default_rng(self.seed) + return IntCollectionOutput( + collection=list(rng.integers(low=self.low, high=self.high, size=self.size)) + ) diff --git a/invokeai/app/invocations/cv.py b/invokeai/app/invocations/cv.py index 52e59b16ac..5a6d703d83 100644 --- a/invokeai/app/invocations/cv.py +++ b/invokeai/app/invocations/cv.py @@ -9,7 +9,7 @@ from pydantic import BaseModel, Field from invokeai.app.models.image import ImageField, ImageType from .baseinvocation import BaseInvocation, InvocationContext, InvocationConfig -from .image import ImageOutput +from .image import ImageOutput, build_image_output class CvInvocationConfig(BaseModel): @@ -56,7 +56,14 @@ class CvInpaintInvocation(BaseInvocation, CvInvocationConfig): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, image_inpainted) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self ) + + context.services.images.save(image_type, image_name, image_inpainted, metadata) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=image_inpainted, + ) \ No newline at end of file diff --git a/invokeai/app/invocations/generate.py b/invokeai/app/invocations/generate.py index d0eeeae698..df79baa0f3 100644 --- a/invokeai/app/invocations/generate.py +++ b/invokeai/app/invocations/generate.py @@ -9,13 +9,12 @@ from torch import Tensor from pydantic import BaseModel, Field from invokeai.app.models.image import ImageField, ImageType -from invokeai.app.invocations.util.get_model import choose_model +from invokeai.app.invocations.util.choose_model import choose_model from .baseinvocation import BaseInvocation, InvocationContext, InvocationConfig -from .image import ImageOutput +from .image import ImageOutput, build_image_output from ...backend.generator import Txt2Img, Img2Img, Inpaint, InvokeAIGenerator from ...backend.stable_diffusion import PipelineIntermediateState -from ..models.exceptions import CanceledException -from ..util.step_callback import diffusers_step_callback_adapter +from ..util.step_callback import stable_diffusion_step_callback SAMPLER_NAME_VALUES = Literal[tuple(InvokeAIGenerator.schedulers())] @@ -58,28 +57,31 @@ class TextToImageInvocation(BaseInvocation, SDImageInvocation): # TODO: pass this an emitter method or something? or a session for dispatching? def dispatch_progress( - self, context: InvocationContext, intermediate_state: PipelineIntermediateState + self, + context: InvocationContext, + source_node_id: str, + intermediate_state: PipelineIntermediateState, ) -> None: - if (context.services.queue.is_canceled(context.graph_execution_state_id)): - raise CanceledException - - step = intermediate_state.step - if intermediate_state.predicted_original is not None: - # Some schedulers report not only the noisy latents at the current timestep, - # but also their estimate so far of what the de-noised latents will be. - sample = intermediate_state.predicted_original - else: - sample = intermediate_state.latents - - diffusers_step_callback_adapter(sample, step, steps=self.steps, id=self.id, context=context) + stable_diffusion_step_callback( + context=context, + intermediate_state=intermediate_state, + node=self.dict(), + source_node_id=source_node_id, + ) def invoke(self, context: InvocationContext) -> ImageOutput: # Handle invalid model parameter model = choose_model(context.services.model_manager, self.model) + # Get the source node id (we are invoking the prepared node) + graph_execution_state = context.services.graph_execution_manager.get( + context.graph_execution_state_id + ) + source_node_id = graph_execution_state.prepared_source_mapping[self.id] + outputs = Txt2Img(model).generate( prompt=self.prompt, - step_callback=partial(self.dispatch_progress, context), + step_callback=partial(self.dispatch_progress, context, source_node_id), **self.dict( exclude={"prompt"} ), # Shorthand for passing all of the parameters above manually @@ -95,9 +97,18 @@ class TextToImageInvocation(BaseInvocation, SDImageInvocation): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, generate_output.image) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save( + image_type, image_name, generate_output.image, metadata + ) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=generate_output.image, ) @@ -117,20 +128,17 @@ class ImageToImageInvocation(TextToImageInvocation): ) def dispatch_progress( - self, context: InvocationContext, intermediate_state: PipelineIntermediateState - ) -> None: - if (context.services.queue.is_canceled(context.graph_execution_state_id)): - raise CanceledException - - step = intermediate_state.step - if intermediate_state.predicted_original is not None: - # Some schedulers report not only the noisy latents at the current timestep, - # but also their estimate so far of what the de-noised latents will be. - sample = intermediate_state.predicted_original - else: - sample = intermediate_state.latents - - diffusers_step_callback_adapter(sample, step, steps=self.steps, id=self.id, context=context) + self, + context: InvocationContext, + source_node_id: str, + intermediate_state: PipelineIntermediateState, + ) -> None: + stable_diffusion_step_callback( + context=context, + intermediate_state=intermediate_state, + node=self.dict(), + source_node_id=source_node_id, + ) def invoke(self, context: InvocationContext) -> ImageOutput: image = ( @@ -145,15 +153,21 @@ class ImageToImageInvocation(TextToImageInvocation): # Handle invalid model parameter model = choose_model(context.services.model_manager, self.model) + # Get the source node id (we are invoking the prepared node) + graph_execution_state = context.services.graph_execution_manager.get( + context.graph_execution_state_id + ) + source_node_id = graph_execution_state.prepared_source_mapping[self.id] + outputs = Img2Img(model).generate( - prompt=self.prompt, - init_image=image, - init_mask=mask, - step_callback=partial(self.dispatch_progress, context), - **self.dict( - exclude={"prompt", "image", "mask"} - ), # Shorthand for passing all of the parameters above manually - ) + prompt=self.prompt, + init_image=image, + init_mask=mask, + step_callback=partial(self.dispatch_progress, context, source_node_id), + **self.dict( + exclude={"prompt", "image", "mask"} + ), # Shorthand for passing all of the parameters above manually + ) # Outputs is an infinite iterator that will return a new InvokeAIGeneratorOutput object # each time it is called. We only need the first one. @@ -168,11 +182,19 @@ class ImageToImageInvocation(TextToImageInvocation): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, result_image) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self ) + context.services.images.save(image_type, image_name, result_image, metadata) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=result_image, + ) + + class InpaintInvocation(ImageToImageInvocation): """Generates an image using inpaint.""" @@ -188,20 +210,17 @@ class InpaintInvocation(ImageToImageInvocation): ) def dispatch_progress( - self, context: InvocationContext, intermediate_state: PipelineIntermediateState - ) -> None: - if (context.services.queue.is_canceled(context.graph_execution_state_id)): - raise CanceledException - - step = intermediate_state.step - if intermediate_state.predicted_original is not None: - # Some schedulers report not only the noisy latents at the current timestep, - # but also their estimate so far of what the de-noised latents will be. - sample = intermediate_state.predicted_original - else: - sample = intermediate_state.latents - - diffusers_step_callback_adapter(sample, step, steps=self.steps, id=self.id, context=context) + self, + context: InvocationContext, + source_node_id: str, + intermediate_state: PipelineIntermediateState, + ) -> None: + stable_diffusion_step_callback( + context=context, + intermediate_state=intermediate_state, + node=self.dict(), + source_node_id=source_node_id, + ) def invoke(self, context: InvocationContext) -> ImageOutput: image = ( @@ -218,17 +237,23 @@ class InpaintInvocation(ImageToImageInvocation): ) # Handle invalid model parameter - model = choose_model(context.services.model_manager, self.model) + model = choose_model(context.services.model_manager, self.model) + + # Get the source node id (we are invoking the prepared node) + graph_execution_state = context.services.graph_execution_manager.get( + context.graph_execution_state_id + ) + source_node_id = graph_execution_state.prepared_source_mapping[self.id] outputs = Inpaint(model).generate( - prompt=self.prompt, - init_img=image, - init_mask=mask, - step_callback=partial(self.dispatch_progress, context), - **self.dict( - exclude={"prompt", "image", "mask"} - ), # Shorthand for passing all of the parameters above manually - ) + prompt=self.prompt, + init_img=image, + init_mask=mask, + step_callback=partial(self.dispatch_progress, context, source_node_id), + **self.dict( + exclude={"prompt", "image", "mask"} + ), # Shorthand for passing all of the parameters above manually + ) # Outputs is an infinite iterator that will return a new InvokeAIGeneratorOutput object # each time it is called. We only need the first one. @@ -243,7 +268,14 @@ class InpaintInvocation(ImageToImageInvocation): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, result_image) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save(image_type, image_name, result_image, metadata) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=result_image, ) diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index cc5f6b53c7..883ef63f69 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -1,6 +1,5 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) -from datetime import datetime, timezone from typing import Literal, Optional import numpy @@ -8,8 +7,12 @@ from PIL import Image, ImageFilter, ImageOps from pydantic import BaseModel, Field from ..models.image import ImageField, ImageType -from ..services.invocation_services import InvocationServices -from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationContext, InvocationConfig +from .baseinvocation import ( + BaseInvocation, + BaseInvocationOutput, + InvocationContext, + InvocationConfig, +) class PILInvocationConfig(BaseModel): @@ -22,50 +25,73 @@ class PILInvocationConfig(BaseModel): }, } + class ImageOutput(BaseInvocationOutput): """Base class for invocations that output an image""" - #fmt: off + + # fmt: off type: Literal["image"] = "image" image: ImageField = Field(default=None, description="The output image") - #fmt: on + width: Optional[int] = Field(default=None, description="The width of the image in pixels") + height: Optional[int] = Field(default=None, description="The height of the image in pixels") + # fmt: on class Config: schema_extra = { - 'required': [ - 'type', - 'image', - ] + "required": ["type", "image", "width", "height", "mode"] } + +def build_image_output( + image_type: ImageType, image_name: str, image: Image.Image +) -> ImageOutput: + """Builds an ImageOutput and its ImageField""" + image_field = ImageField( + image_name=image_name, + image_type=image_type, + ) + return ImageOutput( + image=image_field, + width=image.width, + height=image.height, + mode=image.mode, + ) + + class MaskOutput(BaseInvocationOutput): """Base class for invocations that output a mask""" - #fmt: off + + # fmt: off type: Literal["mask"] = "mask" mask: ImageField = Field(default=None, description="The output mask") - #fmt: on + # fmt: on class Config: schema_extra = { - 'required': [ - 'type', - 'mask', + "required": [ + "type", + "mask", ] } -# TODO: this isn't really necessary anymore + class LoadImageInvocation(BaseInvocation): - """Load an image from a filename and provide it as output.""" - #fmt: off + """Load an image and provide it as output.""" + + # fmt: off type: Literal["load_image"] = "load_image" # Inputs image_type: ImageType = Field(description="The type of the image") image_name: str = Field(description="The name of the image") - #fmt: on - + # fmt: on def invoke(self, context: InvocationContext) -> ImageOutput: - return ImageOutput( - image=ImageField(image_type=self.image_type, image_name=self.image_name) + image = context.services.images.get(self.image_type, self.image_name) + + return build_image_output( + image_type=self.image_type, + image_name=self.image_name, + image=image, ) @@ -86,16 +112,17 @@ class ShowImageInvocation(BaseInvocation): # TODO: how to handle failure? - return ImageOutput( - image=ImageField( - image_type=self.image.image_type, image_name=self.image.image_name - ) + return build_image_output( + image_type=self.image.image_type, + image_name=self.image.image_name, + image=image, ) class CropImageInvocation(BaseInvocation, PILInvocationConfig): """Crops an image to a specified box. The box can be outside of the image.""" - #fmt: off + + # fmt: off type: Literal["crop"] = "crop" # Inputs @@ -104,7 +131,7 @@ class CropImageInvocation(BaseInvocation, PILInvocationConfig): y: int = Field(default=0, description="The top y coordinate of the crop rectangle") width: int = Field(default=512, gt=0, description="The width of the crop rectangle") height: int = Field(default=512, gt=0, description="The height of the crop rectangle") - #fmt: on + # fmt: on def invoke(self, context: InvocationContext) -> ImageOutput: image = context.services.images.get( @@ -120,15 +147,23 @@ class CropImageInvocation(BaseInvocation, PILInvocationConfig): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, image_crop) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save(image_type, image_name, image_crop, metadata) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=image_crop, ) class PasteImageInvocation(BaseInvocation, PILInvocationConfig): """Pastes an image into another image.""" - #fmt: off + + # fmt: off type: Literal["paste"] = "paste" # Inputs @@ -137,7 +172,7 @@ class PasteImageInvocation(BaseInvocation, PILInvocationConfig): mask: Optional[ImageField] = Field(default=None, description="The mask to use when pasting") x: int = Field(default=0, description="The left x coordinate at which to paste the image") y: int = Field(default=0, description="The top y coordinate at which to paste the image") - #fmt: on + # fmt: on def invoke(self, context: InvocationContext) -> ImageOutput: base_image = context.services.images.get( @@ -170,21 +205,29 @@ class PasteImageInvocation(BaseInvocation, PILInvocationConfig): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, new_image) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save(image_type, image_name, new_image, metadata) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=new_image, ) class MaskFromAlphaInvocation(BaseInvocation, PILInvocationConfig): """Extracts the alpha channel of an image as a mask.""" - #fmt: off + + # fmt: off type: Literal["tomask"] = "tomask" # Inputs image: ImageField = Field(default=None, description="The image to create the mask from") invert: bool = Field(default=False, description="Whether or not to invert the mask") - #fmt: on + # fmt: on def invoke(self, context: InvocationContext) -> MaskOutput: image = context.services.images.get( @@ -199,22 +242,27 @@ class MaskFromAlphaInvocation(BaseInvocation, PILInvocationConfig): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, image_mask) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save(image_type, image_name, image_mask, metadata) return MaskOutput(mask=ImageField(image_type=image_type, image_name=image_name)) class BlurInvocation(BaseInvocation, PILInvocationConfig): """Blurs an image""" - #fmt: off + # fmt: off type: Literal["blur"] = "blur" # Inputs image: ImageField = Field(default=None, description="The image to blur") radius: float = Field(default=8.0, ge=0, description="The blur radius") blur_type: Literal["gaussian", "box"] = Field(default="gaussian", description="The type of blur") - #fmt: on - + # fmt: on + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.services.images.get( self.image.image_type, self.image.image_name @@ -231,22 +279,28 @@ class BlurInvocation(BaseInvocation, PILInvocationConfig): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, blur_image) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save(image_type, image_name, blur_image, metadata) + return build_image_output( + image_type=image_type, image_name=image_name, image=blur_image ) class LerpInvocation(BaseInvocation, PILInvocationConfig): """Linear interpolation of all pixels of an image""" - #fmt: off + + # fmt: off type: Literal["lerp"] = "lerp" # Inputs image: ImageField = Field(default=None, description="The image to lerp") min: int = Field(default=0, ge=0, le=255, description="The minimum output value") max: int = Field(default=255, ge=0, le=255, description="The maximum output value") - #fmt: on + # fmt: on def invoke(self, context: InvocationContext) -> ImageOutput: image = context.services.images.get( @@ -262,23 +316,29 @@ class LerpInvocation(BaseInvocation, PILInvocationConfig): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, lerp_image) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save(image_type, image_name, lerp_image, metadata) + return build_image_output( + image_type=image_type, image_name=image_name, image=lerp_image ) class InverseLerpInvocation(BaseInvocation, PILInvocationConfig): """Inverse linear interpolation of all pixels of an image""" - #fmt: off + + # fmt: off type: Literal["ilerp"] = "ilerp" # Inputs image: ImageField = Field(default=None, description="The image to lerp") min: int = Field(default=0, ge=0, le=255, description="The minimum input value") max: int = Field(default=255, ge=0, le=255, description="The maximum input value") - #fmt: on - + # fmt: on + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.services.images.get( self.image.image_type, self.image.image_name @@ -298,7 +358,12 @@ class InverseLerpInvocation(BaseInvocation, PILInvocationConfig): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, ilerp_image) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save(image_type, image_name, ilerp_image, metadata) + return build_image_output( + image_type=image_type, image_name=image_name, image=ilerp_image ) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 7593b34142..3d1c925570 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -5,9 +5,9 @@ from typing import Literal, Optional from pydantic import BaseModel, Field import torch -from invokeai.app.models.exceptions import CanceledException -from invokeai.app.invocations.util.get_model import choose_model -from invokeai.app.util.step_callback import diffusers_step_callback_adapter +from invokeai.app.invocations.util.choose_model import choose_model + +from invokeai.app.util.step_callback import stable_diffusion_step_callback from ...backend.model_management.model_manager import ModelManager from ...backend.util.devices import choose_torch_device, torch_dtype @@ -19,7 +19,7 @@ from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationCont import numpy as np from ..services.image_storage import ImageType from .baseinvocation import BaseInvocation, InvocationContext -from .image import ImageField, ImageOutput +from .image import ImageField, ImageOutput, build_image_output from ...backend.stable_diffusion import PipelineIntermediateState from diffusers.schedulers import SchedulerMixin as Scheduler import diffusers @@ -31,6 +31,8 @@ class LatentsField(BaseModel): latents_name: Optional[str] = Field(default=None, description="The name of the latents") + class Config: + schema_extra = {"required": ["latents_name"]} class LatentsOutput(BaseInvocationOutput): """Base class for invocations that output latents""" @@ -170,21 +172,14 @@ class TextToLatentsInvocation(BaseInvocation): # TODO: pass this an emitter method or something? or a session for dispatching? def dispatch_progress( - self, context: InvocationContext, intermediate_state: PipelineIntermediateState + self, context: InvocationContext, source_node_id: str, intermediate_state: PipelineIntermediateState ) -> None: - if (context.services.queue.is_canceled(context.graph_execution_state_id)): - raise CanceledException - - step = intermediate_state.step - if intermediate_state.predicted_original is not None: - # Some schedulers report not only the noisy latents at the current timestep, - # but also their estimate so far of what the de-noised latents will be. - sample = intermediate_state.predicted_original - else: - sample = intermediate_state.latents - - diffusers_step_callback_adapter(sample, step, steps=self.steps, id=self.id, context=context) - + stable_diffusion_step_callback( + context=context, + intermediate_state=intermediate_state, + node=self.dict(), + source_node_id=source_node_id, + ) def get_model(self, model_manager: ModelManager) -> StableDiffusionGeneratorPipeline: model_info = choose_model(model_manager, self.model) @@ -231,8 +226,12 @@ class TextToLatentsInvocation(BaseInvocation): def invoke(self, context: InvocationContext) -> LatentsOutput: noise = context.services.latents.get(self.noise.latents_name) + # Get the source node id (we are invoking the prepared node) + graph_execution_state = context.services.graph_execution_manager.get(context.graph_execution_state_id) + source_node_id = graph_execution_state.prepared_source_mapping[self.id] + def step_callback(state: PipelineIntermediateState): - self.dispatch_progress(context, state) + self.dispatch_progress(context, source_node_id, state) model = self.get_model(context.services.model_manager) conditioning_data = self.get_conditioning_data(model) @@ -281,8 +280,12 @@ class LatentsToLatentsInvocation(TextToLatentsInvocation): noise = context.services.latents.get(self.noise.latents_name) latent = context.services.latents.get(self.latents.latents_name) + # Get the source node id (we are invoking the prepared node) + graph_execution_state = context.services.graph_execution_manager.get(context.graph_execution_state_id) + source_node_id = graph_execution_state.prepared_source_mapping[self.id] + def step_callback(state: PipelineIntermediateState): - self.dispatch_progress(context, state) + self.dispatch_progress(context, source_node_id, state) model = self.get_model(context.services.model_manager) conditioning_data = self.get_conditioning_data(model) @@ -355,7 +358,14 @@ class LatentsToImageInvocation(BaseInvocation): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, image) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save(image_type, image_name, image, metadata) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=image ) diff --git a/invokeai/app/invocations/reconstruct.py b/invokeai/app/invocations/reconstruct.py index f6df5a2254..94a7277acd 100644 --- a/invokeai/app/invocations/reconstruct.py +++ b/invokeai/app/invocations/reconstruct.py @@ -1,12 +1,11 @@ -from datetime import datetime, timezone from typing import Literal, Union from pydantic import Field from invokeai.app.models.image import ImageField, ImageType -from ..services.invocation_services import InvocationServices + from .baseinvocation import BaseInvocation, InvocationContext, InvocationConfig -from .image import ImageOutput +from .image import ImageOutput, build_image_output class RestoreFaceInvocation(BaseInvocation): """Restores faces in an image.""" @@ -44,7 +43,14 @@ class RestoreFaceInvocation(BaseInvocation): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, results[0][0]) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self ) + + context.services.images.save(image_type, image_name, results[0][0], metadata) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=results[0][0] + ) \ No newline at end of file diff --git a/invokeai/app/invocations/upscale.py b/invokeai/app/invocations/upscale.py index 021f3569e8..c4938dfd19 100644 --- a/invokeai/app/invocations/upscale.py +++ b/invokeai/app/invocations/upscale.py @@ -1,14 +1,12 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) -from datetime import datetime, timezone from typing import Literal, Union from pydantic import Field from invokeai.app.models.image import ImageField, ImageType -from ..services.invocation_services import InvocationServices from .baseinvocation import BaseInvocation, InvocationContext, InvocationConfig -from .image import ImageOutput +from .image import ImageOutput, build_image_output class UpscaleInvocation(BaseInvocation): @@ -49,7 +47,14 @@ class UpscaleInvocation(BaseInvocation): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, results[0][0]) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self ) + + context.services.images.save(image_type, image_name, results[0][0], metadata) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=results[0][0] + ) \ No newline at end of file diff --git a/invokeai/app/invocations/util/get_model.py b/invokeai/app/invocations/util/choose_model.py similarity index 53% rename from invokeai/app/invocations/util/get_model.py rename to invokeai/app/invocations/util/choose_model.py index d3484a0b9d..f0f2dc7120 100644 --- a/invokeai/app/invocations/util/get_model.py +++ b/invokeai/app/invocations/util/choose_model.py @@ -1,11 +1,14 @@ -from invokeai.app.invocations.baseinvocation import InvocationContext from invokeai.backend.model_management.model_manager import ModelManager def choose_model(model_manager: ModelManager, model_name: str): """Returns the default model if the `model_name` not a valid model, else returns the selected model.""" if model_manager.valid_model(model_name): - return model_manager.get_model(model_name) + model = model_manager.get_model(model_name) else: - print(f"* Warning: '{model_name}' is not a valid model name. Using default model instead.") - return model_manager.get_model() \ No newline at end of file + model = model_manager.get_model() + print( + f"* Warning: '{model_name}' is not a valid model name. Using default model \'{model['model_name']}\' instead." + ) + + return model diff --git a/invokeai/app/models/image.py b/invokeai/app/models/image.py index 9edb16800d..5ef1ab0d35 100644 --- a/invokeai/app/models/image.py +++ b/invokeai/app/models/image.py @@ -9,6 +9,14 @@ class ImageType(str, Enum): UPLOAD = "uploads" +def is_image_type(obj): + try: + ImageType(obj) + except ValueError: + return False + return True + + class ImageField(BaseModel): """An image field used for passing image objects between invocations""" @@ -18,9 +26,4 @@ class ImageField(BaseModel): image_name: Optional[str] = Field(default=None, description="The name of the image") class Config: - schema_extra = { - "required": [ - "image_type", - "image_name", - ] - } + schema_extra = {"required": ["image_type", "image_name"]} diff --git a/invokeai/app/models/metadata.py b/invokeai/app/models/metadata.py deleted file mode 100644 index 2531168272..0000000000 --- a/invokeai/app/models/metadata.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Optional -from pydantic import BaseModel, Field - -class ImageMetadata(BaseModel): - """An image's metadata""" - - timestamp: float = Field(description="The creation timestamp of the image") - width: int = Field(description="The width of the image in pixels") - height: int = Field(description="The height of the image in pixels") - # TODO: figure out metadata - sd_metadata: Optional[dict] = Field(default={}, description="The image's SD-specific metadata") diff --git a/invokeai/app/services/events.py b/invokeai/app/services/events.py index c8eb7671d0..5f26c42c17 100644 --- a/invokeai/app/services/events.py +++ b/invokeai/app/services/events.py @@ -1,10 +1,9 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) -from typing import Any, Dict, TypedDict +from typing import Any +from invokeai.app.api.models.images import ProgressImage +from invokeai.app.util.misc import get_timestamp -ProgressImage = TypedDict( - "ProgressImage", {"dataURL": str, "width": int, "height": int} -) class EventServiceBase: session_event: str = "session_event" @@ -14,7 +13,8 @@ class EventServiceBase: def dispatch(self, event_name: str, payload: Any) -> None: pass - def __emit_session_event(self, event_name: str, payload: Dict) -> None: + def __emit_session_event(self, event_name: str, payload: dict) -> None: + payload["timestamp"] = get_timestamp() self.dispatch( event_name=EventServiceBase.session_event, payload=dict(event=event_name, data=payload), @@ -25,7 +25,8 @@ class EventServiceBase: def emit_generator_progress( self, graph_execution_state_id: str, - invocation_id: str, + node: dict, + source_node_id: str, progress_image: ProgressImage | None, step: int, total_steps: int, @@ -35,48 +36,60 @@ class EventServiceBase: event_name="generator_progress", payload=dict( graph_execution_state_id=graph_execution_state_id, - invocation_id=invocation_id, - progress_image=progress_image, + node=node, + source_node_id=source_node_id, + progress_image=progress_image.dict() if progress_image is not None else None, step=step, total_steps=total_steps, ), ) def emit_invocation_complete( - self, graph_execution_state_id: str, invocation_id: str, result: Dict + self, + graph_execution_state_id: str, + result: dict, + node: dict, + source_node_id: str, ) -> None: """Emitted when an invocation has completed""" self.__emit_session_event( event_name="invocation_complete", payload=dict( graph_execution_state_id=graph_execution_state_id, - invocation_id=invocation_id, + node=node, + source_node_id=source_node_id, result=result, ), ) def emit_invocation_error( - self, graph_execution_state_id: str, invocation_id: str, error: str + self, + graph_execution_state_id: str, + node: dict, + source_node_id: str, + error: str, ) -> None: """Emitted when an invocation has completed""" self.__emit_session_event( event_name="invocation_error", payload=dict( graph_execution_state_id=graph_execution_state_id, - invocation_id=invocation_id, + node=node, + source_node_id=source_node_id, error=error, ), ) def emit_invocation_started( - self, graph_execution_state_id: str, invocation_id: str + self, graph_execution_state_id: str, node: dict, source_node_id: str ) -> None: """Emitted when an invocation has started""" self.__emit_session_event( event_name="invocation_started", payload=dict( graph_execution_state_id=graph_execution_state_id, - invocation_id=invocation_id, + node=node, + source_node_id=source_node_id, ), ) @@ -84,5 +97,7 @@ class EventServiceBase: """Emitted when a session has completed all invocations""" self.__emit_session_event( event_name="graph_execution_state_complete", - payload=dict(graph_execution_state_id=graph_execution_state_id), + payload=dict( + graph_execution_state_id=graph_execution_state_id, + ), ) diff --git a/invokeai/app/services/image_storage.py b/invokeai/app/services/image_storage.py index 80d72efca8..335425fe66 100644 --- a/invokeai/app/services/image_storage.py +++ b/invokeai/app/services/image_storage.py @@ -1,24 +1,24 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) -import datetime import os from glob import glob from abc import ABC, abstractmethod -from enum import Enum from pathlib import Path from queue import Queue -from typing import Callable, Dict, List +from typing import Dict, List, Tuple from PIL.Image import Image import PIL.Image as PILImage -from pydantic import BaseModel -from invokeai.app.api.models.images import ImageResponse -from invokeai.app.models.image import ImageField, ImageType -from invokeai.app.models.metadata import ImageMetadata +from invokeai.app.api.models.images import ImageResponse, ImageResponseMetadata +from invokeai.app.models.image import ImageType +from invokeai.app.services.metadata import ( + InvokeAIMetadata, + MetadataServiceBase, + build_invokeai_metadata_pnginfo, +) from invokeai.app.services.item_storage import PaginatedResults -from invokeai.app.util.save_thumbnail import save_thumbnail - -from invokeai.backend.image_util import PngWriter +from invokeai.app.util.misc import get_timestamp +from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail class ImageStorageBase(ABC): @@ -26,12 +26,14 @@ class ImageStorageBase(ABC): @abstractmethod def get(self, image_type: ImageType, image_name: str) -> Image: + """Retrieves an image as PIL Image.""" pass @abstractmethod def list( self, image_type: ImageType, page: int = 0, per_page: int = 10 ) -> PaginatedResults[ImageResponse]: + """Gets a paginated list of images.""" pass # TODO: make this a bit more flexible for e.g. cloud storage @@ -39,35 +41,51 @@ class ImageStorageBase(ABC): def get_path( self, image_type: ImageType, image_name: str, is_thumbnail: bool = False ) -> str: + """Gets the path to an image or its thumbnail.""" + pass + + # TODO: make this a bit more flexible for e.g. cloud storage + @abstractmethod + def validate_path(self, path: str) -> bool: + """Validates an image path.""" pass @abstractmethod - def save(self, image_type: ImageType, image_name: str, image: Image) -> None: + def save( + self, + image_type: ImageType, + image_name: str, + image: Image, + metadata: InvokeAIMetadata | None = None, + ) -> Tuple[str, str, int]: + """Saves an image and a 256x256 WEBP thumbnail. Returns a tuple of the image path, thumbnail path, and created timestamp.""" pass @abstractmethod def delete(self, image_type: ImageType, image_name: str) -> None: + """Deletes an image and its thumbnail (if one exists).""" pass def create_name(self, context_id: str, node_id: str) -> str: - return f"{context_id}_{node_id}_{str(int(datetime.datetime.now(datetime.timezone.utc).timestamp()))}.png" + """Creates a unique contextual image filename.""" + return f"{context_id}_{node_id}_{str(get_timestamp())}.png" class DiskImageStorage(ImageStorageBase): """Stores images on disk""" __output_folder: str - __pngWriter: PngWriter __cache_ids: Queue # TODO: this is an incredibly naive cache __cache: Dict[str, Image] __max_cache_size: int + __metadata_service: MetadataServiceBase - def __init__(self, output_folder: str): + def __init__(self, output_folder: str, metadata_service: MetadataServiceBase): self.__output_folder = output_folder - self.__pngWriter = PngWriter(output_folder) self.__cache = dict() self.__cache_ids = Queue() self.__max_cache_size = 10 # TODO: get this from config + self.__metadata_service = metadata_service Path(output_folder).mkdir(parents=True, exist_ok=True) @@ -100,6 +118,9 @@ class DiskImageStorage(ImageStorageBase): for path in page_of_image_paths: filename = os.path.basename(path) img = PILImage.open(path) + + invokeai_metadata = self.__metadata_service.get_metadata(img) + page_of_images.append( ImageResponse( image_type=image_type.value, @@ -107,11 +128,12 @@ class DiskImageStorage(ImageStorageBase): # TODO: DiskImageStorage should not be building URLs...? image_url=f"api/v1/images/{image_type.value}/{filename}", thumbnail_url=f"api/v1/images/{image_type.value}/thumbnails/{os.path.splitext(filename)[0]}.webp", - # TODO: Creation of this object should happen elsewhere, just making it fit here so it works - metadata=ImageMetadata( - timestamp=os.path.getctime(path), + # TODO: Creation of this object should happen elsewhere (?), just making it fit here so it works + metadata=ImageResponseMetadata( + created=int(os.path.getctime(path)), width=img.width, height=img.height, + invokeai=invokeai_metadata, ), ) ) @@ -142,26 +164,50 @@ class DiskImageStorage(ImageStorageBase): def get_path( self, image_type: ImageType, image_name: str, is_thumbnail: bool = False ) -> str: + # strip out any relative path shenanigans + basename = os.path.basename(image_name) + if is_thumbnail: path = os.path.join( - self.__output_folder, image_type, "thumbnails", image_name + self.__output_folder, image_type, "thumbnails", basename ) else: - path = os.path.join(self.__output_folder, image_type, image_name) + path = os.path.join(self.__output_folder, image_type, basename) + return path - def save(self, image_type: ImageType, image_name: str, image: Image) -> None: - image_subpath = os.path.join(image_type, image_name) - self.__pngWriter.save_image_and_prompt_to_png( - image, "", image_subpath, None - ) # TODO: just pass full path to png writer - save_thumbnail( - image=image, - filename=image_name, - path=os.path.join(self.__output_folder, image_type, "thumbnails"), - ) + def validate_path(self, path: str) -> bool: + try: + os.stat(path) + return True + except Exception: + return False + + def save( + self, + image_type: ImageType, + image_name: str, + image: Image, + metadata: InvokeAIMetadata | None = None, + ) -> Tuple[str, str, int]: image_path = self.get_path(image_type, image_name) + + # TODO: Reading the image and then saving it strips the metadata... + if metadata: + pnginfo = build_invokeai_metadata_pnginfo(metadata=metadata) + image.save(image_path, "PNG", pnginfo=pnginfo) + else: + image.save(image_path) # this saved image has an empty info + + thumbnail_name = get_thumbnail_name(image_name) + thumbnail_path = self.get_path(image_type, thumbnail_name, is_thumbnail=True) + thumbnail_image = make_thumbnail(image) + thumbnail_image.save(thumbnail_path) + self.__set_cache(image_path, image) + self.__set_cache(thumbnail_path, thumbnail_image) + + return (image_path, thumbnail_path, int(os.path.getctime(image_path))) def delete(self, image_type: ImageType, image_name: str) -> None: image_path = self.get_path(image_type, image_name) diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index c3c6bbce7e..1ff42f063d 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -1,4 +1,5 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) +from invokeai.app.services.metadata import MetadataServiceBase from invokeai.backend import ModelManager from .events import EventServiceBase @@ -14,6 +15,7 @@ class InvocationServices: events: EventServiceBase latents: LatentsStorageBase images: ImageStorageBase + metadata: MetadataServiceBase queue: InvocationQueueABC model_manager: ModelManager restoration: RestorationServices @@ -29,6 +31,7 @@ class InvocationServices: events: EventServiceBase, latents: LatentsStorageBase, images: ImageStorageBase, + metadata: MetadataServiceBase, queue: InvocationQueueABC, graph_library: ItemStorageABC["LibraryGraph"], graph_execution_manager: ItemStorageABC["GraphExecutionState"], @@ -39,6 +42,7 @@ class InvocationServices: self.events = events self.latents = latents self.images = images + self.metadata = metadata self.queue = queue self.graph_library = graph_library self.graph_execution_manager = graph_execution_manager diff --git a/invokeai/app/services/metadata.py b/invokeai/app/services/metadata.py new file mode 100644 index 0000000000..2c8bb0d26b --- /dev/null +++ b/invokeai/app/services/metadata.py @@ -0,0 +1,96 @@ +import json +from abc import ABC, abstractmethod +from typing import Any, Dict, Optional, TypedDict +from PIL import Image, PngImagePlugin +from pydantic import BaseModel + +from invokeai.app.models.image import ImageType, is_image_type + + +class MetadataImageField(TypedDict): + """Pydantic-less ImageField, used for metadata parsing.""" + + image_type: ImageType + image_name: str + + +class MetadataLatentsField(TypedDict): + """Pydantic-less LatentsField, used for metadata parsing.""" + + latents_name: str + + +# TODO: This is a placeholder for `InvocationsUnion` pending resolution of circular imports +NodeMetadata = Dict[ + str, str | int | float | bool | MetadataImageField | MetadataLatentsField +] + + +class InvokeAIMetadata(TypedDict, total=False): + """InvokeAI-specific metadata format.""" + + session_id: Optional[str] + node: Optional[NodeMetadata] + + +def build_invokeai_metadata_pnginfo( + metadata: InvokeAIMetadata | None, +) -> PngImagePlugin.PngInfo: + """Builds a PngInfo object with key `"invokeai"` and value `metadata`""" + pnginfo = PngImagePlugin.PngInfo() + + if metadata is not None: + pnginfo.add_text("invokeai", json.dumps(metadata)) + + return pnginfo + + +class MetadataServiceBase(ABC): + @abstractmethod + def get_metadata(self, image: Image.Image) -> InvokeAIMetadata | None: + """Gets the InvokeAI metadata from a PIL Image, skipping invalid values""" + pass + + @abstractmethod + def build_metadata( + self, session_id: str, node: BaseModel + ) -> InvokeAIMetadata | None: + """Builds an InvokeAIMetadata object""" + pass + + +class PngMetadataService(MetadataServiceBase): + """Handles loading and building metadata for images.""" + + # TODO: Use `InvocationsUnion` to **validate** metadata as representing a fully-functioning node + def _load_metadata(self, image: Image.Image) -> dict | None: + """Loads a specific info entry from a PIL Image.""" + + try: + info = image.info.get("invokeai") + + if type(info) is not str: + return None + + loaded_metadata = json.loads(info) + + if type(loaded_metadata) is not dict: + return None + + if len(loaded_metadata.items()) == 0: + return None + + return loaded_metadata + except: + return None + + def get_metadata(self, image: Image.Image) -> dict | None: + """Retrieves an image's metadata as a dict""" + loaded_metadata = self._load_metadata(image) + + return loaded_metadata + + def build_metadata(self, session_id: str, node: BaseModel) -> InvokeAIMetadata: + metadata = InvokeAIMetadata(session_id=session_id, node=node.dict()) + + return metadata diff --git a/invokeai/app/services/processor.py b/invokeai/app/services/processor.py index 0125d7eb62..c622906750 100644 --- a/invokeai/app/services/processor.py +++ b/invokeai/app/services/processor.py @@ -43,10 +43,14 @@ class DefaultInvocationProcessor(InvocationProcessorABC): queue_item.invocation_id ) + # get the source node id to provide to clients (the prepared node id is not as useful) + source_node_id = graph_execution_state.prepared_source_mapping[invocation.id] + # Send starting event self.__invoker.services.events.emit_invocation_started( graph_execution_state_id=graph_execution_state.id, - invocation_id=invocation.id, + node=invocation.dict(), + source_node_id=source_node_id ) # Invoke @@ -75,7 +79,8 @@ class DefaultInvocationProcessor(InvocationProcessorABC): # Send complete event self.__invoker.services.events.emit_invocation_complete( graph_execution_state_id=graph_execution_state.id, - invocation_id=invocation.id, + node=invocation.dict(), + source_node_id=source_node_id, result=outputs.dict(), ) @@ -99,7 +104,8 @@ class DefaultInvocationProcessor(InvocationProcessorABC): # Send error event self.__invoker.services.events.emit_invocation_error( graph_execution_state_id=graph_execution_state.id, - invocation_id=invocation.id, + node=invocation.dict(), + source_node_id=source_node_id, error=error, ) diff --git a/invokeai/app/services/sqlite.py b/invokeai/app/services/sqlite.py index e06ca8c1ac..fd089014bb 100644 --- a/invokeai/app/services/sqlite.py +++ b/invokeai/app/services/sqlite.py @@ -35,7 +35,8 @@ class SqliteItemStorage(ItemStorageABC, Generic[T]): self._create_table() def _create_table(self): - with self._lock: + try: + self._lock.acquire() self._cursor.execute( f"""CREATE TABLE IF NOT EXISTS {self._table_name} ( item TEXT, @@ -44,27 +45,34 @@ class SqliteItemStorage(ItemStorageABC, Generic[T]): self._cursor.execute( f"""CREATE UNIQUE INDEX IF NOT EXISTS {self._table_name}_id ON {self._table_name}(id);""" ) - self._conn.commit() + finally: + self._lock.release() def _parse_item(self, item: str) -> T: item_type = get_args(self.__orig_class__)[0] return parse_raw_as(item_type, item) def set(self, item: T): - with self._lock: + try: + self._lock.acquire() self._cursor.execute( f"""INSERT OR REPLACE INTO {self._table_name} (item) VALUES (?);""", (item.json(),), ) self._conn.commit() + finally: + self._lock.release() self._on_changed(item) def get(self, id: str) -> Union[T, None]: - with self._lock: + try: + self._lock.acquire() self._cursor.execute( f"""SELECT item FROM {self._table_name} WHERE id = ?;""", (str(id),) ) result = self._cursor.fetchone() + finally: + self._lock.release() if not result: return None @@ -72,15 +80,19 @@ class SqliteItemStorage(ItemStorageABC, Generic[T]): return self._parse_item(result[0]) def delete(self, id: str): - with self._lock: + try: + self._lock.acquire() self._cursor.execute( f"""DELETE FROM {self._table_name} WHERE id = ?;""", (str(id),) ) self._conn.commit() + finally: + self._lock.release() self._on_deleted(id) def list(self, page: int = 0, per_page: int = 10) -> PaginatedResults[T]: - with self._lock: + try: + self._lock.acquire() self._cursor.execute( f"""SELECT item FROM {self._table_name} LIMIT ? OFFSET ?;""", (per_page, page * per_page), @@ -91,6 +103,8 @@ class SqliteItemStorage(ItemStorageABC, Generic[T]): self._cursor.execute(f"""SELECT count(*) FROM {self._table_name};""") count = self._cursor.fetchone()[0] + finally: + self._lock.release() pageCount = int(count / per_page) + 1 @@ -101,7 +115,8 @@ class SqliteItemStorage(ItemStorageABC, Generic[T]): def search( self, query: str, page: int = 0, per_page: int = 10 ) -> PaginatedResults[T]: - with self._lock: + try: + self._lock.acquire() self._cursor.execute( f"""SELECT item FROM {self._table_name} WHERE item LIKE ? LIMIT ? OFFSET ?;""", (f"%{query}%", per_page, page * per_page), @@ -115,6 +130,8 @@ class SqliteItemStorage(ItemStorageABC, Generic[T]): (f"%{query}%",), ) count = self._cursor.fetchone()[0] + finally: + self._lock.release() pageCount = int(count / per_page) + 1 diff --git a/invokeai/app/util/misc.py b/invokeai/app/util/misc.py new file mode 100644 index 0000000000..b2b57bd086 --- /dev/null +++ b/invokeai/app/util/misc.py @@ -0,0 +1,5 @@ +import datetime + + +def get_timestamp(): + return int(datetime.datetime.now(datetime.timezone.utc).timestamp()) diff --git a/invokeai/app/util/save_thumbnail.py b/invokeai/app/util/save_thumbnail.py deleted file mode 100644 index 86fdbe7ef6..0000000000 --- a/invokeai/app/util/save_thumbnail.py +++ /dev/null @@ -1,25 +0,0 @@ -import os -from PIL import Image - - -def save_thumbnail( - image: Image.Image, - filename: str, - path: str, - size: int = 256, -) -> str: - """ - Saves a thumbnail of an image, returning its path. - """ - base_filename = os.path.splitext(filename)[0] - thumbnail_path = os.path.join(path, base_filename + ".webp") - - if os.path.exists(thumbnail_path): - return thumbnail_path - - image_copy = image.copy() - image_copy.thumbnail(size=(size, size)) - - image_copy.save(thumbnail_path, "WEBP") - - return thumbnail_path diff --git a/invokeai/app/util/step_callback.py b/invokeai/app/util/step_callback.py index 466f78ddb0..963e770406 100644 --- a/invokeai/app/util/step_callback.py +++ b/invokeai/app/util/step_callback.py @@ -1,16 +1,41 @@ -import torch +from invokeai.app.api.models.images import ProgressImage +from invokeai.app.models.exceptions import CanceledException from ..invocations.baseinvocation import InvocationContext from ...backend.util.util import image_to_dataURL from ...backend.generator.base import Generator from ...backend.stable_diffusion import PipelineIntermediateState -def fast_latents_step_callback( - sample: torch.Tensor, - step: int, - steps: int, - id: str, + +def stable_diffusion_step_callback( context: InvocationContext, + intermediate_state: PipelineIntermediateState, + node: dict, + source_node_id: str, ): + if context.services.queue.is_canceled(context.graph_execution_state_id): + raise CanceledException + + # Some schedulers report not only the noisy latents at the current timestep, + # but also their estimate so far of what the de-noised latents will be. Use + # that estimate if it is available. + if intermediate_state.predicted_original is not None: + sample = intermediate_state.predicted_original + else: + sample = intermediate_state.latents + + # TODO: This does not seem to be needed any more? + # # txt2img provides a Tensor in the step_callback + # # img2img provides a PipelineIntermediateState + # if isinstance(sample, PipelineIntermediateState): + # # this was an img2img + # print('img2img') + # latents = sample.latents + # step = sample.step + # else: + # print('txt2img') + # latents = sample + # step = intermediate_state.step + # TODO: only output a preview image when requested image = Generator.sample_to_lowres_estimated_image(sample) @@ -21,23 +46,10 @@ def fast_latents_step_callback( dataURL = image_to_dataURL(image, image_format="JPEG") context.services.events.emit_generator_progress( - context.graph_execution_state_id, - id, - {"width": width, "height": height, "dataURL": dataURL}, - step, - steps, + graph_execution_state_id=context.graph_execution_state_id, + node=node, + source_node_id=source_node_id, + progress_image=ProgressImage(width=width, height=height, dataURL=dataURL), + step=intermediate_state.step, + total_steps=node["steps"], ) - - -def diffusers_step_callback_adapter(*cb_args, **kwargs): - """ - txt2img gives us a Tensor in the step_callbak, while img2img gives us a PipelineIntermediateState. - This adapter grabs the needed data and passes it along to the callback function. - """ - if isinstance(cb_args[0], PipelineIntermediateState): - progress_state: PipelineIntermediateState = cb_args[0] - return fast_latents_step_callback( - progress_state.latents, progress_state.step, **kwargs - ) - else: - return fast_latents_step_callback(*cb_args, **kwargs) diff --git a/invokeai/app/util/thumbnails.py b/invokeai/app/util/thumbnails.py new file mode 100644 index 0000000000..42a6fe9962 --- /dev/null +++ b/invokeai/app/util/thumbnails.py @@ -0,0 +1,15 @@ +import os +from PIL import Image + + +def get_thumbnail_name(image_name: str) -> str: + """Formats given an image name, returns the appropriate thumbnail image name""" + thumbnail_name = os.path.splitext(image_name)[0] + ".webp" + return thumbnail_name + + +def make_thumbnail(image: Image.Image, size: int = 256) -> Image.Image: + """Makes a thumbnail from a PIL Image""" + thumbnail = image.copy() + thumbnail.thumbnail(size=(size, size)) + return thumbnail diff --git a/invokeai/frontend/web/.eslintignore b/invokeai/frontend/web/.eslintignore index 99d8bab48c..b351fc6a96 100644 --- a/invokeai/frontend/web/.eslintignore +++ b/invokeai/frontend/web/.eslintignore @@ -6,3 +6,5 @@ stats.html index.html .yarn/ *.scss +src/services/api/ +src/services/fixtures/* diff --git a/invokeai/frontend/web/.prettierignore b/invokeai/frontend/web/.prettierignore index 905f177fde..b351fc6a96 100644 --- a/invokeai/frontend/web/.prettierignore +++ b/invokeai/frontend/web/.prettierignore @@ -3,4 +3,8 @@ dist/ node_modules/ patches/ stats.html +index.html .yarn/ +*.scss +src/services/api/ +src/services/fixtures/* diff --git a/invokeai/frontend/web/docs/API_CLIENT.md b/invokeai/frontend/web/docs/API_CLIENT.md new file mode 100644 index 0000000000..51f3a6510c --- /dev/null +++ b/invokeai/frontend/web/docs/API_CLIENT.md @@ -0,0 +1,87 @@ +# Generated axios API client + +- [Generated axios API client](#generated-axios-api-client) + - [Generation](#generation) + - [Generate the API client from the nodes web server](#generate-the-api-client-from-the-nodes-web-server) + - [Generate the API client from JSON](#generate-the-api-client-from-json) + - [Getting the JSON from the nodes web server](#getting-the-json-from-the-nodes-web-server) + - [Getting the JSON with a python script](#getting-the-json-with-a-python-script) + - [Generate the API client](#generate-the-api-client) + - [The generated client](#the-generated-client) + - [API client customisation](#api-client-customisation) + +This API client is generated by an [openapi code generator](https://github.com/ferdikoomen/openapi-typescript-codegen). + +All files in `invokeai/frontend/web/src/services/api/` are made by the generator. + +## Generation + +The axios client may be generated by from the OpenAPI schema from the nodes web server, or from JSON. + +### Generate the API client from the nodes web server + +We need to start the nodes web server, which serves the OpenAPI schema to the generator. + +1. Start the nodes web server. + +```bash +# from the repo root +python scripts/invoke-new.py --web +``` + +2. Generate the API client. + +```bash +# from invokeai/frontend/web/ +yarn api:web +``` + +### Generate the API client from JSON + +The JSON can be acquired from the nodes web server, or with a python script. + +#### Getting the JSON from the nodes web server + +Start the nodes web server as described above, then download the file. + +```bash +# from invokeai/frontend/web/ +curl http://localhost:9090/openapi.json -o openapi.json +``` + +#### Getting the JSON with a python script + +Run this python script from the repo root, so it can access the nodes server modules. + +The script will output `openapi.json` in the repo root. Then we need to move it to `invokeai/frontend/web/`. + +```bash +# from the repo root +python invokeai/app/util/generate_openapi_json.py +mv invokeai/app/util/openapi.json invokeai/frontend/web/services/fixtures/ +``` + +#### Generate the API client + +Now we can generate the API client from the JSON. + +```bash +# from invokeai/frontend/web/ +yarn api:file +``` + +## The generated client + +The client will be written to `invokeai/frontend/web/services/api/`: + +- `axios` client +- TS types +- An easily parseable schema, which we can use to generate UI + +## API client customisation + +The generator has a default `request.ts` file that implements a base `axios` client. The generated client uses this base client. + +One shortcoming of this is base client is it does not provide response headers unless the response body is empty. To fix this, we provide our own lightly-patched `request.ts`. + +To access the headers, call `getHeaders(response)` on any response from the generated api client. This function is exported from `invokeai/frontend/web/src/services/util/getHeaders.ts`. diff --git a/invokeai/frontend/web/docs/EVENTS.md b/invokeai/frontend/web/docs/EVENTS.md new file mode 100644 index 0000000000..24f2497a20 --- /dev/null +++ b/invokeai/frontend/web/docs/EVENTS.md @@ -0,0 +1,21 @@ +# Events + +Events via `socket.io` + +## `actions.ts` + +Redux actions for all socket events. Payloads all include a timestamp, and optionally some other data. + +Any reducer (or middleware) can respond to the actions. + +## `middleware.ts` + +Redux middleware for events. + +Handles dispatching the event actions. Only put logic here if it can't really go anywhere else. + +For example, on connect we want to load images to the gallery if it's not populated. This requires dispatching a thunk, so we need to directly dispatch this in the middleware. + +## `types.ts` + +Hand-written types for the socket events. Cannot generate these from the server, but fortunately they are few and simple. diff --git a/invokeai/frontend/web/docs/NODE_EDITOR.md b/invokeai/frontend/web/docs/NODE_EDITOR.md new file mode 100644 index 0000000000..0b4fbcbc81 --- /dev/null +++ b/invokeai/frontend/web/docs/NODE_EDITOR.md @@ -0,0 +1,17 @@ +# Node Editor Design + +WIP + +nodes + +everything in `src/features/nodes/` + +have a look at `state.nodes.invocation` + +- on socket connect, if no schema saved, fetch `localhost:9090/openapi.json`, save JSON to `state.nodes.schema` +- on fulfilled schema fetch, `parseSchema()` the schema. this outputs a `Record` which is saved to `state.nodes.invocations` - `Invocation` is like a template for the node +- when you add a node, the the `Invocation` template is passed to `InvocationComponent.tsx` to build the UI component for that node +- inputs/outputs have field types - and each field type gets an `FieldComponent` which includes a dispatcher to write state changes to redux `nodesSlice` +- `reactflow` sends changes to nodes/edges to redux +- to invoke, `buildNodesGraph()` state, then send this +- changed onClick Invoke button actions to build the schema, then when schema builds it dispatches the actual network request to create the session - see `session.ts` diff --git a/invokeai/frontend/web/docs/PACKAGE_SCRIPTS.md b/invokeai/frontend/web/docs/PACKAGE_SCRIPTS.md new file mode 100644 index 0000000000..90d85bb540 --- /dev/null +++ b/invokeai/frontend/web/docs/PACKAGE_SCRIPTS.md @@ -0,0 +1,29 @@ +# Package Scripts + +WIP walkthrough of `package.json` scripts. + +## `theme` & `theme:watch` + +These run the Chakra CLI to generate types for the theme, or watch for code change and re-generate the types. + +The CLI essentially monkeypatches Chakra's files in `node_modules`. + +## `postinstall` + +The `postinstall` script patches a few packages and runs the Chakra CLI to generate types for the theme. + +### Patch `@chakra-ui/cli` + +See: + +### Patch `redux-persist` + +We want to persist the canvas state to `localStorage` but many canvas operations change data very quickly, so we need to debounce the writes to `localStorage`. + +`redux-persist` is unfortunately unmaintained. The repo's current code is nonfunctional, but the last release's code depends on a package that was removed from `npm` for being malware, so we cannot just fork it. + +So, we have to patch it directly. Perhaps a better way would be to write a debounced storage adapter, but I couldn't figure out how to do that. + +### Patch `redux-deep-persist` + +This package makes blacklisting and whitelisting persist configs very simple, but we have to patch it to match `redux-persist` for the types to work. diff --git a/invokeai/frontend/web/README.md b/invokeai/frontend/web/docs/README.md similarity index 88% rename from invokeai/frontend/web/README.md rename to invokeai/frontend/web/docs/README.md index ef8c503550..787725cdda 100644 --- a/invokeai/frontend/web/README.md +++ b/invokeai/frontend/web/docs/README.md @@ -1,10 +1,16 @@ # InvokeAI Web UI +- [InvokeAI Web UI](#invokeai-web-ui) + - [Stack](#stack) + - [Contributing](#contributing) + - [Dev Environment](#dev-environment) + - [Production builds](#production-builds) + The UI is a fairly straightforward Typescript React app. The only really fancy stuff is the Unified Canvas. Code in `invokeai/frontend/web/` if you want to have a look. -## Details +## Stack State management is Redux via [Redux Toolkit](https://github.com/reduxjs/redux-toolkit). Communication with server is a mix of HTTP and [socket.io](https://github.com/socketio/socket.io-client) (with a custom redux middleware to help). @@ -32,7 +38,7 @@ Start everything in dev mode: 1. Start the dev server: `yarn dev` 2. Start the InvokeAI UI per usual: `invokeai --web` -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. ### Production builds diff --git a/invokeai/frontend/web/index.d.ts b/invokeai/frontend/web/index.d.ts index a3ab75d17c..af21f29231 100644 --- a/invokeai/frontend/web/index.d.ts +++ b/invokeai/frontend/web/index.d.ts @@ -1,6 +1,7 @@ import React, { PropsWithChildren } from 'react'; import { IAIPopoverProps } from '../web/src/common/components/IAIPopover'; import { IAIIconButtonProps } from '../web/src/common/components/IAIIconButton'; +import { InvokeTabName } from 'features/ui/store/tabMap'; export {}; @@ -64,9 +65,25 @@ declare module '@invoke-ai/invoke-ai-ui' { declare class SettingsModal extends React.Component { public constructor(props: SettingsModalProps); } + + declare class StatusIndicator extends React.Component { + public constructor(props: StatusIndicatorProps); + } + + declare class ModelSelect extends React.Component { + public constructor(props: ModelSelectProps); + } } -declare function Invoke(props: PropsWithChildren): JSX.Element; +interface InvokeProps extends PropsWithChildren { + apiUrl?: string; + disabledPanels?: string[]; + disabledTabs?: InvokeTabName[]; + token?: string; + shouldTransformUrls?: boolean; +} + +declare function Invoke(props: InvokeProps): JSX.Element; export { ThemeChanger, @@ -74,5 +91,7 @@ export { IAIPopover, IAIIconButton, SettingsModal, + StatusIndicator, + ModelSelect, }; export = Invoke; diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index c47948746d..cecba05d6a 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -5,7 +5,10 @@ "scripts": { "prepare": "cd ../../../ && husky install invokeai/frontend/web/.husky", "dev": "concurrently \"vite dev\" \"yarn run theme:watch\"", + "dev:nodes": "concurrently \"vite dev --mode nodes\" \"yarn run theme:watch\"", "build": "yarn run lint && vite build", + "api:web": "openapi -i http://localhost:9090/openapi.json -o src/services/api --client axios --useOptions --useUnionTypes --exportSchemas true --indent 2 --request src/services/fixtures/request.ts", + "api:file": "openapi -i src/services/fixtures/openapi.json -o src/services/api --client axios --useOptions --useUnionTypes --exportSchemas true --indent 2 --request src/services/fixtures/request.ts", "preview": "vite preview", "lint:madge": "madge --circular src/main.tsx", "lint:eslint": "eslint --max-warnings=0 .", @@ -41,9 +44,11 @@ "@chakra-ui/react": "^2.5.1", "@chakra-ui/styled-system": "^2.6.1", "@chakra-ui/theme-tools": "^2.0.16", + "@dagrejs/graphlib": "^2.1.12", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", - "@reduxjs/toolkit": "^1.9.2", + "@fontsource/inter": "^4.5.15", + "@reduxjs/toolkit": "^1.9.3", "chakra-ui-contextmenu": "^1.0.5", "dateformat": "^5.0.3", "formik": "^2.2.9", @@ -67,15 +72,17 @@ "react-redux": "^8.0.5", "react-transition-group": "^4.4.5", "react-zoom-pan-pinch": "^2.6.1", + "reactflow": "^11.7.0", "redux-deep-persist": "^1.0.7", + "redux-dynamic-middlewares": "^2.2.0", "redux-persist": "^6.0.0", "socket.io-client": "^4.6.0", "use-image": "^1.1.0", "uuid": "^9.0.0" }, "devDependencies": { - "@fontsource/inter": "^4.5.15", "@types/dateformat": "^5.0.0", + "@types/lodash": "^4.14.194", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", "@types/react-transition-group": "^4.4.5", @@ -83,6 +90,7 @@ "@typescript-eslint/eslint-plugin": "^5.52.0", "@typescript-eslint/parser": "^5.52.0", "@vitejs/plugin-react-swc": "^3.2.0", + "axios": "^1.3.4", "babel-plugin-transform-imports": "^2.0.0", "concurrently": "^7.6.0", "eslint": "^8.34.0", @@ -90,13 +98,17 @@ "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", + "form-data": "^4.0.0", "husky": "^8.0.3", "lint-staged": "^13.1.2", "madge": "^6.0.0", + "openapi-types": "^12.1.0", + "openapi-typescript-codegen": "^0.23.0", "postinstall-postinstall": "^2.1.0", "prettier": "^2.8.4", "rollup-plugin-visualizer": "^5.9.0", "terser": "^5.16.4", + "typescript": "4.9.5", "vite": "^4.1.2", "vite-plugin-eslint": "^1.8.1", "vite-tsconfig-paths": "^4.0.5", diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index a99f54d741..0bb6b49f8a 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -52,6 +52,7 @@ "txt2img": "Text To Image", "img2img": "Image To Image", "unifiedCanvas": "Unified Canvas", + "linear": "Linear", "nodes": "Nodes", "postprocessing": "Post Processing", "nodesDesc": "A node based system for the generation of images is under development currently. Stay tuned for updates about this amazing feature.", @@ -524,6 +525,10 @@ "resetComplete": "Web UI has been reset. Refresh the page to reload." }, "toast": { + "serverError": "Server Error", + "disconnected": "Disconnected from Server", + "connected": "Connected to Server", + "canceled": "Processing Canceled", "tempFoldersEmptied": "Temp Folder Emptied", "uploadFailed": "Upload failed", "uploadFailedMultipleImagesDesc": "Multiple images pasted, may only upload one image at a time", diff --git a/invokeai/frontend/web/src/app/App.tsx b/invokeai/frontend/web/src/app/App.tsx index 40c15b38c0..1e86c5f222 100644 --- a/invokeai/frontend/web/src/app/App.tsx +++ b/invokeai/frontend/web/src/app/App.tsx @@ -13,16 +13,42 @@ import { Box, Flex, Grid, Portal, useColorMode } from '@chakra-ui/react'; import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants'; import ImageGalleryPanel from 'features/gallery/components/ImageGalleryPanel'; import Lightbox from 'features/lightbox/components/Lightbox'; -import { useAppSelector } from './storeHooks'; +import { useAppDispatch, useAppSelector } from './storeHooks'; import { PropsWithChildren, useEffect } from 'react'; +import { setDisabledPanels, setDisabledTabs } from 'features/ui/store/uiSlice'; +import { InvokeTabName } from 'features/ui/store/tabMap'; +import { shouldTransformUrlsChanged } from 'features/system/store/systemSlice'; keepGUIAlive(); -const App = (props: PropsWithChildren) => { +interface Props extends PropsWithChildren { + options: { + disabledPanels: string[]; + disabledTabs: InvokeTabName[]; + shouldTransformUrls?: boolean; + }; +} + +const App = (props: Props) => { useToastWatcher(); const currentTheme = useAppSelector((state) => state.ui.currentTheme); const { setColorMode } = useColorMode(); + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(setDisabledPanels(props.options.disabledPanels)); + }, [dispatch, props.options.disabledPanels]); + + useEffect(() => { + dispatch(setDisabledTabs(props.options.disabledTabs)); + }, [dispatch, props.options.disabledTabs]); + + useEffect(() => { + dispatch( + shouldTransformUrlsChanged(Boolean(props.options.shouldTransformUrls)) + ); + }, [dispatch, props.options.shouldTransformUrls]); useEffect(() => { setColorMode(['light'].includes(currentTheme) ? 'light' : 'dark'); diff --git a/invokeai/frontend/web/src/app/invokeai.d.ts b/invokeai/frontend/web/src/app/invokeai.d.ts index e01e414d03..f98ca73675 100644 --- a/invokeai/frontend/web/src/app/invokeai.d.ts +++ b/invokeai/frontend/web/src/app/invokeai.d.ts @@ -14,6 +14,8 @@ import { InvokeTabName } from 'features/ui/store/tabMap'; import { IRect } from 'konva/lib/types'; +import { ImageMetadata, ImageType } from 'services/api'; +import { AnyInvocation } from 'services/events/types'; /** * TODO: @@ -113,7 +115,7 @@ export declare type Metadata = SystemGenerationMetadata & { }; // An Image has a UUID, url, modified timestamp, width, height and maybe metadata -export declare type Image = { +export declare type _Image = { uuid: string; url: string; thumbnail: string; @@ -124,11 +126,23 @@ export declare type Image = { category: GalleryCategory; isBase64?: boolean; dreamPrompt?: 'string'; + name?: string; +}; + +/** + * ResultImage + */ +export declare type Image = { + name: string; + type: ImageType; + url: string; + thumbnail: string; + metadata: ImageMetadata; }; // GalleryImages is an array of Image. export declare type GalleryImages = { - images: Array; + images: Array<_Image>; }; /** @@ -275,7 +289,7 @@ export declare type SystemStatusResponse = SystemStatus; export declare type SystemConfigResponse = SystemConfig; -export declare type ImageResultResponse = Omit & { +export declare type ImageResultResponse = Omit<_Image, 'uuid'> & { boundingBox?: IRect; generationMode: InvokeTabName; }; @@ -296,7 +310,7 @@ export declare type ErrorResponse = { }; export declare type GalleryImagesResponse = { - images: Array>; + images: Array>; areMoreImagesAvailable: boolean; category: GalleryCategory; }; diff --git a/invokeai/frontend/web/src/app/selectors/readinessSelector.ts b/invokeai/frontend/web/src/app/selectors/readinessSelector.ts index cc85c3ca6c..82672756c8 100644 --- a/invokeai/frontend/web/src/app/selectors/readinessSelector.ts +++ b/invokeai/frontend/web/src/app/selectors/readinessSelector.ts @@ -20,6 +20,7 @@ export const readinessSelector = createSelector( seedWeights, initialImage, seed, + isImageToImageEnabled, } = generation; const { isProcessing, isConnected } = system; @@ -33,7 +34,7 @@ export const readinessSelector = createSelector( reasonsWhyNotReady.push('Missing prompt'); } - if (activeTabName === 'img2img' && !initialImage) { + if (isImageToImageEnabled && !initialImage) { isReady = false; reasonsWhyNotReady.push('No initial image selected'); } diff --git a/invokeai/frontend/web/src/app/socketio/actions.ts b/invokeai/frontend/web/src/app/socketio/actions.ts index 57758d1914..4907595c75 100644 --- a/invokeai/frontend/web/src/app/socketio/actions.ts +++ b/invokeai/frontend/web/src/app/socketio/actions.ts @@ -13,9 +13,13 @@ import { InvokeTabName } from 'features/ui/store/tabMap'; export const generateImage = createAction( 'socketio/generateImage' ); -export const runESRGAN = createAction('socketio/runESRGAN'); -export const runFacetool = createAction('socketio/runFacetool'); -export const deleteImage = createAction('socketio/deleteImage'); +export const runESRGAN = createAction('socketio/runESRGAN'); +export const runFacetool = createAction( + 'socketio/runFacetool' +); +export const deleteImage = createAction( + 'socketio/deleteImage' +); export const requestImages = createAction( 'socketio/requestImages' ); diff --git a/invokeai/frontend/web/src/app/socketio/emitters.ts b/invokeai/frontend/web/src/app/socketio/emitters.ts index 2aa1e03552..cd25319aee 100644 --- a/invokeai/frontend/web/src/app/socketio/emitters.ts +++ b/invokeai/frontend/web/src/app/socketio/emitters.ts @@ -91,7 +91,7 @@ const makeSocketIOEmitters = ( }) ); }, - emitRunESRGAN: (imageToProcess: InvokeAI.Image) => { + emitRunESRGAN: (imageToProcess: InvokeAI._Image) => { dispatch(setIsProcessing(true)); const { @@ -119,7 +119,7 @@ const makeSocketIOEmitters = ( }) ); }, - emitRunFacetool: (imageToProcess: InvokeAI.Image) => { + emitRunFacetool: (imageToProcess: InvokeAI._Image) => { dispatch(setIsProcessing(true)); const { @@ -150,7 +150,7 @@ const makeSocketIOEmitters = ( }) ); }, - emitDeleteImage: (imageToDelete: InvokeAI.Image) => { + emitDeleteImage: (imageToDelete: InvokeAI._Image) => { const { url, uuid, category, thumbnail } = imageToDelete; dispatch(removeImage(imageToDelete)); socketio.emit('deleteImage', url, thumbnail, uuid, category); diff --git a/invokeai/frontend/web/src/app/socketio/listeners.ts b/invokeai/frontend/web/src/app/socketio/listeners.ts index 08de671260..dc6c35d862 100644 --- a/invokeai/frontend/web/src/app/socketio/listeners.ts +++ b/invokeai/frontend/web/src/app/socketio/listeners.ts @@ -34,8 +34,9 @@ import type { RootState } from 'app/store'; import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; import { clearInitialImage, + initialImageSelected, setInfillMethod, - setInitialImage, + // setInitialImage, setMaskPath, } from 'features/parameters/store/generationSlice'; import { tabMap } from 'features/ui/store/tabMap'; @@ -142,15 +143,17 @@ const makeSocketIOListeners = ( } } - if (shouldLoopback) { - const activeTabName = tabMap[activeTab]; - switch (activeTabName) { - case 'img2img': { - dispatch(setInitialImage(newImage)); - break; - } - } - } + // TODO: fix + // if (shouldLoopback) { + // const activeTabName = tabMap[activeTab]; + // switch (activeTabName) { + // case 'img2img': { + // dispatch(initialImageSelected(newImage.uuid)); + // // dispatch(setInitialImage(newImage)); + // break; + // } + // } + // } dispatch(clearIntermediateImage()); @@ -262,7 +265,7 @@ const makeSocketIOListeners = ( */ // Generate a UUID for each image - const preparedImages = images.map((image): InvokeAI.Image => { + const preparedImages = images.map((image): InvokeAI._Image => { return { uuid: uuidv4(), ...image, @@ -334,7 +337,7 @@ const makeSocketIOListeners = ( if ( initialImage === url || - (initialImage as InvokeAI.Image)?.url === url + (initialImage as InvokeAI._Image)?.url === url ) { dispatch(clearInitialImage()); } diff --git a/invokeai/frontend/web/src/app/socketio/middleware.ts b/invokeai/frontend/web/src/app/socketio/middleware.ts index a28e4edc80..46dafc7656 100644 --- a/invokeai/frontend/web/src/app/socketio/middleware.ts +++ b/invokeai/frontend/web/src/app/socketio/middleware.ts @@ -29,6 +29,8 @@ export const socketioMiddleware = () => { path: `${window.location.pathname}socket.io`, }); + socketio.disconnect(); + let areListenersSet = false; const middleware: Middleware = (store) => (next) => (action) => { diff --git a/invokeai/frontend/web/src/app/store.ts b/invokeai/frontend/web/src/app/store.ts index 29dbff3fba..3e046d8ed9 100644 --- a/invokeai/frontend/web/src/app/store.ts +++ b/invokeai/frontend/web/src/app/store.ts @@ -2,18 +2,32 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit'; import { persistReducer } from 'redux-persist'; import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web - +import dynamicMiddlewares from 'redux-dynamic-middlewares'; import { getPersistConfig } from 'redux-deep-persist'; import canvasReducer from 'features/canvas/store/canvasSlice'; import galleryReducer from 'features/gallery/store/gallerySlice'; +import resultsReducer from 'features/gallery/store/resultsSlice'; +import uploadsReducer from 'features/gallery/store/uploadsSlice'; import lightboxReducer from 'features/lightbox/store/lightboxSlice'; import generationReducer from 'features/parameters/store/generationSlice'; import postprocessingReducer from 'features/parameters/store/postprocessingSlice'; import systemReducer from 'features/system/store/systemSlice'; import uiReducer from 'features/ui/store/uiSlice'; +import modelsReducer from 'features/system/store/modelSlice'; +import nodesReducer from 'features/nodes/store/nodesSlice'; import { socketioMiddleware } from './socketio/middleware'; +import { socketMiddleware } from 'services/events/middleware'; +import { canvasBlacklist } from 'features/canvas/store/canvasPersistBlacklist'; +import { galleryBlacklist } from 'features/gallery/store/galleryPersistBlacklist'; +import { generationBlacklist } from 'features/parameters/store/generationPersistBlacklist'; +import { lightboxBlacklist } from 'features/lightbox/store/lightboxPersistBlacklist'; +import { modelsBlacklist } from 'features/system/store/modelsPersistBlacklist'; +import { nodesBlacklist } from 'features/nodes/store/nodesPersistBlacklist'; +import { postprocessingBlacklist } from 'features/parameters/store/postprocessingPersistBlacklist'; +import { systemBlacklist } from 'features/system/store/systemPersistsBlacklist'; +import { uiBlacklist } from 'features/ui/store/uiPersistBlacklist'; /** * redux-persist provides an easy and reliable way to persist state across reloads. @@ -29,49 +43,18 @@ import { socketioMiddleware } from './socketio/middleware'; * The necesssary nested persistors with blacklists are configured below. */ -const canvasBlacklist = [ - 'cursorPosition', - 'isCanvasInitialized', - 'doesCanvasNeedScaling', -].map((blacklistItem) => `canvas.${blacklistItem}`); - -const systemBlacklist = [ - 'currentIteration', - 'currentStatus', - 'currentStep', - 'isCancelable', - 'isConnected', - 'isESRGANAvailable', - 'isGFPGANAvailable', - 'isProcessing', - 'socketId', - 'totalIterations', - 'totalSteps', - 'openModel', - 'cancelOptions.cancelAfter', -].map((blacklistItem) => `system.${blacklistItem}`); - -const galleryBlacklist = [ - 'categories', - 'currentCategory', - 'currentImage', - 'currentImageUuid', - 'shouldAutoSwitchToNewImages', - 'intermediateImage', -].map((blacklistItem) => `gallery.${blacklistItem}`); - -const lightboxBlacklist = ['isLightboxOpen'].map( - (blacklistItem) => `lightbox.${blacklistItem}` -); - const rootReducer = combineReducers({ - generation: generationReducer, - postprocessing: postprocessingReducer, - gallery: galleryReducer, - system: systemReducer, canvas: canvasReducer, - ui: uiReducer, + gallery: galleryReducer, + generation: generationReducer, lightbox: lightboxReducer, + models: modelsReducer, + nodes: nodesReducer, + postprocessing: postprocessingReducer, + results: resultsReducer, + system: systemReducer, + ui: uiReducer, + uploads: uploadsReducer, }); const rootPersistConfig = getPersistConfig({ @@ -80,23 +63,40 @@ const rootPersistConfig = getPersistConfig({ rootReducer, blacklist: [ ...canvasBlacklist, - ...systemBlacklist, ...galleryBlacklist, + ...generationBlacklist, ...lightboxBlacklist, + ...modelsBlacklist, + ...nodesBlacklist, + ...postprocessingBlacklist, + // ...resultsBlacklist, + 'results', + ...systemBlacklist, + ...uiBlacklist, + // ...uploadsBlacklist, + 'uploads', ], debounce: 300, }); const persistedReducer = persistReducer(rootPersistConfig, rootReducer); -// Continue with store setup +// TODO: rip the old middleware out when nodes is complete +export function buildMiddleware() { + if (import.meta.env.MODE === 'nodes' || import.meta.env.MODE === 'package') { + return socketMiddleware(); + } else { + return socketioMiddleware(); + } +} + export const store = configureStore({ reducer: persistedReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ immutableCheck: false, serializableCheck: false, - }).concat(socketioMiddleware()), + }).concat(dynamicMiddlewares), devTools: { // Uncommenting these very rapidly called actions makes the redux dev tools output much more readable actionsDenylist: [ diff --git a/invokeai/frontend/web/src/app/storeUtils.ts b/invokeai/frontend/web/src/app/storeUtils.ts new file mode 100644 index 0000000000..851c0ba09d --- /dev/null +++ b/invokeai/frontend/web/src/app/storeUtils.ts @@ -0,0 +1,8 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { AppDispatch, RootState } from './store'; + +// https://redux-toolkit.js.org/usage/usage-with-typescript#defining-a-pre-typed-createasyncthunk +export const createAppAsyncThunk = createAsyncThunk.withTypes<{ + state: RootState; + dispatch: AppDispatch; +}>(); diff --git a/invokeai/frontend/web/src/common/components/IAISlider.tsx b/invokeai/frontend/web/src/common/components/IAISlider.tsx index 03742c0100..189ef4f5ad 100644 --- a/invokeai/frontend/web/src/common/components/IAISlider.tsx +++ b/invokeai/frontend/web/src/common/components/IAISlider.tsx @@ -44,12 +44,10 @@ export type IAIFullSliderProps = { inputReadOnly?: boolean; withReset?: boolean; handleReset?: () => void; - isResetDisabled?: boolean; - isSliderDisabled?: boolean; - isInputDisabled?: boolean; tooltipSuffix?: string; hideTooltip?: boolean; isCompact?: boolean; + isDisabled?: boolean; sliderFormControlProps?: FormControlProps; sliderFormLabelProps?: FormLabelProps; sliderMarkProps?: Omit; @@ -80,10 +78,8 @@ const IAISlider = (props: IAIFullSliderProps) => { withReset = false, hideTooltip = false, isCompact = false, + isDisabled = false, handleReset, - isResetDisabled, - isSliderDisabled, - isInputDisabled, sliderFormControlProps, sliderFormLabelProps, sliderMarkProps, @@ -149,6 +145,7 @@ const IAISlider = (props: IAIFullSliderProps) => { } : {} } + isDisabled={isDisabled} {...sliderFormControlProps} > @@ -166,15 +163,13 @@ const IAISlider = (props: IAIFullSliderProps) => { onMouseEnter={() => setShowTooltip(true)} onMouseLeave={() => setShowTooltip(false)} focusThumbOnChange={false} - isDisabled={isSliderDisabled} - // width={width} + isDisabled={isDisabled} {...rest} > {withSliderMarks && ( <> { { value={localInputValue} onChange={handleInputChange} onBlur={handleInputBlur} - isDisabled={isInputDisabled} {...sliderNumberInputProps} > { aria-label={t('accessibility.reset')} tooltip="Reset" icon={} + isDisabled={isDisabled} onClick={handleResetDisable} - isDisabled={isResetDisabled} {...sliderIAIIconButtonProps} /> )} diff --git a/invokeai/frontend/web/src/common/components/ImageToImageOverlay.tsx b/invokeai/frontend/web/src/common/components/ImageToImageOverlay.tsx new file mode 100644 index 0000000000..c006cf0c6b --- /dev/null +++ b/invokeai/frontend/web/src/common/components/ImageToImageOverlay.tsx @@ -0,0 +1,79 @@ +import { Badge, Box, ButtonGroup, Flex } from '@chakra-ui/react'; +import { RootState } from 'app/store'; +import { useAppDispatch, useAppSelector } from 'app/storeHooks'; +import { clearInitialImage } from 'features/parameters/store/generationSlice'; +import { useCallback } from 'react'; +import IAIIconButton from 'common/components/IAIIconButton'; +import { FaUndo, FaUpload } from 'react-icons/fa'; +import { useTranslation } from 'react-i18next'; +import { Image } from 'app/invokeai'; + +type ImageToImageOverlayProps = { + setIsLoaded: (isLoaded: boolean) => void; + image: Image; +}; + +const ImageToImageOverlay = ({ + setIsLoaded, + image, +}: ImageToImageOverlayProps) => { + const isImageToImageEnabled = useAppSelector( + (state: RootState) => state.generation.isImageToImageEnabled + ); + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const handleResetInitialImage = useCallback(() => { + dispatch(clearInitialImage()); + setIsLoaded(false); + }, [dispatch, setIsLoaded]); + + return ( + + + } + aria-label={t('accessibility.reset')} + onClick={handleResetInitialImage} + /> + } + aria-label={t('common.upload')} + /> + + + + {image.metadata?.width} × {image.metadata?.height} + + + + ); +}; + +export default ImageToImageOverlay; diff --git a/invokeai/frontend/web/src/common/components/ImageUploader.tsx b/invokeai/frontend/web/src/common/components/ImageUploader.tsx index c4f4dca9df..beaa5f02d2 100644 --- a/invokeai/frontend/web/src/common/components/ImageUploader.tsx +++ b/invokeai/frontend/web/src/common/components/ImageUploader.tsx @@ -2,7 +2,6 @@ import { Box, useToast } from '@chakra-ui/react'; import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import useImageUploader from 'common/hooks/useImageUploader'; -import { uploadImage } from 'features/gallery/store/thunks/uploadImage'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { ResourceKey } from 'i18next'; import { @@ -15,6 +14,7 @@ import { } from 'react'; import { FileRejection, useDropzone } from 'react-dropzone'; import { useTranslation } from 'react-i18next'; +import { imageUploaded } from 'services/thunks/image'; import ImageUploadOverlay from './ImageUploadOverlay'; type ImageUploaderProps = { @@ -49,7 +49,7 @@ const ImageUploader = (props: ImageUploaderProps) => { const fileAcceptedCallback = useCallback( async (file: File) => { - dispatch(uploadImage({ imageFile: file })); + dispatch(imageUploaded({ formData: { file } })); }, [dispatch] ); @@ -124,7 +124,7 @@ const ImageUploader = (props: ImageUploaderProps) => { return; } - dispatch(uploadImage({ imageFile: file })); + dispatch(imageUploaded({ formData: { file } })); }; document.addEventListener('paste', pasteImageListener); return () => { diff --git a/invokeai/frontend/web/src/common/components/SelectImagePlaceholder.tsx b/invokeai/frontend/web/src/common/components/SelectImagePlaceholder.tsx new file mode 100644 index 0000000000..c52cf75f9f --- /dev/null +++ b/invokeai/frontend/web/src/common/components/SelectImagePlaceholder.tsx @@ -0,0 +1,12 @@ +import { Flex, Icon } from '@chakra-ui/react'; +import { FaImage } from 'react-icons/fa'; + +const SelectImagePlaceholder = () => { + return ( + + + + ); +}; + +export default SelectImagePlaceholder; diff --git a/invokeai/frontend/web/src/common/components/WorkInProgress/NodesWIP.tsx b/invokeai/frontend/web/src/common/components/WorkInProgress/NodesWIP.tsx index c86aa767dd..6129670d06 100644 --- a/invokeai/frontend/web/src/common/components/WorkInProgress/NodesWIP.tsx +++ b/invokeai/frontend/web/src/common/components/WorkInProgress/NodesWIP.tsx @@ -1,27 +1,160 @@ -import { Flex, Heading, Text, VStack } from '@chakra-ui/react'; -import { useTranslation } from 'react-i18next'; -import WorkInProgress from './WorkInProgress'; +// import WorkInProgress from './WorkInProgress'; +// import ReactFlow, { +// applyEdgeChanges, +// applyNodeChanges, +// Background, +// Controls, +// Edge, +// Handle, +// Node, +// NodeTypes, +// OnEdgesChange, +// OnNodesChange, +// Position, +// } from 'reactflow'; -export default function NodesWIP() { - const { t } = useTranslation(); - return ( - - - {t('common.nodes')} - - {t('common.nodesDesc')} - - - - ); -} +// import 'reactflow/dist/style.css'; +// import { +// Fragment, +// FunctionComponent, +// ReactNode, +// useCallback, +// useMemo, +// useState, +// } from 'react'; +// import { OpenAPIV3 } from 'openapi-types'; +// import { filter, map, reduce } from 'lodash'; +// import { +// Box, +// Flex, +// FormControl, +// FormLabel, +// Input, +// Select, +// Switch, +// Text, +// NumberInput, +// NumberInputField, +// NumberInputStepper, +// NumberIncrementStepper, +// NumberDecrementStepper, +// Tooltip, +// chakra, +// Badge, +// Heading, +// VStack, +// HStack, +// Menu, +// MenuButton, +// MenuList, +// MenuItem, +// MenuItemOption, +// MenuGroup, +// MenuOptionGroup, +// MenuDivider, +// IconButton, +// } from '@chakra-ui/react'; +// import { FaPlus } from 'react-icons/fa'; +// import { +// FIELD_NAMES as FIELD_NAMES, +// FIELDS, +// INVOCATION_NAMES as INVOCATION_NAMES, +// INVOCATIONS, +// } from 'features/nodeEditor/constants'; + +// console.log('invocations', INVOCATIONS); + +// const nodeTypes = reduce( +// INVOCATIONS, +// (acc, val, key) => { +// acc[key] = val.component; +// return acc; +// }, +// {} as NodeTypes +// ); + +// console.log('nodeTypes', nodeTypes); + +// // make initial nodes one of every node for now +// let n = 0; +// const initialNodes = map(INVOCATIONS, (i) => ({ +// id: i.type, +// type: i.title, +// position: { x: (n += 20), y: (n += 20) }, +// data: {}, +// })); + +// console.log('initialNodes', initialNodes); + +// export default function NodesWIP() { +// const [nodes, setNodes] = useState([]); +// const [edges, setEdges] = useState([]); + +// const onNodesChange: OnNodesChange = useCallback( +// (changes) => setNodes((nds) => applyNodeChanges(changes, nds)), +// [] +// ); + +// const onEdgesChange: OnEdgesChange = useCallback( +// (changes) => setEdges((eds: Edge[]) => applyEdgeChanges(changes, eds)), +// [] +// ); + +// return ( +// +// +// +// +// +// +// {FIELD_NAMES.map((field) => ( +// +// {field} +// +// ))} +// +// +// } +// sx={{ position: 'absolute', top: 2, left: 2 }} +// /> +// +// {INVOCATION_NAMES.map((name) => { +// const invocation = INVOCATIONS[name]; +// return ( +// +// {invocation.title} +// +// ); +// })} +// +// +// +// ); +// } + +export default {}; diff --git a/invokeai/frontend/web/src/common/components/WorkInProgress/WorkInProgress.tsx b/invokeai/frontend/web/src/common/components/WorkInProgress/WorkInProgress.tsx index deb9110d56..385796d53b 100644 --- a/invokeai/frontend/web/src/common/components/WorkInProgress/WorkInProgress.tsx +++ b/invokeai/frontend/web/src/common/components/WorkInProgress/WorkInProgress.tsx @@ -14,6 +14,8 @@ const WorkInProgress = (props: WorkInProgressProps) => { width: '100%', height: '100%', bg: 'base.850', + borderRadius: 'base', + position: 'relative', }} > {children} diff --git a/invokeai/frontend/web/src/common/util/_parseMetadataZod.ts b/invokeai/frontend/web/src/common/util/_parseMetadataZod.ts new file mode 100644 index 0000000000..584399233f --- /dev/null +++ b/invokeai/frontend/web/src/common/util/_parseMetadataZod.ts @@ -0,0 +1,119 @@ +/** + * PARTIAL ZOD IMPLEMENTATION + * + * doesn't work well bc like most validators, zod is not built to skip invalid values. + * it mostly works but just seems clearer and simpler to manually parse for now. + * + * in the future it would be really nice if we could use zod for some things: + * - zodios (axios + zod): https://github.com/ecyrbe/zodios + * - openapi to zodios: https://github.com/astahmer/openapi-zod-client + */ + +// import { z } from 'zod'; + +// const zMetadataStringField = z.string(); +// export type MetadataStringField = z.infer; + +// const zMetadataIntegerField = z.number().int(); +// export type MetadataIntegerField = z.infer; + +// const zMetadataFloatField = z.number(); +// export type MetadataFloatField = z.infer; + +// const zMetadataBooleanField = z.boolean(); +// export type MetadataBooleanField = z.infer; + +// const zMetadataImageField = z.object({ +// image_type: z.union([ +// z.literal('results'), +// z.literal('uploads'), +// z.literal('intermediates'), +// ]), +// image_name: z.string().min(1), +// }); +// export type MetadataImageField = z.infer; + +// const zMetadataLatentsField = z.object({ +// latents_name: z.string().min(1), +// }); +// export type MetadataLatentsField = z.infer; + +// /** +// * zod Schema for any node field. Use a `transform()` to manually parse, skipping invalid values. +// */ +// const zAnyMetadataField = z.any().transform((val, ctx) => { +// // Grab the field name from the path +// const fieldName = String(ctx.path[ctx.path.length - 1]); + +// // `id` and `type` must be strings if they exist +// if (['id', 'type'].includes(fieldName)) { +// const reservedStringPropertyResult = zMetadataStringField.safeParse(val); +// if (reservedStringPropertyResult.success) { +// return reservedStringPropertyResult.data; +// } + +// return; +// } + +// // Parse the rest of the fields, only returning the data if the parsing is successful + +// const stringFieldResult = zMetadataStringField.safeParse(val); +// if (stringFieldResult.success) { +// return stringFieldResult.data; +// } + +// const integerFieldResult = zMetadataIntegerField.safeParse(val); +// if (integerFieldResult.success) { +// return integerFieldResult.data; +// } + +// const floatFieldResult = zMetadataFloatField.safeParse(val); +// if (floatFieldResult.success) { +// return floatFieldResult.data; +// } + +// const booleanFieldResult = zMetadataBooleanField.safeParse(val); +// if (booleanFieldResult.success) { +// return booleanFieldResult.data; +// } + +// const imageFieldResult = zMetadataImageField.safeParse(val); +// if (imageFieldResult.success) { +// return imageFieldResult.data; +// } + +// const latentsFieldResult = zMetadataImageField.safeParse(val); +// if (latentsFieldResult.success) { +// return latentsFieldResult.data; +// } +// }); + +// /** +// * The node metadata schema. +// */ +// const zNodeMetadata = z.object({ +// session_id: z.string().min(1).optional(), +// node: z.record(z.string().min(1), zAnyMetadataField).optional(), +// }); + +// export type NodeMetadata = z.infer; + +// const zMetadata = z.object({ +// invokeai: zNodeMetadata.optional(), +// 'sd-metadata': z.record(z.string().min(1), z.any()).optional(), +// }); +// export type Metadata = z.infer; + +// export const parseMetadata = ( +// metadata: Record +// ): Metadata | undefined => { +// const result = zMetadata.safeParse(metadata); +// if (!result.success) { +// console.log(result.error.issues); +// return; +// } + +// return result.data; +// }; + +export default {}; diff --git a/invokeai/frontend/web/src/common/util/getTimestamp.ts b/invokeai/frontend/web/src/common/util/getTimestamp.ts new file mode 100644 index 0000000000..570283fa8f --- /dev/null +++ b/invokeai/frontend/web/src/common/util/getTimestamp.ts @@ -0,0 +1,6 @@ +import dateFormat from 'dateformat'; + +/** + * Get a `now` timestamp with 1s precision, formatted as ISO datetime. + */ +export const getTimestamp = () => dateFormat(new Date(), 'isoDateTime'); diff --git a/invokeai/frontend/web/src/common/util/getUrl.ts b/invokeai/frontend/web/src/common/util/getUrl.ts new file mode 100644 index 0000000000..325d220e6b --- /dev/null +++ b/invokeai/frontend/web/src/common/util/getUrl.ts @@ -0,0 +1,28 @@ +import { RootState } from 'app/store'; +import { useAppSelector } from 'app/storeHooks'; +import { OpenAPI } from 'services/api'; + +export const getUrlAlt = (url: string, shouldTransformUrls: boolean) => { + if (OpenAPI.BASE && shouldTransformUrls) { + return [OpenAPI.BASE, url].join('/'); + } + + return url; +}; + +export const useGetUrl = () => { + const shouldTransformUrls = useAppSelector( + (state: RootState) => state.system.shouldTransformUrls + ); + + return { + shouldTransformUrls, + getUrl: (url?: string) => { + if (OpenAPI.BASE && shouldTransformUrls) { + return [OpenAPI.BASE, url].join('/'); + } + + return url; + }, + }; +}; diff --git a/invokeai/frontend/web/src/common/util/parseMetadata.ts b/invokeai/frontend/web/src/common/util/parseMetadata.ts new file mode 100644 index 0000000000..433aa9b2a1 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/parseMetadata.ts @@ -0,0 +1,169 @@ +import { forEach, size } from 'lodash'; +import { ImageField, LatentsField } from 'services/api'; + +const OBJECT_TYPESTRING = '[object Object]'; +const STRING_TYPESTRING = '[object String]'; +const NUMBER_TYPESTRING = '[object Number]'; +const BOOLEAN_TYPESTRING = '[object Boolean]'; +const ARRAY_TYPESTRING = '[object Array]'; + +const isObject = (obj: unknown): obj is Record => + Object.prototype.toString.call(obj) === OBJECT_TYPESTRING; + +const isString = (obj: unknown): obj is string => + Object.prototype.toString.call(obj) === STRING_TYPESTRING; + +const isNumber = (obj: unknown): obj is number => + Object.prototype.toString.call(obj) === NUMBER_TYPESTRING; + +const isBoolean = (obj: unknown): obj is boolean => + Object.prototype.toString.call(obj) === BOOLEAN_TYPESTRING; + +const isArray = (obj: unknown): obj is Array => + Object.prototype.toString.call(obj) === ARRAY_TYPESTRING; + +const parseImageField = (imageField: unknown): ImageField | undefined => { + // Must be an object + if (!isObject(imageField)) { + return; + } + + // An ImageField must have both `image_name` and `image_type` + if (!('image_name' in imageField && 'image_type' in imageField)) { + return; + } + + // An ImageField's `image_type` must be one of the allowed values + if ( + !['results', 'uploads', 'intermediates'].includes(imageField.image_type) + ) { + return; + } + + // An ImageField's `image_name` must be a string + if (typeof imageField.image_name !== 'string') { + return; + } + + // Build a valid ImageField + return { + image_type: imageField.image_type, + image_name: imageField.image_name, + }; +}; + +const parseLatentsField = (latentsField: unknown): LatentsField | undefined => { + // Must be an object + if (!isObject(latentsField)) { + return; + } + + // A LatentsField must have a `latents_name` + if (!('latents_name' in latentsField)) { + return; + } + + // A LatentsField's `latents_name` must be a string + if (typeof latentsField.latents_name !== 'string') { + return; + } + + // Build a valid LatentsField + return { + latents_name: latentsField.latents_name, + }; +}; + +type NodeMetadata = { + [key: string]: string | number | boolean | ImageField | LatentsField; +}; + +type InvokeAIMetadata = { + session_id?: string; + node?: NodeMetadata; +}; + +export const parseNodeMetadata = ( + nodeMetadata: Record +): NodeMetadata | undefined => { + if (!isObject(nodeMetadata)) { + return; + } + + const parsed: NodeMetadata = {}; + + forEach(nodeMetadata, (nodeItem, nodeKey) => { + // `id` and `type` must be strings if they are present + if (['id', 'type'].includes(nodeKey)) { + if (isString(nodeItem)) { + parsed[nodeKey] = nodeItem; + } + return; + } + + // the only valid object types are ImageField and LatentsField + if (isObject(nodeItem)) { + if ('image_name' in nodeItem || 'image_type' in nodeItem) { + const imageField = parseImageField(nodeItem); + if (imageField) { + parsed[nodeKey] = imageField; + } + return; + } + + if ('latents_name' in nodeItem) { + const latentsField = parseLatentsField(nodeItem); + if (latentsField) { + parsed[nodeKey] = latentsField; + } + return; + } + } + + // otherwise we accept any string, number or boolean + if (isString(nodeItem) || isNumber(nodeItem) || isBoolean(nodeItem)) { + parsed[nodeKey] = nodeItem; + return; + } + }); + + if (size(parsed) === 0) { + return; + } + + return parsed; +}; + +export const parseInvokeAIMetadata = ( + metadata: Record | undefined +): InvokeAIMetadata | undefined => { + if (metadata === undefined) { + return; + } + + if (!isObject(metadata)) { + return; + } + + const parsed: InvokeAIMetadata = {}; + + forEach(metadata, (item, key) => { + if (key === 'session_id' && isString(item)) { + parsed['session_id'] = item; + } + + if (key === 'node' && isObject(item)) { + const nodeMetadata = parseNodeMetadata(item); + + if (nodeMetadata) { + parsed['node'] = nodeMetadata; + } + } + }); + + if (size(parsed) === 0) { + return; + } + + return parsed; +}; diff --git a/invokeai/frontend/web/src/component.tsx b/invokeai/frontend/web/src/component.tsx index 3b6d16855e..01c3513a78 100644 --- a/invokeai/frontend/web/src/component.tsx +++ b/invokeai/frontend/web/src/component.tsx @@ -1,8 +1,10 @@ -import React, { lazy, PropsWithChildren } from 'react'; +import React, { lazy, PropsWithChildren, useEffect, useState } from 'react'; import { Provider } from 'react-redux'; import { PersistGate } from 'redux-persist/integration/react'; -import { store } from './app/store'; +import { buildMiddleware, store } from './app/store'; import { persistor } from './persistor'; +import { OpenAPI } from 'services/api'; +import { InvokeTabName } from 'features/ui/store/tabMap'; import '@fontsource/inter/100.css'; import '@fontsource/inter/200.css'; import '@fontsource/inter/300.css'; @@ -17,18 +19,61 @@ import Loading from './Loading'; // Localization import './i18n'; +import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares'; const App = lazy(() => import('./app/App')); const ThemeLocaleProvider = lazy(() => import('./app/ThemeLocaleProvider')); -export default function Component(props: PropsWithChildren) { +interface Props extends PropsWithChildren { + apiUrl?: string; + disabledPanels?: string[]; + disabledTabs?: InvokeTabName[]; + token?: string; + shouldTransformUrls?: boolean; +} + +export default function Component({ + apiUrl, + disabledPanels = [], + disabledTabs = [], + token, + children, + shouldTransformUrls, +}: Props) { + useEffect(() => { + // configure API client token + if (token) { + OpenAPI.TOKEN = token; + } + + // configure API client base url + if (apiUrl) { + OpenAPI.BASE = apiUrl; + } + + // reset dynamically added middlewares + resetMiddlewares(); + + // TODO: at this point, after resetting the middleware, we really ought to clean up the socket + // stuff by calling `dispatch(socketReset())`. but we cannot dispatch from here as we are + // outside the provider. it's not needed until there is the possibility that we will change + // the `apiUrl`/`token` dynamically. + + // rebuild socket middleware with token and apiUrl + addMiddleware(buildMiddleware()); + }, [apiUrl, token]); + return ( } persistor={persistor}> }> - {props.children} + + {children} + diff --git a/invokeai/frontend/web/src/exports.tsx b/invokeai/frontend/web/src/exports.tsx index 35c6bb5022..ffab33ead7 100644 --- a/invokeai/frontend/web/src/exports.tsx +++ b/invokeai/frontend/web/src/exports.tsx @@ -5,6 +5,8 @@ import ThemeChanger from './features/system/components/ThemeChanger'; import IAIPopover from './common/components/IAIPopover'; import IAIIconButton from './common/components/IAIIconButton'; import SettingsModal from './features/system/components/SettingsModal/SettingsModal'; +import StatusIndicator from './features/system/components/StatusIndicator'; +import ModelSelect from 'features/system/components/ModelSelect'; export default Component; export { @@ -13,4 +15,6 @@ export { IAIPopover, IAIIconButton, SettingsModal, + StatusIndicator, + ModelSelect, }; diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx index 6e47b19bc7..cb7ab5fee8 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx @@ -1,6 +1,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'app/store'; import { useAppSelector } from 'app/storeHooks'; +import { useGetUrl } from 'common/util/getUrl'; import { GalleryState } from 'features/gallery/store/gallerySlice'; import { ImageConfig } from 'konva/lib/shapes/Image'; import { isEqual } from 'lodash'; @@ -25,7 +26,7 @@ type Props = Omit; const IAICanvasIntermediateImage = (props: Props) => { const { ...rest } = props; const intermediateImage = useAppSelector(selector); - + const { getUrl } = useGetUrl(); const [loadedImageElement, setLoadedImageElement] = useState(null); @@ -36,8 +37,8 @@ const IAICanvasIntermediateImage = (props: Props) => { tempImage.onload = () => { setLoadedImageElement(tempImage); }; - tempImage.src = intermediateImage.url; - }, [intermediateImage]); + tempImage.src = getUrl(intermediateImage.url); + }, [intermediateImage, getUrl]); if (!intermediateImage?.boundingBox) return null; diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx index 3ee493c7c0..1d2852eef6 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx @@ -1,5 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/storeHooks'; +import { useGetUrl } from 'common/util/getUrl'; import { canvasSelector } from 'features/canvas/store/canvasSelectors'; import { rgbaColorToString } from 'features/canvas/util/colorToString'; import { isEqual } from 'lodash'; @@ -32,6 +33,7 @@ const selector = createSelector( const IAICanvasObjectRenderer = () => { const { objects } = useAppSelector(selector); + const { getUrl } = useGetUrl(); if (!objects) return null; @@ -40,7 +42,12 @@ const IAICanvasObjectRenderer = () => { {objects.map((obj, i) => { if (isCanvasBaseImage(obj)) { return ( - + ); } else if (isCanvasBaseLine(obj)) { const line = ( diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx index 5ccb072942..1a84aa88bb 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx @@ -1,5 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/storeHooks'; +import { useGetUrl } from 'common/util/getUrl'; import { canvasSelector } from 'features/canvas/store/canvasSelectors'; import { GroupConfig } from 'konva/lib/Group'; import { isEqual } from 'lodash'; @@ -53,11 +54,16 @@ const IAICanvasStagingArea = (props: Props) => { width, height, } = useAppSelector(selector); + const { getUrl } = useGetUrl(); return ( {shouldShowStagingImage && currentStagingAreaImage && ( - + )} {shouldShowStagingOutline && ( diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasPersistBlacklist.ts b/invokeai/frontend/web/src/features/canvas/store/canvasPersistBlacklist.ts new file mode 100644 index 0000000000..67754cfc91 --- /dev/null +++ b/invokeai/frontend/web/src/features/canvas/store/canvasPersistBlacklist.ts @@ -0,0 +1,14 @@ +import { CanvasState } from './canvasTypes'; + +/** + * Canvas slice persist blacklist + */ +const itemsToBlacklist: (keyof CanvasState)[] = [ + 'cursorPosition', + 'isCanvasInitialized', + 'doesCanvasNeedScaling', +]; + +export const canvasBlacklist = itemsToBlacklist.map( + (blacklistItem) => `canvas.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index 3e564af907..34688ef659 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -156,7 +156,7 @@ export const canvasSlice = createSlice({ setCursorPosition: (state, action: PayloadAction) => { state.cursorPosition = action.payload; }, - setInitialCanvasImage: (state, action: PayloadAction) => { + setInitialCanvasImage: (state, action: PayloadAction) => { const image = action.payload; const { stageDimensions } = state; @@ -291,7 +291,7 @@ export const canvasSlice = createSlice({ state, action: PayloadAction<{ boundingBox: IRect; - image: InvokeAI.Image; + image: InvokeAI._Image; }> ) => { const { boundingBox, image } = action.payload; diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts b/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts index 984f0d4f6b..95cf573c3b 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts @@ -37,7 +37,7 @@ export type CanvasImage = { y: number; width: number; height: number; - image: InvokeAI.Image; + image: InvokeAI._Image; }; export type CanvasMaskLine = { @@ -125,7 +125,7 @@ export interface CanvasState { cursorPosition: Vector2d | null; doesCanvasNeedScaling: boolean; futureLayerStates: CanvasLayerState[]; - intermediateImage?: InvokeAI.Image; + intermediateImage?: InvokeAI._Image; isCanvasInitialized: boolean; isDrawing: boolean; isMaskEnabled: boolean; diff --git a/invokeai/frontend/web/src/features/canvas/store/thunks/mergeAndUploadCanvas.ts b/invokeai/frontend/web/src/features/canvas/store/thunks/mergeAndUploadCanvas.ts index 58e3af1523..a1a7bd3989 100644 --- a/invokeai/frontend/web/src/features/canvas/store/thunks/mergeAndUploadCanvas.ts +++ b/invokeai/frontend/web/src/features/canvas/store/thunks/mergeAndUploadCanvas.ts @@ -105,7 +105,7 @@ export const mergeAndUploadCanvas = const { url, width, height } = image; - const newImage: InvokeAI.Image = { + const newImage: InvokeAI._Image = { uuid: uuidv4(), category: shouldSaveToGallery ? 'result' : 'user', ...image, diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx index 18457d0cb3..fb6d861d8b 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx @@ -14,8 +14,9 @@ import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice'; import FaceRestoreSettings from 'features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreSettings'; import UpscaleSettings from 'features/parameters/components/AdvancedParameters/Upscale/UpscaleSettings'; import { + initialImageSelected, setAllParameters, - setInitialImage, + // setInitialImage, setSeed, } from 'features/parameters/store/generationSlice'; import { postprocessingSelector } from 'features/parameters/store/postprocessingSelectors'; @@ -48,11 +49,15 @@ import { FaShareAlt, FaTrash, } from 'react-icons/fa'; -import { gallerySelector } from '../store/gallerySelectors'; +import { + gallerySelector, + selectedImageSelector, +} from '../store/gallerySelectors'; import DeleteImageModal from './DeleteImageModal'; import { useCallback } from 'react'; import useSetBothPrompts from 'features/parameters/hooks/usePrompt'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; +import { useGetUrl } from 'common/util/getUrl'; const currentImageButtonsSelector = createSelector( [ @@ -62,6 +67,7 @@ const currentImageButtonsSelector = createSelector( uiSelector, lightboxSelector, activeTabNameSelector, + selectedImageSelector, ], ( system: SystemState, @@ -69,7 +75,8 @@ const currentImageButtonsSelector = createSelector( postprocessing, ui, lightbox, - activeTabName + activeTabName, + selectedImage ) => { const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } = system; @@ -95,6 +102,7 @@ const currentImageButtonsSelector = createSelector( activeTabName, isLightboxOpen, shouldHidePreview, + selectedImage, }; }, { @@ -121,27 +129,33 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { facetoolStrength, shouldDisableToolbarButtons, shouldShowImageDetails, - currentImage, + // currentImage, isLightboxOpen, activeTabName, shouldHidePreview, + selectedImage, } = useAppSelector(currentImageButtonsSelector); + const { getUrl, shouldTransformUrls } = useGetUrl(); const toast = useToast(); const { t } = useTranslation(); const setBothPrompts = useSetBothPrompts(); const handleClickUseAsInitialImage = () => { - if (!currentImage) return; + if (!selectedImage) return; if (isLightboxOpen) dispatch(setIsLightboxOpen(false)); - dispatch(setInitialImage(currentImage)); - dispatch(setActiveTab('img2img')); + dispatch(initialImageSelected(selectedImage.name)); + // dispatch(setInitialImage(currentImage)); + + // dispatch(setActiveTab('img2img')); }; const handleCopyImage = async () => { - if (!currentImage) return; + if (!selectedImage) return; - const blob = await fetch(currentImage.url).then((res) => res.blob()); + const blob = await fetch(getUrl(selectedImage.url)).then((res) => + res.blob() + ); const data = [new ClipboardItem({ [blob.type]: blob })]; await navigator.clipboard.write(data); @@ -155,24 +169,26 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { }; const handleCopyImageLink = () => { - navigator.clipboard - .writeText( - currentImage ? window.location.toString() + currentImage.url : '' - ) - .then(() => { - toast({ - title: t('toast.imageLinkCopied'), - status: 'success', - duration: 2500, - isClosable: true, - }); + const url = selectedImage + ? shouldTransformUrls + ? getUrl(selectedImage.url) + : window.location.toString() + selectedImage.url + : ''; + + navigator.clipboard.writeText(url).then(() => { + toast({ + title: t('toast.imageLinkCopied'), + status: 'success', + duration: 2500, + isClosable: true, }); + }); }; useHotkeys( 'shift+i', () => { - if (currentImage) { + if (selectedImage) { handleClickUseAsInitialImage(); toast({ title: t('toast.sentToImageToImage'), @@ -190,7 +206,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { }); } }, - [currentImage] + [selectedImage] ); const handlePreviewVisibility = () => { @@ -198,20 +214,23 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { }; const handleClickUseAllParameters = () => { - if (!currentImage) return; - currentImage.metadata && dispatch(setAllParameters(currentImage.metadata)); - if (currentImage.metadata?.image.type === 'img2img') { - dispatch(setActiveTab('img2img')); - } else if (currentImage.metadata?.image.type === 'txt2img') { - dispatch(setActiveTab('txt2img')); - } + if (!selectedImage) return; + // selectedImage.metadata && + // dispatch(setAllParameters(selectedImage.metadata)); + // if (selectedImage.metadata?.image.type === 'img2img') { + // dispatch(setActiveTab('img2img')); + // } else if (selectedImage.metadata?.image.type === 'txt2img') { + // dispatch(setActiveTab('txt2img')); + // } }; useHotkeys( 'a', () => { if ( - ['txt2img', 'img2img'].includes(currentImage?.metadata?.image?.type) + ['txt2img', 'img2img'].includes( + selectedImage?.metadata?.sd_metadata?.type + ) ) { handleClickUseAllParameters(); toast({ @@ -230,18 +249,18 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { }); } }, - [currentImage] + [selectedImage] ); const handleClickUseSeed = () => { - currentImage?.metadata && - dispatch(setSeed(currentImage.metadata.image.seed)); + selectedImage?.metadata && + dispatch(setSeed(selectedImage.metadata.sd_metadata.seed)); }; useHotkeys( 's', () => { - if (currentImage?.metadata?.image?.seed) { + if (selectedImage?.metadata?.sd_metadata?.seed) { handleClickUseSeed(); toast({ title: t('toast.seedSet'), @@ -259,19 +278,19 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { }); } }, - [currentImage] + [selectedImage] ); const handleClickUsePrompt = useCallback(() => { - if (currentImage?.metadata?.image?.prompt) { - setBothPrompts(currentImage?.metadata?.image?.prompt); + if (selectedImage?.metadata?.sd_metadata?.prompt) { + setBothPrompts(selectedImage?.metadata?.sd_metadata?.prompt); } - }, [currentImage?.metadata?.image?.prompt, setBothPrompts]); + }, [selectedImage?.metadata?.sd_metadata?.prompt, setBothPrompts]); useHotkeys( 'p', () => { - if (currentImage?.metadata?.image?.prompt) { + if (selectedImage?.metadata?.sd_metadata?.prompt) { handleClickUsePrompt(); toast({ title: t('toast.promptSet'), @@ -289,11 +308,11 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { }); } }, - [currentImage] + [selectedImage] ); const handleClickUpscale = () => { - currentImage && dispatch(runESRGAN(currentImage)); + // selectedImage && dispatch(runESRGAN(selectedImage)); }; useHotkeys( @@ -317,7 +336,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { } }, [ - currentImage, + selectedImage, isESRGANAvailable, shouldDisableToolbarButtons, isConnected, @@ -327,7 +346,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { ); const handleClickFixFaces = () => { - currentImage && dispatch(runFacetool(currentImage)); + // selectedImage && dispatch(runFacetool(selectedImage)); }; useHotkeys( @@ -351,7 +370,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { } }, [ - currentImage, + selectedImage, isGFPGANAvailable, shouldDisableToolbarButtons, isConnected, @@ -364,10 +383,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { dispatch(setShouldShowImageDetails(!shouldShowImageDetails)); const handleSendToCanvas = () => { - if (!currentImage) return; + if (!selectedImage) return; if (isLightboxOpen) dispatch(setIsLightboxOpen(false)); - dispatch(setInitialCanvasImage(currentImage)); + // dispatch(setInitialCanvasImage(selectedImage)); dispatch(requestCanvasRescale()); if (activeTabName !== 'unifiedCanvas') { @@ -385,7 +404,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { useHotkeys( 'i', () => { - if (currentImage) { + if (selectedImage) { handleClickShowImageDetails(); } else { toast({ @@ -396,7 +415,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { }); } }, - [currentImage, shouldShowImageDetails] + [selectedImage, shouldShowImageDetails] ); const handleLightBox = () => { @@ -457,7 +476,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { {t('parameters.copyImageToLink')} - + } size="sm" w="100%"> {t('parameters.downloadImage')} @@ -501,7 +520,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { icon={} tooltip={`${t('parameters.usePrompt')} (P)`} aria-label={`${t('parameters.usePrompt')} (P)`} - isDisabled={!currentImage?.metadata?.image?.prompt} + isDisabled={!selectedImage?.metadata?.sd_metadata?.prompt} onClick={handleClickUsePrompt} /> @@ -509,7 +528,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { icon={} tooltip={`${t('parameters.useSeed')} (S)`} aria-label={`${t('parameters.useSeed')} (S)`} - isDisabled={!currentImage?.metadata?.image?.seed} + isDisabled={!selectedImage?.metadata?.sd_metadata?.seed} onClick={handleClickUseSeed} /> @@ -519,7 +538,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { aria-label={`${t('parameters.useAll')} (A)`} isDisabled={ !['txt2img', 'img2img'].includes( - currentImage?.metadata?.image?.type + selectedImage?.metadata?.sd_metadata?.type ) } onClick={handleClickUseAllParameters} @@ -545,7 +564,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { { { /> - + {/* } tooltip={`${t('parameters.deleteImage')} (Del)`} aria-label={`${t('parameters.deleteImage')} (Del)`} - isDisabled={!currentImage || !isConnected || isProcessing} + isDisabled={!selectedImage || !isConnected || isProcessing} colorScheme="error" /> - + */} ); }; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx index 6c46e14391..2f249d77f8 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx @@ -4,17 +4,20 @@ import { useAppSelector } from 'app/storeHooks'; import { isEqual } from 'lodash'; import { MdPhoto } from 'react-icons/md'; -import { gallerySelector } from '../store/gallerySelectors'; +import { + gallerySelector, + selectedImageSelector, +} from '../store/gallerySelectors'; import CurrentImageButtons from './CurrentImageButtons'; import CurrentImagePreview from './CurrentImagePreview'; export const currentImageDisplaySelector = createSelector( - [gallerySelector], - (gallery) => { + [gallerySelector, selectedImageSelector], + (gallery, selectedImage) => { const { currentImage, intermediateImage } = gallery; return { - hasAnImageToDisplay: currentImage || intermediateImage, + hasAnImageToDisplay: selectedImage || intermediateImage, }; }, { diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx index 7b5ed2c181..0f049d3368 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx @@ -1,28 +1,48 @@ import { Box, Flex, Image } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/storeHooks'; -import { GalleryState } from 'features/gallery/store/gallerySlice'; +import { useGetUrl } from 'common/util/getUrl'; +import { systemSelector } from 'features/system/store/systemSelectors'; import { uiSelector } from 'features/ui/store/uiSelectors'; import { isEqual } from 'lodash'; +import { ReactEventHandler } from 'react'; import { APP_METADATA_HEIGHT } from 'theme/util/constants'; -import { gallerySelector } from '../store/gallerySelectors'; +import { selectedImageSelector } from '../store/gallerySelectors'; import CurrentImageFallback from './CurrentImageFallback'; import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer'; import NextPrevImageButtons from './NextPrevImageButtons'; import CurrentImageHidden from './CurrentImageHidden'; export const imagesSelector = createSelector( - [gallerySelector, uiSelector], - (gallery: GalleryState, ui) => { - const { currentImage, intermediateImage } = gallery; + [uiSelector, selectedImageSelector, systemSelector], + (ui, selectedImage, system) => { const { shouldShowImageDetails, shouldHidePreview } = ui; + const { progressImage } = system; + + // TODO: Clean this up, this is really gross + const imageToDisplay = progressImage + ? { + url: progressImage.dataURL, + width: progressImage.width, + height: progressImage.height, + isProgressImage: true, + image: progressImage, + } + : selectedImage + ? { + url: selectedImage.url, + width: selectedImage.metadata.width, + height: selectedImage.metadata.height, + isProgressImage: false, + image: selectedImage, + } + : null; return { - imageToDisplay: intermediateImage ? intermediateImage : currentImage, - isIntermediate: Boolean(intermediateImage), shouldShowImageDetails, shouldHidePreview, + imageToDisplay, }; }, { @@ -33,12 +53,9 @@ export const imagesSelector = createSelector( ); export default function CurrentImagePreview() { - const { - shouldShowImageDetails, - imageToDisplay, - isIntermediate, - shouldHidePreview, - } = useAppSelector(imagesSelector); + const { shouldShowImageDetails, imageToDisplay, shouldHidePreview } = + useAppSelector(imagesSelector); + const { getUrl } = useGetUrl(); return ( {imageToDisplay && ( - ) : !isIntermediate ? ( + ) : !imageToDisplay.isProgressImage ? ( ) : undefined } @@ -68,27 +91,31 @@ export default function CurrentImagePreview() { maxHeight: '100%', height: 'auto', position: 'absolute', - imageRendering: isIntermediate ? 'pixelated' : 'initial', + imageRendering: imageToDisplay.isProgressImage + ? 'pixelated' + : 'initial', borderRadius: 'base', }} /> )} {!shouldShowImageDetails && } - {shouldShowImageDetails && imageToDisplay && ( - - - - )} + {shouldShowImageDetails && + imageToDisplay && + 'metadata' in imageToDisplay.image && ( + + + + )} ); } diff --git a/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx index 734dc3b682..a1276df6d9 100644 --- a/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx @@ -52,7 +52,7 @@ interface DeleteImageModalProps { /** * The image to delete. */ - image?: InvokeAI.Image; + image?: InvokeAI._Image; } /** diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx index 0d034ed976..5973227b0b 100644 --- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx @@ -9,11 +9,14 @@ import { useToast, } from '@chakra-ui/react'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; -import { setCurrentImage } from 'features/gallery/store/gallerySlice'; import { + imageSelected, + setCurrentImage, +} from 'features/gallery/store/gallerySlice'; +import { + initialImageSelected, setAllImageToImageParameters, setAllParameters, - setInitialImage, setSeed, } from 'features/parameters/store/generationSlice'; import { DragEvent, memo, useState } from 'react'; @@ -31,6 +34,7 @@ import { useTranslation } from 'react-i18next'; import useSetBothPrompts from 'features/parameters/hooks/usePrompt'; import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice'; import IAIIconButton from 'common/components/IAIIconButton'; +import { useGetUrl } from 'common/util/getUrl'; interface HoverableImageProps { image: InvokeAI.Image; @@ -40,7 +44,7 @@ interface HoverableImageProps { const memoEqualityCheck = ( prev: HoverableImageProps, next: HoverableImageProps -) => prev.image.uuid === next.image.uuid && prev.isSelected === next.isSelected; +) => prev.image.name === next.image.name && prev.isSelected === next.isSelected; /** * Gallery image component with delete/use all/use seed buttons on hover. @@ -55,7 +59,8 @@ const HoverableImage = memo((props: HoverableImageProps) => { shouldUseSingleGalleryColumn, } = useAppSelector(hoverableImageSelector); const { image, isSelected } = props; - const { url, thumbnail, uuid, metadata } = image; + const { url, thumbnail, name, metadata } = image; + const { getUrl } = useGetUrl(); const [isHovered, setIsHovered] = useState(false); @@ -69,10 +74,9 @@ const HoverableImage = memo((props: HoverableImageProps) => { const handleMouseOut = () => setIsHovered(false); const handleUsePrompt = () => { - if (image.metadata?.image?.prompt) { - setBothPrompts(image.metadata?.image?.prompt); + if (image.metadata?.sd_metadata?.prompt) { + setBothPrompts(image.metadata?.sd_metadata?.prompt); } - toast({ title: t('toast.promptSet'), status: 'success', @@ -82,7 +86,8 @@ const HoverableImage = memo((props: HoverableImageProps) => { }; const handleUseSeed = () => { - image.metadata && dispatch(setSeed(image.metadata.image.seed)); + image.metadata.sd_metadata && + dispatch(setSeed(image.metadata.sd_metadata.image.seed)); toast({ title: t('toast.seedSet'), status: 'success', @@ -92,20 +97,11 @@ const HoverableImage = memo((props: HoverableImageProps) => { }; const handleSendToImageToImage = () => { - dispatch(setInitialImage(image)); - if (activeTabName !== 'img2img') { - dispatch(setActiveTab('img2img')); - } - toast({ - title: t('toast.sentToImageToImage'), - status: 'success', - duration: 2500, - isClosable: true, - }); + dispatch(initialImageSelected(image.name)); }; const handleSendToCanvas = () => { - dispatch(setInitialCanvasImage(image)); + // dispatch(setInitialCanvasImage(image)); dispatch(resizeAndScaleCanvas()); @@ -122,7 +118,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { }; const handleUseAllParameters = () => { - metadata && dispatch(setAllParameters(metadata)); + metadata.sd_metadata && dispatch(setAllParameters(metadata.sd_metadata)); toast({ title: t('toast.parametersSet'), status: 'success', @@ -132,11 +128,13 @@ const HoverableImage = memo((props: HoverableImageProps) => { }; const handleUseInitialImage = async () => { - if (metadata?.image?.init_image_path) { - const response = await fetch(metadata.image.init_image_path); + if (metadata.sd_metadata?.image?.init_image_path) { + const response = await fetch( + metadata.sd_metadata?.image?.init_image_path + ); if (response.ok) { dispatch(setActiveTab('img2img')); - dispatch(setAllImageToImageParameters(metadata)); + dispatch(setAllImageToImageParameters(metadata?.sd_metadata)); toast({ title: t('toast.initialImageSet'), status: 'success', @@ -155,16 +153,20 @@ const HoverableImage = memo((props: HoverableImageProps) => { }); }; - const handleSelectImage = () => dispatch(setCurrentImage(image)); + const handleSelectImage = () => { + dispatch(imageSelected(image.name)); + }; const handleDragStart = (e: DragEvent) => { - e.dataTransfer.setData('invokeai/imageUuid', uuid); + console.log('drag started'); + e.dataTransfer.setData('invokeai/imageName', image.name); + e.dataTransfer.setData('invokeai/imageType', image.type); e.dataTransfer.effectAllowed = 'move'; }; const handleLightBox = () => { - dispatch(setCurrentImage(image)); - dispatch(setIsLightboxOpen(true)); + // dispatch(setCurrentImage(image)); + // dispatch(setIsLightboxOpen(true)); }; return ( @@ -177,28 +179,30 @@ const HoverableImage = memo((props: HoverableImageProps) => { {t('parameters.usePrompt')} {t('parameters.useSeed')} {t('parameters.useAll')} {t('parameters.useInitImg')} @@ -209,9 +213,9 @@ const HoverableImage = memo((props: HoverableImageProps) => { {t('parameters.sendToUnifiedCanvas')} - + {/*

{t('parameters.deleteImage')}

-
+
*/}
)} @@ -219,7 +223,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { {(ref) => ( { shouldUseSingleGalleryColumn ? 'contain' : galleryImageObjectFit } rounded="md" - src={thumbnail || url} + src={getUrl(thumbnail || url)} loading="lazy" sx={{ position: 'absolute', @@ -290,7 +294,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { insetInlineEnd: 1, }} > - + {/* } @@ -298,7 +302,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { fontSize={14} isDisabled={!mayDeleteImage} /> - + */} )} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx index 8ddf862c47..31a2c4b055 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx @@ -1,4 +1,4 @@ -import { ButtonGroup, Flex, Grid, Icon, Text } from '@chakra-ui/react'; +import { ButtonGroup, Flex, Grid, Icon, Image, Text } from '@chakra-ui/react'; import { requestImages } from 'app/socketio/actions'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import IAIButton from 'common/components/IAIButton'; @@ -25,9 +25,44 @@ import HoverableImage from './HoverableImage'; import Scrollable from 'features/ui/components/common/Scrollable'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; +import { + resultsAdapter, + selectResultsAll, + selectResultsTotal, +} from '../store/resultsSlice'; +import { + receivedResultImagesPage, + receivedUploadImagesPage, +} from 'services/thunks/gallery'; +import { selectUploadsAll, uploadsAdapter } from '../store/uploadsSlice'; +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from 'app/store'; const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290; +const gallerySelector = createSelector( + [ + (state: RootState) => state.uploads, + (state: RootState) => state.results, + (state: RootState) => state.gallery, + ], + (uploads, results, gallery) => { + const { currentCategory } = gallery; + + return currentCategory === 'result' + ? { + images: resultsAdapter.getSelectors().selectAll(results), + isLoading: results.isLoading, + areMoreImagesAvailable: results.page < results.pages - 1, + } + : { + images: uploadsAdapter.getSelectors().selectAll(uploads), + isLoading: uploads.isLoading, + areMoreImagesAvailable: uploads.page < uploads.pages - 1, + }; + } +); + const ImageGalleryContent = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -35,7 +70,7 @@ const ImageGalleryContent = () => { const [shouldShouldIconButtons, setShouldShouldIconButtons] = useState(true); const { - images, + // images, currentCategory, currentImageUuid, shouldPinGallery, @@ -43,12 +78,24 @@ const ImageGalleryContent = () => { galleryGridTemplateColumns, galleryImageObjectFit, shouldAutoSwitchToNewImages, - areMoreImagesAvailable, + // areMoreImagesAvailable, shouldUseSingleGalleryColumn, } = useAppSelector(imageGallerySelector); + const { images, areMoreImagesAvailable, isLoading } = + useAppSelector(gallerySelector); + + // const handleClickLoadMore = () => { + // dispatch(requestImages(currentCategory)); + // }; const handleClickLoadMore = () => { - dispatch(requestImages(currentCategory)); + if (currentCategory === 'result') { + dispatch(receivedResultImagesPage()); + } + + if (currentCategory === 'user') { + dispatch(receivedUploadImagesPage()); + } }; const handleChangeGalleryImageMinimumWidth = (v: number) => { @@ -203,11 +250,11 @@ const ImageGalleryContent = () => { style={{ gridTemplateColumns: galleryGridTemplateColumns }} > {images.map((image) => { - const { uuid } = image; - const isSelected = currentImageUuid === uuid; + const { name } = image; + const isSelected = currentImageUuid === name; return ( @@ -217,6 +264,7 @@ const ImageGalleryContent = () => { {areMoreImagesAvailable diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryPanel.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryPanel.tsx index da97dea9bf..1d43cac476 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryPanel.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryPanel.tsx @@ -31,12 +31,13 @@ const GALLERY_TAB_WIDTHS: Record< InvokeTabName, { galleryMinWidth: number; galleryMaxWidth: number } > = { - txt2img: { galleryMinWidth: 200, galleryMaxWidth: 500 }, - img2img: { galleryMinWidth: 200, galleryMaxWidth: 500 }, + // txt2img: { galleryMinWidth: 200, galleryMaxWidth: 500 }, + // img2img: { galleryMinWidth: 200, galleryMaxWidth: 500 }, + linear: { galleryMinWidth: 200, galleryMaxWidth: 500 }, unifiedCanvas: { galleryMinWidth: 200, galleryMaxWidth: 200 }, nodes: { galleryMinWidth: 200, galleryMaxWidth: 500 }, - postprocessing: { galleryMinWidth: 200, galleryMaxWidth: 500 }, - training: { galleryMinWidth: 200, galleryMaxWidth: 500 }, + // postprocessing: { galleryMinWidth: 200, galleryMaxWidth: 500 }, + // training: { galleryMinWidth: 200, galleryMaxWidth: 500 }, }; const galleryPanelSelector = createSelector( diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx index 130c716f6b..1909dc56a7 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx @@ -11,6 +11,7 @@ import { } from '@chakra-ui/react'; import * as InvokeAI from 'app/invokeai'; import { useAppDispatch } from 'app/storeHooks'; +import { useGetUrl } from 'common/util/getUrl'; import promptToString from 'common/util/promptToString'; import { seedWeightsToString } from 'common/util/seedWeightPairs'; import useSetBothPrompts from 'features/parameters/hooks/usePrompt'; @@ -18,7 +19,7 @@ import { setCfgScale, setHeight, setImg2imgStrength, - setInitialImage, + // setInitialImage, setMaskPath, setPerlin, setSampler, @@ -120,7 +121,7 @@ type ImageMetadataViewerProps = { const memoEqualityCheck = ( prev: ImageMetadataViewerProps, next: ImageMetadataViewerProps -) => prev.image.uuid === next.image.uuid; +) => prev.image.name === next.image.name; // TODO: Show more interesting information in this component. @@ -137,34 +138,13 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { dispatch(setShouldShowImageDetails(false)); }); - const metadata = image?.metadata?.image || {}; - const dreamPrompt = image?.dreamPrompt; - - const { - cfg_scale, - fit, - height, - hires_fix, - init_image_path, - mask_image_path, - orig_path, - perlin, - postprocessing, - prompt, - sampler, - seamless, - seed, - steps, - strength, - threshold, - type, - variations, - width, - } = metadata; + const sessionId = image.metadata.invokeai?.session_id; + const node = image.metadata.invokeai?.node as Record; const { t } = useTranslation(); + const { getUrl } = useGetUrl(); - const metadataJSON = JSON.stringify(image.metadata, null, 2); + const metadataJSON = JSON.stringify(image, null, 2); return ( { > File: - + {image.url.length > 64 ? image.url.substring(0, 64).concat('...') : image.url} - {Object.keys(metadata).length > 0 ? ( + {node && Object.keys(node).length > 0 ? ( <> - {type && } - {image.metadata?.model_weights && ( - + {node.type && ( + )} - {['esrgan', 'gfpgan'].includes(type) && ( - - )} - {prompt && ( + {node.model && } + {node.prompt && ( setBothPrompts(prompt)} + onClick={() => setBothPrompts(node.prompt)} /> )} - {seed !== undefined && ( + {node.seed !== undefined && ( dispatch(setSeed(seed))} + value={node.seed} + onClick={() => dispatch(setSeed(Number(node.seed)))} /> )} - {threshold !== undefined && ( + {node.threshold !== undefined && ( dispatch(setThreshold(threshold))} + value={node.threshold} + onClick={() => dispatch(setThreshold(Number(node.threshold)))} /> )} - {perlin !== undefined && ( + {node.perlin !== undefined && ( dispatch(setPerlin(perlin))} + value={node.perlin} + onClick={() => dispatch(setPerlin(Number(node.perlin)))} /> )} - {sampler && ( + {node.scheduler && ( dispatch(setSampler(sampler))} + value={node.scheduler} + onClick={() => dispatch(setSampler(node.scheduler))} /> )} - {steps && ( + {node.steps && ( dispatch(setSteps(steps))} + value={node.steps} + onClick={() => dispatch(setSteps(Number(node.steps)))} /> )} - {cfg_scale !== undefined && ( + {node.cfg_scale !== undefined && ( dispatch(setCfgScale(cfg_scale))} + value={node.cfg_scale} + onClick={() => dispatch(setCfgScale(Number(node.cfg_scale)))} /> )} - {variations && variations.length > 0 && ( + {node.variations && node.variations.length > 0 && ( - dispatch(setSeedWeights(seedWeightsToString(variations))) + dispatch(setSeedWeights(seedWeightsToString(node.variations))) } /> )} - {seamless && ( + {node.seamless && ( dispatch(setSeamless(seamless))} + value={node.seamless} + onClick={() => dispatch(setSeamless(node.seamless))} /> )} - {hires_fix && ( + {node.hires_fix && ( dispatch(setHiresFix(hires_fix))} + value={node.hires_fix} + onClick={() => dispatch(setHiresFix(node.hires_fix))} /> )} - {width && ( + {node.width && ( dispatch(setWidth(width))} + value={node.width} + onClick={() => dispatch(setWidth(Number(node.width)))} /> )} - {height && ( + {node.height && ( dispatch(setHeight(height))} + value={node.height} + onClick={() => dispatch(setHeight(Number(node.height)))} /> )} - {init_image_path && ( + {/* {init_image_path && ( dispatch(setInitialImage(init_image_path))} /> - )} - {mask_image_path && ( - dispatch(setMaskPath(mask_image_path))} - /> - )} - {type === 'img2img' && strength && ( + )} */} + {node.strength && ( dispatch(setImg2imgStrength(strength))} + value={node.strength} + onClick={() => + dispatch(setImg2imgStrength(Number(node.strength))) + } /> )} - {fit && ( + {node.fit && ( dispatch(setShouldFitToWidthHeight(fit))} + value={node.fit} + onClick={() => dispatch(setShouldFitToWidthHeight(node.fit))} /> )} - {postprocessing && postprocessing.length > 0 && ( - <> - Postprocessing - {postprocessing.map( - ( - postprocess: InvokeAI.PostProcessedImageMetadata, - i: number - ) => { - if (postprocess.type === 'esrgan') { - const { scale, strength, denoise_str } = postprocess; - return ( - - {`${i + 1}: Upscale (ESRGAN)`} - dispatch(setUpscalingLevel(scale))} - /> - - dispatch(setUpscalingStrength(strength)) - } - /> - {denoise_str !== undefined && ( - - dispatch(setUpscalingDenoising(denoise_str)) - } - /> - )} - - ); - } else if (postprocess.type === 'gfpgan') { - const { strength } = postprocess; - return ( - - {`${ - i + 1 - }: Face restoration (GFPGAN)`} - - { - dispatch(setFacetoolStrength(strength)); - dispatch(setFacetoolType('gfpgan')); - }} - /> - - ); - } else if (postprocess.type === 'codeformer') { - const { strength, fidelity } = postprocess; - return ( - - {`${ - i + 1 - }: Face restoration (Codeformer)`} - - { - dispatch(setFacetoolStrength(strength)); - dispatch(setFacetoolType('codeformer')); - }} - /> - {fidelity && ( - { - dispatch(setCodeformerFidelity(fidelity)); - dispatch(setFacetoolType('codeformer')); - }} - /> - )} - - ); - } - } - )} - - )} - {dreamPrompt && ( - - )} - - - - } - size="xs" - variant="ghost" - fontSize={14} - onClick={() => navigator.clipboard.writeText(metadataJSON)} - /> - - Metadata JSON: - - -
{metadataJSON}
-
-
) : (
@@ -447,6 +299,37 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
)} + + + + } + size="xs" + variant="ghost" + fontSize={14} + onClick={() => navigator.clipboard.writeText(metadataJSON)} + /> + + Metadata JSON: + + +
{metadataJSON}
+
+
); }, memoEqualityCheck); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/OLD_ImageMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/OLD_ImageMetadataViewer.tsx new file mode 100644 index 0000000000..3339140a52 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/OLD_ImageMetadataViewer.tsx @@ -0,0 +1,470 @@ +import { ExternalLinkIcon } from '@chakra-ui/icons'; +import { + Box, + Center, + Flex, + Heading, + IconButton, + Link, + Text, + Tooltip, +} from '@chakra-ui/react'; +import * as InvokeAI from 'app/invokeai'; +import { useAppDispatch } from 'app/storeHooks'; +import { useGetUrl } from 'common/util/getUrl'; +import promptToString from 'common/util/promptToString'; +import { seedWeightsToString } from 'common/util/seedWeightPairs'; +import useSetBothPrompts from 'features/parameters/hooks/usePrompt'; +import { + setCfgScale, + setHeight, + setImg2imgStrength, + // setInitialImage, + setMaskPath, + setPerlin, + setSampler, + setSeamless, + setSeed, + setSeedWeights, + setShouldFitToWidthHeight, + setSteps, + setThreshold, + setWidth, +} from 'features/parameters/store/generationSlice'; +import { + setCodeformerFidelity, + setFacetoolStrength, + setFacetoolType, + setHiresFix, + setUpscalingDenoising, + setUpscalingLevel, + setUpscalingStrength, +} from 'features/parameters/store/postprocessingSlice'; +import { setShouldShowImageDetails } from 'features/ui/store/uiSlice'; +import { memo } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { FaCopy } from 'react-icons/fa'; +import { IoArrowUndoCircleOutline } from 'react-icons/io5'; +import * as png from '@stevebel/png'; + +type MetadataItemProps = { + isLink?: boolean; + label: string; + onClick?: () => void; + value: number | string | boolean; + labelPosition?: string; + withCopy?: boolean; +}; + +/** + * Component to display an individual metadata item or parameter. + */ +const MetadataItem = ({ + label, + value, + onClick, + isLink, + labelPosition, + withCopy = false, +}: MetadataItemProps) => { + const { t } = useTranslation(); + + return ( + + {onClick && ( + + } + size="xs" + variant="ghost" + fontSize={20} + onClick={onClick} + /> + + )} + {withCopy && ( + + } + size="xs" + variant="ghost" + fontSize={14} + onClick={() => navigator.clipboard.writeText(value.toString())} + /> + + )} + + + {label}: + + {isLink ? ( + + {value.toString()} + + ) : ( + + {value.toString()} + + )} + + + ); +}; + +type ImageMetadataViewerProps = { + image: InvokeAI.Image; +}; + +// TODO: I don't know if this is needed. +const memoEqualityCheck = ( + prev: ImageMetadataViewerProps, + next: ImageMetadataViewerProps +) => prev.image.name === next.image.name; + +// TODO: Show more interesting information in this component. + +/** + * Image metadata viewer overlays currently selected image and provides + * access to any of its metadata for use in processing. + */ +const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { + const dispatch = useAppDispatch(); + + const setBothPrompts = useSetBothPrompts(); + + useHotkeys('esc', () => { + dispatch(setShouldShowImageDetails(false)); + }); + + const metadata = image?.metadata.sd_metadata || {}; + const dreamPrompt = image?.metadata.sd_metadata?.dreamPrompt; + + const { + cfg_scale, + fit, + height, + hires_fix, + init_image_path, + mask_image_path, + orig_path, + perlin, + postprocessing, + prompt, + sampler, + seamless, + seed, + steps, + strength, + threshold, + type, + variations, + width, + model_weights, + } = metadata; + + const { t } = useTranslation(); + const { getUrl } = useGetUrl(); + + const metadataJSON = JSON.stringify(image, null, 2); + + // fetch(getUrl(image.url)) + // .then((r) => r.arrayBuffer()) + // .then((buffer) => { + // const { text } = png.decode(buffer); + // const metadata = text?.['sd-metadata'] + // ? JSON.parse(text['sd-metadata'] ?? {}) + // : {}; + // console.log(metadata); + // }); + + return ( + + + File: + + {image.url.length > 64 + ? image.url.substring(0, 64).concat('...') + : image.url} + + + + + + + } + size="xs" + variant="ghost" + fontSize={14} + onClick={() => navigator.clipboard.writeText(metadataJSON)} + /> + + Metadata JSON: + + +
{metadataJSON}
+
+
+ {Object.keys(metadata).length > 0 ? ( + <> + {type && } + {model_weights && ( + + )} + {['esrgan', 'gfpgan'].includes(type) && ( + + )} + {prompt && ( + setBothPrompts(prompt)} + /> + )} + {seed !== undefined && ( + dispatch(setSeed(seed))} + /> + )} + {threshold !== undefined && ( + dispatch(setThreshold(threshold))} + /> + )} + {perlin !== undefined && ( + dispatch(setPerlin(perlin))} + /> + )} + {sampler && ( + dispatch(setSampler(sampler))} + /> + )} + {steps && ( + dispatch(setSteps(steps))} + /> + )} + {cfg_scale !== undefined && ( + dispatch(setCfgScale(cfg_scale))} + /> + )} + {variations && variations.length > 0 && ( + + dispatch(setSeedWeights(seedWeightsToString(variations))) + } + /> + )} + {seamless && ( + dispatch(setSeamless(seamless))} + /> + )} + {hires_fix && ( + dispatch(setHiresFix(hires_fix))} + /> + )} + {width && ( + dispatch(setWidth(width))} + /> + )} + {height && ( + dispatch(setHeight(height))} + /> + )} + {/* {init_image_path && ( + dispatch(setInitialImage(init_image_path))} + /> + )} */} + {mask_image_path && ( + dispatch(setMaskPath(mask_image_path))} + /> + )} + {type === 'img2img' && strength && ( + dispatch(setImg2imgStrength(strength))} + /> + )} + {fit && ( + dispatch(setShouldFitToWidthHeight(fit))} + /> + )} + {postprocessing && postprocessing.length > 0 && ( + <> + Postprocessing + {postprocessing.map( + ( + postprocess: InvokeAI.PostProcessedImageMetadata, + i: number + ) => { + if (postprocess.type === 'esrgan') { + const { scale, strength, denoise_str } = postprocess; + return ( + + {`${i + 1}: Upscale (ESRGAN)`} + dispatch(setUpscalingLevel(scale))} + /> + + dispatch(setUpscalingStrength(strength)) + } + /> + {denoise_str !== undefined && ( + + dispatch(setUpscalingDenoising(denoise_str)) + } + /> + )} + + ); + } else if (postprocess.type === 'gfpgan') { + const { strength } = postprocess; + return ( + + {`${ + i + 1 + }: Face restoration (GFPGAN)`} + + { + dispatch(setFacetoolStrength(strength)); + dispatch(setFacetoolType('gfpgan')); + }} + /> + + ); + } else if (postprocess.type === 'codeformer') { + const { strength, fidelity } = postprocess; + return ( + + {`${ + i + 1 + }: Face restoration (Codeformer)`} + + { + dispatch(setFacetoolStrength(strength)); + dispatch(setFacetoolType('codeformer')); + }} + /> + {fidelity && ( + { + dispatch(setCodeformerFidelity(fidelity)); + dispatch(setFacetoolType('codeformer')); + }} + /> + )} + + ); + } + } + )} + + )} + {dreamPrompt && ( + + )} + + ) : ( +
+ + No metadata available + +
+ )} +
+ ); +}, memoEqualityCheck); + +ImageMetadataViewer.displayName = 'ImageMetadataViewer'; + +export default ImageMetadataViewer; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts new file mode 100644 index 0000000000..b662cf02c1 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts @@ -0,0 +1,35 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/storeHooks'; +import { ImageType } from 'services/api'; +import { selectResultsEntities } from '../store/resultsSlice'; +import { selectUploadsEntities } from '../store/uploadsSlice'; + +const useGetImageByNameSelector = createSelector( + [selectResultsEntities, selectUploadsEntities], + (allResults, allUploads) => { + return { allResults, allUploads }; + } +); + +const useGetImageByNameAndType = () => { + const { allResults, allUploads } = useAppSelector(useGetImageByNameSelector); + + return (name: string, type: ImageType) => { + if (type === 'results') { + const resultImagesResult = allResults[name]; + + if (resultImagesResult) { + return resultImagesResult; + } + } + + if (type === 'uploads') { + const userImagesResult = allUploads[name]; + if (userImagesResult) { + return userImagesResult; + } + } + }; +}; + +export default useGetImageByNameAndType; diff --git a/invokeai/frontend/web/src/features/gallery/store/galleryPersistBlacklist.ts b/invokeai/frontend/web/src/features/gallery/store/galleryPersistBlacklist.ts new file mode 100644 index 0000000000..37f3f48746 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/store/galleryPersistBlacklist.ts @@ -0,0 +1,17 @@ +import { GalleryState } from './gallerySlice'; + +/** + * Gallery slice persist blacklist + */ +const itemsToBlacklist: (keyof GalleryState)[] = [ + 'categories', + 'currentCategory', + 'currentImage', + 'currentImageUuid', + 'shouldAutoSwitchToNewImages', + 'intermediateImage', +]; + +export const galleryBlacklist = itemsToBlacklist.map( + (blacklistItem) => `gallery.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts index 61223692fa..e9ef5ea88d 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts @@ -7,6 +7,16 @@ import { uiSelector, } from 'features/ui/store/uiSelectors'; import { isEqual } from 'lodash'; +import { + selectResultsAll, + selectResultsById, + selectResultsEntities, +} from './resultsSlice'; +import { + selectUploadsAll, + selectUploadsById, + selectUploadsEntities, +} from './uploadsSlice'; export const gallerySelector = (state: RootState) => state.gallery; @@ -75,3 +85,18 @@ export const hoverableImageSelector = createSelector( }, } ); + +export const selectedImageSelector = createSelector( + [gallerySelector, selectResultsEntities, selectUploadsEntities], + (gallery, allResults, allUploads) => { + const selectedImageName = gallery.selectedImageName; + + if (selectedImageName in allResults) { + return allResults[selectedImageName]; + } + + if (selectedImageName in allUploads) { + return allUploads[selectedImageName]; + } + } +); diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index dbb173c74a..ab5ac8e466 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -1,14 +1,17 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import * as InvokeAI from 'app/invokeai'; +import { invocationComplete } from 'services/events/actions'; import { InvokeTabName } from 'features/ui/store/tabMap'; import { IRect } from 'konva/lib/types'; import { clamp } from 'lodash'; +import { isImageOutput } from 'services/types/guards'; +import { imageUploaded } from 'services/thunks/image'; export type GalleryCategory = 'user' | 'result'; export type AddImagesPayload = { - images: Array; + images: Array; areMoreImagesAvailable: boolean; category: GalleryCategory; }; @@ -16,16 +19,33 @@ export type AddImagesPayload = { type GalleryImageObjectFitType = 'contain' | 'cover'; export type Gallery = { - images: InvokeAI.Image[]; + images: InvokeAI._Image[]; latest_mtime?: number; earliest_mtime?: number; areMoreImagesAvailable: boolean; }; export interface GalleryState { - currentImage?: InvokeAI.Image; + /** + * The selected image's unique name + * Use `selectedImageSelector` to access the image + */ + selectedImageName: string; + /** + * The currently selected image + * @deprecated See `state.gallery.selectedImageName` + */ + currentImage?: InvokeAI._Image; + /** + * The currently selected image's uuid. + * @deprecated See `state.gallery.selectedImageName`, use `selectedImageSelector` to access the image + */ currentImageUuid: string; - intermediateImage?: InvokeAI.Image & { + /** + * The current progress image + * @deprecated See `state.system.progressImage` + */ + intermediateImage?: InvokeAI._Image & { boundingBox?: IRect; generationMode?: InvokeTabName; }; @@ -42,6 +62,7 @@ export interface GalleryState { } const initialState: GalleryState = { + selectedImageName: '', currentImageUuid: '', galleryImageMinimumWidth: 64, galleryImageObjectFit: 'cover', @@ -69,7 +90,10 @@ export const gallerySlice = createSlice({ name: 'gallery', initialState, reducers: { - setCurrentImage: (state, action: PayloadAction) => { + imageSelected: (state, action: PayloadAction) => { + state.selectedImageName = action.payload; + }, + setCurrentImage: (state, action: PayloadAction) => { state.currentImage = action.payload; state.currentImageUuid = action.payload.uuid; }, @@ -124,7 +148,7 @@ export const gallerySlice = createSlice({ addImage: ( state, action: PayloadAction<{ - image: InvokeAI.Image; + image: InvokeAI._Image; category: GalleryCategory; }> ) => { @@ -150,7 +174,10 @@ export const gallerySlice = createSlice({ setIntermediateImage: ( state, action: PayloadAction< - InvokeAI.Image & { boundingBox?: IRect; generationMode?: InvokeTabName } + InvokeAI._Image & { + boundingBox?: IRect; + generationMode?: InvokeTabName; + } > ) => { state.intermediateImage = action.payload; @@ -252,9 +279,31 @@ export const gallerySlice = createSlice({ state.shouldUseSingleGalleryColumn = action.payload; }, }, + extraReducers(builder) { + /** + * Invocation Complete + */ + builder.addCase(invocationComplete, (state, action) => { + const { data } = action.payload; + if (isImageOutput(data.result)) { + state.selectedImageName = data.result.image.image_name; + state.intermediateImage = undefined; + } + }); + + /** + * Upload Image - FULFILLED + */ + builder.addCase(imageUploaded.fulfilled, (state, action) => { + const { location } = action.payload; + const imageName = location.split('/').pop() || ''; + state.selectedImageName = imageName; + }); + }, }); export const { + imageSelected, addImage, clearIntermediateImage, removeImage, diff --git a/invokeai/frontend/web/src/features/gallery/store/resultsPersistBlacklist.ts b/invokeai/frontend/web/src/features/gallery/store/resultsPersistBlacklist.ts new file mode 100644 index 0000000000..bd246865fb --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/store/resultsPersistBlacklist.ts @@ -0,0 +1,12 @@ +import { ResultsState } from './resultsSlice'; + +/** + * Results slice persist blacklist + * + * Currently blacklisting results slice entirely, see persist config in store.ts + */ +const itemsToBlacklist: (keyof ResultsState)[] = []; + +export const resultsBlacklist = itemsToBlacklist.map( + (blacklistItem) => `results.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts new file mode 100644 index 0000000000..bb789a4a5f --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts @@ -0,0 +1,139 @@ +import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; +import { Image } from 'app/invokeai'; +import { invocationComplete } from 'services/events/actions'; + +import { RootState } from 'app/store'; +import { + receivedResultImagesPage, + IMAGES_PER_PAGE, +} from 'services/thunks/gallery'; +import { isImageOutput } from 'services/types/guards'; +import { + buildImageUrls, + extractTimestampFromImageName, +} from 'services/util/deserializeImageField'; +import { deserializeImageResponse } from 'services/util/deserializeImageResponse'; + +// use `createEntityAdapter` to create a slice for results images +// https://redux-toolkit.js.org/api/createEntityAdapter#overview + +// the "Entity" is InvokeAI.ResultImage, while the "entities" are instances of that type +export const resultsAdapter = createEntityAdapter({ + // Provide a callback to get a stable, unique identifier for each entity. This defaults to + // `(item) => item.id`, but for our result images, the `name` is the unique identifier. + selectId: (image) => image.name, + // Order all images by their time (in descending order) + sortComparer: (a, b) => b.metadata.created - a.metadata.created, +}); + +// This type is intersected with the Entity type to create the shape of the state +type AdditionalResultsState = { + // these are a bit misleading; they refer to sessions, not results, but we don't have a route + // to list all images directly at this time... + page: number; // current page we are on + pages: number; // the total number of pages available + isLoading: boolean; // whether we are loading more images or not, mostly a placeholder + nextPage: number; // the next page to request +}; + +export const initialResultsState = + resultsAdapter.getInitialState({ + // provide the additional initial state + page: 0, + pages: 0, + isLoading: false, + nextPage: 0, + }); + +export type ResultsState = typeof initialResultsState; + +const resultsSlice = createSlice({ + name: 'results', + initialState: initialResultsState, + reducers: { + // the adapter provides some helper reducers; see the docs for all of them + // can use them as helper functions within a reducer, or use the function itself as a reducer + + // here we just use the function itself as the reducer. we'll call this on `invocation_complete` + // to add a single result + resultAdded: resultsAdapter.upsertOne, + }, + extraReducers: (builder) => { + // here we can respond to a fulfilled call of the `getNextResultsPage` thunk + // because we pass in the fulfilled thunk action creator, everything is typed + + /** + * Received Result Images Page - PENDING + */ + builder.addCase(receivedResultImagesPage.pending, (state) => { + state.isLoading = true; + }); + + /** + * Received Result Images Page - FULFILLED + */ + builder.addCase(receivedResultImagesPage.fulfilled, (state, action) => { + const { items, page, pages } = action.payload; + + const resultImages = items.map((image) => + deserializeImageResponse(image) + ); + + // use the adapter reducer to append all the results to state + resultsAdapter.addMany(state, resultImages); + + state.page = page; + state.pages = pages; + state.nextPage = items.length < IMAGES_PER_PAGE ? page : page + 1; + state.isLoading = false; + }); + + /** + * Invocation Complete + */ + builder.addCase(invocationComplete, (state, action) => { + const { data } = action.payload; + const { result, node, graph_execution_state_id } = data; + + if (isImageOutput(result)) { + const name = result.image.image_name; + const type = result.image.image_type; + const { url, thumbnail } = buildImageUrls(type, name); + + const timestamp = extractTimestampFromImageName(name); + + const image: Image = { + name, + type, + url, + thumbnail, + metadata: { + created: timestamp, + width: result.width, // TODO: add tese dimensions + height: result.height, + invokeai: { + session_id: graph_execution_state_id, + ...(node ? { node } : {}), + }, + }, + }; + + resultsAdapter.addOne(state, image); + } + }); + }, +}); + +// Create a set of memoized selectors based on the location of this entity state +// to be used as selectors in a `useAppSelector()` call +export const { + selectAll: selectResultsAll, + selectById: selectResultsById, + selectEntities: selectResultsEntities, + selectIds: selectResultsIds, + selectTotal: selectResultsTotal, +} = resultsAdapter.getSelectors((state) => state.results); + +export const { resultAdded } = resultsSlice.actions; + +export default resultsSlice.reducer; diff --git a/invokeai/frontend/web/src/features/gallery/store/thunks/uploadImage.ts b/invokeai/frontend/web/src/features/gallery/store/thunks/uploadImage.ts deleted file mode 100644 index 7f28928987..0000000000 --- a/invokeai/frontend/web/src/features/gallery/store/thunks/uploadImage.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { AnyAction, ThunkAction } from '@reduxjs/toolkit'; -import * as InvokeAI from 'app/invokeai'; -import { RootState } from 'app/store'; -import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; -import { setInitialImage } from 'features/parameters/store/generationSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { v4 as uuidv4 } from 'uuid'; -import { addImage } from '../gallerySlice'; - -type UploadImageConfig = { - imageFile: File; -}; - -export const uploadImage = - ( - config: UploadImageConfig - ): ThunkAction => - async (dispatch, getState) => { - const { imageFile } = config; - - const state = getState() as RootState; - - const activeTabName = activeTabNameSelector(state); - - const formData = new FormData(); - - formData.append('file', imageFile, imageFile.name); - formData.append( - 'data', - JSON.stringify({ - kind: 'init', - }) - ); - - const response = await fetch(`${window.location.origin}/upload`, { - method: 'POST', - body: formData, - }); - - const image = (await response.json()) as InvokeAI.ImageUploadResponse; - const newImage: InvokeAI.Image = { - uuid: uuidv4(), - category: 'user', - ...image, - }; - - dispatch(addImage({ image: newImage, category: 'user' })); - - if (activeTabName === 'unifiedCanvas') { - dispatch(setInitialCanvasImage(newImage)); - } else if (activeTabName === 'img2img') { - dispatch(setInitialImage(newImage)); - } - }; diff --git a/invokeai/frontend/web/src/features/gallery/store/uploadsPersistBlacklist.ts b/invokeai/frontend/web/src/features/gallery/store/uploadsPersistBlacklist.ts new file mode 100644 index 0000000000..4159d37184 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/store/uploadsPersistBlacklist.ts @@ -0,0 +1,12 @@ +import { UploadsState } from './uploadsSlice'; + +/** + * Uploads slice persist blacklist + * + * Currently blacklisting uploads slice entirely, see persist config in store.ts + */ +const itemsToBlacklist: (keyof UploadsState)[] = []; + +export const uploadsBlacklist = itemsToBlacklist.map( + (blacklistItem) => `uploads.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts new file mode 100644 index 0000000000..224d4c2335 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts @@ -0,0 +1,87 @@ +import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; +import { Image } from 'app/invokeai'; + +import { RootState } from 'app/store'; +import { + receivedUploadImagesPage, + IMAGES_PER_PAGE, +} from 'services/thunks/gallery'; +import { imageUploaded } from 'services/thunks/image'; +import { deserializeImageResponse } from 'services/util/deserializeImageResponse'; + +export const uploadsAdapter = createEntityAdapter({ + selectId: (image) => image.name, + sortComparer: (a, b) => b.metadata.created - a.metadata.created, +}); + +type AdditionalUploadsState = { + page: number; + pages: number; + isLoading: boolean; + nextPage: number; +}; + +const initialUploadsState = + uploadsAdapter.getInitialState({ + page: 0, + pages: 0, + nextPage: 0, + isLoading: false, + }); + +export type UploadsState = typeof initialUploadsState; + +const uploadsSlice = createSlice({ + name: 'uploads', + initialState: initialUploadsState, + reducers: { + uploadAdded: uploadsAdapter.addOne, + }, + extraReducers: (builder) => { + /** + * Received Upload Images Page - PENDING + */ + builder.addCase(receivedUploadImagesPage.pending, (state) => { + state.isLoading = true; + }); + + /** + * Received Upload Images Page - FULFILLED + */ + builder.addCase(receivedUploadImagesPage.fulfilled, (state, action) => { + const { items, page, pages } = action.payload; + + const images = items.map((image) => deserializeImageResponse(image)); + + uploadsAdapter.addMany(state, images); + + state.page = page; + state.pages = pages; + state.nextPage = items.length < IMAGES_PER_PAGE ? page : page + 1; + state.isLoading = false; + }); + + /** + * Upload Image - FULFILLED + */ + builder.addCase(imageUploaded.fulfilled, (state, action) => { + const { location, response } = action.payload; + + const uploadedImage = deserializeImageResponse(response); + + uploadsAdapter.addOne(state, uploadedImage); + }); + }, +}); + +export const { + selectAll: selectUploadsAll, + selectById: selectUploadsById, + selectEntities: selectUploadsEntities, + selectIds: selectUploadsIds, + selectTotal: selectUploadsTotal, +} = uploadsAdapter.getSelectors((state) => state.uploads); + +export const { uploadAdded } = uploadsSlice.actions; + +export default uploadsSlice.reducer; diff --git a/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx b/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx index 0658fd1756..660b07d75f 100644 --- a/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx +++ b/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; import { TransformComponent, useTransformContext } from 'react-zoom-pan-pinch'; import * as InvokeAI from 'app/invokeai'; +import { useGetUrl } from 'common/util/getUrl'; type ReactPanZoomProps = { - image: InvokeAI.Image; + image: InvokeAI._Image; styleClass?: string; alt?: string; ref?: React.Ref; @@ -22,6 +23,7 @@ export default function ReactPanZoomImage({ scaleY, }: ReactPanZoomProps) { const { centerView } = useTransformContext(); + const { getUrl } = useGetUrl(); return ( `lightbox.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx b/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx new file mode 100644 index 0000000000..ee6db90ec1 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx @@ -0,0 +1,63 @@ +import { v4 as uuidv4 } from 'uuid'; + +import 'reactflow/dist/style.css'; +import { useCallback } from 'react'; +import { + Tooltip, + Menu, + MenuButton, + MenuList, + MenuItem, + IconButton, +} from '@chakra-ui/react'; +import { FaPlus } from 'react-icons/fa'; +import { useAppDispatch, useAppSelector } from 'app/storeHooks'; +import { nodeAdded } from '../store/nodesSlice'; +import { cloneDeep, map } from 'lodash'; +import { RootState } from 'app/store'; +import { useBuildInvocation } from '../hooks/useBuildInvocation'; +import { addToast } from 'features/system/store/systemSlice'; +import { makeToast } from 'features/system/hooks/useToastWatcher'; + +export const AddNodeMenu = () => { + const dispatch = useAppDispatch(); + + const invocationTemplates = useAppSelector( + (state: RootState) => state.nodes.invocationTemplates + ); + + const buildInvocation = useBuildInvocation(); + + const addNode = useCallback( + (nodeType: string) => { + const invocation = buildInvocation(nodeType); + + if (!invocation) { + const toast = makeToast({ + status: 'error', + title: `Unknown Invocation type ${nodeType}`, + }); + dispatch(addToast(toast)); + return; + } + + dispatch(nodeAdded(invocation)); + }, + [dispatch, buildInvocation] + ); + + return ( + + } /> + + {map(invocationTemplates, ({ title, description, type }, key) => { + return ( + + addNode(type)}>{title} + + ); + })} + + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx b/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx new file mode 100644 index 0000000000..cc5b430382 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx @@ -0,0 +1,69 @@ +import { Tooltip } from '@chakra-ui/react'; +import { CSSProperties, useMemo } from 'react'; +import { + Handle, + Position, + Connection, + HandleType, + useReactFlow, +} from 'reactflow'; +import { FIELDS, HANDLE_TOOLTIP_OPEN_DELAY } from '../types/constants'; +// import { useConnectionEventStyles } from '../hooks/useConnectionEventStyles'; +import { InputFieldTemplate, OutputFieldTemplate } from '../types/types'; + +const handleBaseStyles: CSSProperties = { + position: 'absolute', + width: '1rem', + height: '1rem', + borderWidth: 0, +}; + +const inputHandleStyles: CSSProperties = { + left: '-1.7rem', +}; + +const outputHandleStyles: CSSProperties = { + right: '-1.7rem', +}; + +const requiredConnectionStyles: CSSProperties = { + boxShadow: '0 0 0.5rem 0.5rem var(--invokeai-colors-error-400)', +}; + +type FieldHandleProps = { + nodeId: string; + field: InputFieldTemplate | OutputFieldTemplate; + isValidConnection: (connection: Connection) => boolean; + handleType: HandleType; + styles?: CSSProperties; +}; + +export const FieldHandle = (props: FieldHandleProps) => { + const { nodeId, field, isValidConnection, handleType, styles } = props; + const { name, title, type, description } = field; + + return ( + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx b/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx new file mode 100644 index 0000000000..a420376016 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx @@ -0,0 +1,18 @@ +import 'reactflow/dist/style.css'; +import { Tooltip, Badge, HStack } from '@chakra-ui/react'; +import { map } from 'lodash'; +import { FIELDS } from '../types/constants'; + +export const FieldTypeLegend = () => { + return ( + + {map(FIELDS, ({ title, description, color }, key) => ( + + + {title} + + + ))} + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx new file mode 100644 index 0000000000..ad21e92b6d --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx @@ -0,0 +1,104 @@ +import { + Background, + Controls, + MiniMap, + OnConnect, + OnEdgesChange, + OnNodesChange, + ReactFlow, + ConnectionLineType, + OnConnectStart, + OnConnectEnd, + Panel, +} from 'reactflow'; +import { useAppDispatch, useAppSelector } from 'app/storeHooks'; +import { RootState } from 'app/store'; +import { + connectionEnded, + connectionMade, + connectionStarted, + edgesChanged, + nodesChanged, +} from '../store/nodesSlice'; +import { useCallback } from 'react'; +import { InvocationComponent } from './InvocationComponent'; +import { AddNodeMenu } from './AddNodeMenu'; +import { FieldTypeLegend } from './FieldTypeLegend'; +import { Button } from '@chakra-ui/react'; +import { nodesGraphBuilt } from 'services/thunks/session'; + +const nodeTypes = { invocation: InvocationComponent }; + +export const Flow = () => { + const dispatch = useAppDispatch(); + const nodes = useAppSelector((state: RootState) => state.nodes.nodes); + const edges = useAppSelector((state: RootState) => state.nodes.edges); + + const onNodesChange: OnNodesChange = useCallback( + (changes) => { + dispatch(nodesChanged(changes)); + }, + [dispatch] + ); + + const onEdgesChange: OnEdgesChange = useCallback( + (changes) => { + dispatch(edgesChanged(changes)); + }, + [dispatch] + ); + + const onConnectStart: OnConnectStart = useCallback( + (event, params) => { + dispatch(connectionStarted(params)); + }, + [dispatch] + ); + + const onConnect: OnConnect = useCallback( + (connection) => { + dispatch(connectionMade(connection)); + }, + [dispatch] + ); + + const onConnectEnd: OnConnectEnd = useCallback( + (event) => { + dispatch(connectionEnded()); + }, + [dispatch] + ); + + const handleInvoke = useCallback(() => { + dispatch(nodesGraphBuilt()); + }, [dispatch]); + + return ( + + + + + + + + + + + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx new file mode 100644 index 0000000000..58ca432ffc --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx @@ -0,0 +1,107 @@ +import { Box } from '@chakra-ui/react'; +import { InputFieldTemplate, InputFieldValue } from '../types/types'; +import { ArrayInputFieldComponent } from './fields/ArrayInputField.tsx'; +import { BooleanInputFieldComponent } from './fields/BooleanInputFieldComponent'; +import { EnumInputFieldComponent } from './fields/EnumInputFieldComponent'; +import { ImageInputFieldComponent } from './fields/ImageInputFieldComponent'; +import { LatentsInputFieldComponent } from './fields/LatentsInputFieldComponent'; +import { ModelInputFieldComponent } from './fields/ModelInputFieldComponent'; +import { NumberInputFieldComponent } from './fields/NumberInputFieldComponent'; +import { StringInputFieldComponent } from './fields/StringInputFieldComponent'; + +type InputFieldComponentProps = { + nodeId: string; + field: InputFieldValue; + template: InputFieldTemplate; +}; + +// build an individual input element based on the schema +export const InputFieldComponent = (props: InputFieldComponentProps) => { + const { nodeId, field, template } = props; + const { type, value } = field; + + if (type === 'string' && template.type === 'string') { + return ( + + ); + } + + if (type === 'boolean' && template.type === 'boolean') { + return ( + + ); + } + + if ( + (type === 'integer' && template.type === 'integer') || + (type === 'float' && template.type === 'float') + ) { + return ( + + ); + } + + if (type === 'enum' && template.type === 'enum') { + return ( + + ); + } + + if (type === 'image' && template.type === 'image') { + return ( + + ); + } + + if (type === 'latents' && template.type === 'latents') { + return ( + + ); + } + + if (type === 'model' && template.type === 'model') { + return ( + + ); + } + + if (type === 'array' && template.type === 'array') { + return ( + + ); + } + + return Unknown field type: {type}; +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx new file mode 100644 index 0000000000..5f06ee9352 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx @@ -0,0 +1,243 @@ +import { NodeProps, useReactFlow } from 'reactflow'; +import { + Box, + Flex, + FormControl, + FormLabel, + Heading, + HStack, + Tooltip, + Icon, + Code, + Text, +} from '@chakra-ui/react'; +import { FaExclamationCircle, FaInfoCircle } from 'react-icons/fa'; +import { InvocationValue } from '../types/types'; +import { InputFieldComponent } from './InputFieldComponent'; +import { FieldHandle } from './FieldHandle'; +import { isEqual, map, size } from 'lodash'; +import { memo, useMemo, useRef } from 'react'; +import { useIsValidConnection } from '../hooks/useIsValidConnection'; +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from 'app/store'; +import { useAppSelector } from 'app/storeHooks'; +import { useGetInvocationTemplate } from '../hooks/useInvocationTemplate'; + +const connectedInputFieldsSelector = createSelector( + [(state: RootState) => state.nodes.edges], + (edges) => { + // return edges.map((e) => e.targetHandle); + return edges; + }, + { + memoizeOptions: { + resultEqualityCheck: isEqual, + }, + } +); + +export const InvocationComponent = memo((props: NodeProps) => { + const { id: nodeId, data, selected } = props; + const { type, inputs, outputs } = data; + + const isValidConnection = useIsValidConnection(); + + const connectedInputs = useAppSelector(connectedInputFieldsSelector); + const getInvocationTemplate = useGetInvocationTemplate(); + // TODO: determine if a field/handle is connected and disable the input if so + + const template = useRef(getInvocationTemplate(type)); + + if (!template.current) { + return ( + + + + + + ); + } + + return ( + + + <> + {nodeId} + + + {template.current.title} + + + + + + {map(inputs, (input, i) => { + const { id: fieldId } = input; + const inputTemplate = template.current?.inputs[input.name]; + + if (!inputTemplate) { + return ( + + + + Unknown input: {input.name} + + + + ); + } + + const isConnected = Boolean( + connectedInputs.filter((connectedInput) => { + return ( + connectedInput.target === nodeId && + connectedInput.targetHandle === input.name + ); + }).length + ); + + return ( + + + + {inputTemplate?.title} + + + + + + + {!['never', 'directOnly'].includes( + inputTemplate?.inputRequirement ?? '' + ) && ( + + )} + + ); + })} + {map(outputs).map((output, i) => { + const outputTemplate = template.current?.outputs[output.name]; + + const isConnected = Boolean( + connectedInputs.filter((connectedInput) => { + return ( + connectedInput.source === nodeId && + connectedInput.sourceHandle === output.name + ); + }).length + ); + + if (!outputTemplate) { + return ( + + + + Unknown output: {output.name} + + + + ); + } + + return ( + + + + {outputTemplate?.title} Output + + + + + ); + })} + + + + + ); +}); + +InvocationComponent.displayName = 'InvocationComponent'; diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx new file mode 100644 index 0000000000..98e3b2d19a --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx @@ -0,0 +1,46 @@ +import 'reactflow/dist/style.css'; +import { Box } from '@chakra-ui/react'; +import { ReactFlowProvider } from 'reactflow'; + +import { Flow } from './Flow'; +import { useAppSelector } from 'app/storeHooks'; +import { RootState } from 'app/store'; +import { buildNodesGraph } from '../util/nodesGraphBuilder/buildNodesGraph'; + +const NodeEditor = () => { + const state = useAppSelector((state: RootState) => state); + + const graph = buildNodesGraph(state); + + return ( + + + + + + {JSON.stringify(graph, null, 2)} + + + ); +}; + +export default NodeEditor; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputField.tsx.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputField.tsx.tsx new file mode 100644 index 0000000000..d9717f14a4 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputField.tsx.tsx @@ -0,0 +1,14 @@ +import { + ArrayInputFieldTemplate, + ArrayInputFieldValue, +} from 'features/nodes/types'; +import { FaImage, FaList } from 'react-icons/fa'; +import { FieldComponentProps } from './types'; + +export const ArrayInputFieldComponent = ( + props: FieldComponentProps +) => { + const { nodeId, field } = props; + + return ; +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx new file mode 100644 index 0000000000..f9fe404f82 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx @@ -0,0 +1,31 @@ +import { Switch } from '@chakra-ui/react'; +import { useAppDispatch } from 'app/storeHooks'; +import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; +import { + BooleanInputFieldTemplate, + BooleanInputFieldValue, +} from 'features/nodes/types'; +import { ChangeEvent } from 'react'; +import { FieldComponentProps } from './types'; + +export const BooleanInputFieldComponent = ( + props: FieldComponentProps +) => { + const { nodeId, field } = props; + + const dispatch = useAppDispatch(); + + const handleValueChanged = (e: ChangeEvent) => { + dispatch( + fieldValueChanged({ + nodeId, + fieldName: field.name, + value: e.target.checked, + }) + ); + }; + + return ( + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx new file mode 100644 index 0000000000..8de8e17484 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx @@ -0,0 +1,35 @@ +import { Select } from '@chakra-ui/react'; +import { useAppDispatch } from 'app/storeHooks'; +import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; +import { + EnumInputFieldTemplate, + EnumInputFieldValue, +} from 'features/nodes/types'; +import { ChangeEvent } from 'react'; +import { FieldComponentProps } from './types'; + +export const EnumInputFieldComponent = ( + props: FieldComponentProps +) => { + const { nodeId, field, template } = props; + + const dispatch = useAppDispatch(); + + const handleValueChanged = (e: ChangeEvent) => { + dispatch( + fieldValueChanged({ + nodeId, + fieldName: field.name, + value: e.target.value, + }) + ); + }; + + return ( + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx new file mode 100644 index 0000000000..599fa61e38 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx @@ -0,0 +1,64 @@ +import { Box, Image, Icon, Flex } from '@chakra-ui/react'; +import { useAppDispatch } from 'app/storeHooks'; +import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder'; +import { useGetUrl } from 'common/util/getUrl'; +import useGetImageByNameAndType from 'features/gallery/hooks/useGetImageByName'; +import useGetImageByUuid from 'features/gallery/hooks/useGetImageByUuid'; +import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; +import { + ImageInputFieldTemplate, + ImageInputFieldValue, +} from 'features/nodes/types'; +import { DragEvent, useCallback, useState } from 'react'; +import { FaImage } from 'react-icons/fa'; +import { ImageType } from 'services/api'; +import { FieldComponentProps } from './types'; + +export const ImageInputFieldComponent = ( + props: FieldComponentProps +) => { + const { nodeId, field } = props; + const { value } = field; + + const getImageByNameAndType = useGetImageByNameAndType(); + const dispatch = useAppDispatch(); + const [url, setUrl] = useState(); + const { getUrl } = useGetUrl(); + + const handleDrop = useCallback( + (e: DragEvent) => { + const name = e.dataTransfer.getData('invokeai/imageName'); + const type = e.dataTransfer.getData('invokeai/imageType') as ImageType; + + if (!name || !type) { + return; + } + + const image = getImageByNameAndType(name, type); + + if (!image) { + return; + } + + setUrl(image.url); + + dispatch( + fieldValueChanged({ + nodeId, + fieldName: field.name, + value: { + image_name: name, + image_type: type, + }, + }) + ); + }, + [getImageByNameAndType, dispatch, field.name, nodeId] + ); + + return ( + + } /> + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx new file mode 100644 index 0000000000..ed053aab7c --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx @@ -0,0 +1,13 @@ +import { + LatentsInputFieldTemplate, + LatentsInputFieldValue, +} from 'features/nodes/types'; +import { FieldComponentProps } from './types'; + +export const LatentsInputFieldComponent = ( + props: FieldComponentProps +) => { + const { nodeId, field } = props; + + return null; +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx new file mode 100644 index 0000000000..5aaf83a186 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx @@ -0,0 +1,57 @@ +import { Select } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from 'app/store'; +import { useAppDispatch, useAppSelector } from 'app/storeHooks'; +import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; +import { + ModelInputFieldTemplate, + ModelInputFieldValue, +} from 'features/nodes/types'; +import { + selectModelsById, + selectModelsIds, +} from 'features/system/store/modelSlice'; +import { isEqual, map } from 'lodash'; +import { ChangeEvent } from 'react'; +import { FieldComponentProps } from './types'; + +const availableModelsSelector = createSelector( + [selectModelsIds], + (allModelNames) => { + return { allModelNames }; + // return map(modelList, (_, name) => name); + }, + { + memoizeOptions: { + resultEqualityCheck: isEqual, + }, + } +); + +export const ModelInputFieldComponent = ( + props: FieldComponentProps +) => { + const { nodeId, field } = props; + + const dispatch = useAppDispatch(); + + const { allModelNames } = useAppSelector(availableModelsSelector); + + const handleValueChanged = (e: ChangeEvent) => { + dispatch( + fieldValueChanged({ + nodeId, + fieldName: field.name, + value: e.target.value, + }) + ); + }; + + return ( + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx new file mode 100644 index 0000000000..57b8527e00 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx @@ -0,0 +1,41 @@ +import { + NumberDecrementStepper, + NumberIncrementStepper, + NumberInput, + NumberInputField, + NumberInputStepper, +} from '@chakra-ui/react'; +import { useAppDispatch } from 'app/storeHooks'; +import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; +import { + FloatInputFieldTemplate, + FloatInputFieldValue, + IntegerInputFieldTemplate, + IntegerInputFieldValue, +} from 'features/nodes/types'; +import { FieldComponentProps } from './types'; + +export const NumberInputFieldComponent = ( + props: FieldComponentProps< + IntegerInputFieldValue | FloatInputFieldValue, + IntegerInputFieldTemplate | FloatInputFieldTemplate + > +) => { + const { nodeId, field } = props; + + const dispatch = useAppDispatch(); + + const handleValueChanged = (_: string, value: number) => { + dispatch(fieldValueChanged({ nodeId, fieldName: field.name, value })); + }; + + return ( + + + + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx new file mode 100644 index 0000000000..7ed3b5d435 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx @@ -0,0 +1,29 @@ +import { Input } from '@chakra-ui/react'; +import { useAppDispatch } from 'app/storeHooks'; +import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; +import { + StringInputFieldTemplate, + StringInputFieldValue, +} from 'features/nodes/types'; +import { ChangeEvent } from 'react'; +import { FieldComponentProps } from './types'; + +export const StringInputFieldComponent = ( + props: FieldComponentProps +) => { + const { nodeId, field } = props; + + const dispatch = useAppDispatch(); + + const handleValueChanged = (e: ChangeEvent) => { + dispatch( + fieldValueChanged({ + nodeId, + fieldName: field.name, + value: e.target.value, + }) + ); + }; + + return ; +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/types.ts b/invokeai/frontend/web/src/features/nodes/components/fields/types.ts new file mode 100644 index 0000000000..fb7e73ad92 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/types.ts @@ -0,0 +1,10 @@ +import { InputFieldTemplate, InputFieldValue } from 'features/nodes/types'; + +export type FieldComponentProps< + V extends InputFieldValue, + T extends InputFieldTemplate +> = { + nodeId: string; + field: V; + template: T; +}; diff --git a/invokeai/frontend/web/src/features/nodes/examples/iterationGraph.ts b/invokeai/frontend/web/src/features/nodes/examples/iterationGraph.ts new file mode 100644 index 0000000000..46ee5289b2 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/examples/iterationGraph.ts @@ -0,0 +1,52 @@ +export const iterationGraph = { + nodes: { + '0': { + id: '0', + type: 'range', + start: 0, + stop: 5, + step: 1, + }, + '1': { + collection: [], + id: '1', + index: 0, + type: 'iterate', + }, + '2': { + cfg_scale: 7.5, + height: 512, + id: '2', + model: '', + progress_images: false, + prompt: 'dog', + sampler_name: 'k_lms', + seamless: false, + steps: 11, + type: 'txt2img', + width: 512, + }, + }, + edges: [ + { + source: { + field: 'collection', + node_id: '0', + }, + destination: { + field: 'collection', + node_id: '1', + }, + }, + { + source: { + field: 'item', + node_id: '1', + }, + destination: { + field: 'seed', + node_id: '2', + }, + }, + ], +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts b/invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts new file mode 100644 index 0000000000..19eeac8378 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts @@ -0,0 +1,78 @@ +import { RootState } from 'app/store'; +import { useAppSelector } from 'app/storeHooks'; +import { reduce } from 'lodash'; +import { Node } from 'reactflow'; +import { AnyInvocationType } from 'services/events/types'; +import { v4 as uuidv4 } from 'uuid'; +import { + InputFieldValue, + InvocationValue, + OutputFieldValue, +} from '../types/types'; +import { buildInputFieldValue } from '../util/fieldValueBuilders'; + +export const useBuildInvocation = () => { + const invocationTemplates = useAppSelector( + (state: RootState) => state.nodes.invocationTemplates + ); + + return (type: AnyInvocationType) => { + const template = invocationTemplates[type]; + + if (template === undefined) { + console.error(`Unable to find template ${type}.`); + return; + } + + const nodeId = uuidv4(); + + const inputs = reduce( + template.inputs, + (inputsAccumulator, inputTemplate, inputName) => { + const fieldId = uuidv4(); + + const inputFieldValue: InputFieldValue = buildInputFieldValue( + fieldId, + inputTemplate + ); + + inputsAccumulator[inputName] = inputFieldValue; + + return inputsAccumulator; + }, + {} as Record + ); + + const outputs = reduce( + template.outputs, + (outputsAccumulator, outputTemplate, outputName) => { + const fieldId = uuidv4(); + + const outputFieldValue: OutputFieldValue = { + id: fieldId, + name: outputName, + type: outputTemplate.type, + }; + + outputsAccumulator[outputName] = outputFieldValue; + + return outputsAccumulator; + }, + {} as Record + ); + + const invocation: Node = { + id: nodeId, + type: 'invocation', + position: { x: 0, y: 0 }, + data: { + id: nodeId, + type, + inputs, + outputs, + }, + }; + + return invocation; + }; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInvocationTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInvocationTemplate.ts new file mode 100644 index 0000000000..f58e82b897 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useInvocationTemplate.ts @@ -0,0 +1,16 @@ +import { useAppSelector } from 'app/storeHooks'; +import { invocationTemplatesSelector } from '../store/selectors/invocationTemplatesSelector'; + +export const useGetInvocationTemplate = () => { + const invocationTemplates = useAppSelector(invocationTemplatesSelector); + + return (invocationType: string) => { + const template = invocationTemplates[invocationType]; + + if (!template) { + return; + } + + return template; + }; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts new file mode 100644 index 0000000000..dec9120d08 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts @@ -0,0 +1,93 @@ +import { useCallback } from 'react'; +import { Connection, Node, useReactFlow } from 'reactflow'; +import graphlib from '@dagrejs/graphlib'; +import { InvocationValue } from '../types/types'; + +export const useIsValidConnection = () => { + const flow = useReactFlow(); + + // Check if an in-progress connection is valid + const isValidConnection = useCallback( + ({ source, sourceHandle, target, targetHandle }: Connection): boolean => { + const edges = flow.getEdges(); + const nodes = flow.getNodes(); + + return true; + + // Connection must have valid targets + if (!(source && sourceHandle && target && targetHandle)) { + return false; + } + + // Connection is invalid if target already has a connection + if ( + edges.find((edge) => { + return edge.target === target && edge.targetHandle === targetHandle; + }) + ) { + return false; + } + + // Find the source and target nodes + const sourceNode = flow.getNode(source) as Node; + + const targetNode = flow.getNode(target) as Node; + + // Conditional guards against undefined nodes/handles + if (!(sourceNode && targetNode && sourceNode.data && targetNode.data)) { + return false; + } + + // Connection types must be the same for a connection + if ( + sourceNode.data.outputs[sourceHandle].type !== + targetNode.data.inputs[targetHandle].type + ) { + return false; + } + + // Graphs much be acyclic (no loops!) + + /** + * TODO: use `graphlib.alg.findCycles()` to identify strong connections + * + * this validation func only runs when the cursor hits the second handle of the connection, + * and only on that second handle - so it cannot tell us exhaustively which connections + * are valid. + * + * ideally, we check when the connection starts to calculate all invalid handles at once. + * + * requires making a new graphlib graph - and calling `findCycles()` - for each potential + * handle. instead of using the `isValidConnection` prop, it would use the `onConnectStart` + * prop. + * + * the strong connections should be stored in global state. + * + * then, `isValidConnection` would simple loop through the strong connections and if the + * source and target are in a single strong connection, return false. + * + * and also, we can use this knowledge to style every handle when a connection starts, + * which is otherwise not possible. + */ + + // build a graphlib graph + const g = new graphlib.Graph(); + + nodes.forEach((n) => { + g.setNode(n.id); + }); + + edges.forEach((e) => { + g.setEdge(e.source, e.target); + }); + + // Add the candidate edge to the graph + g.setEdge(source, target); + + return graphlib.alg.isAcyclic(g); + }, + [flow] + ); + + return isValidConnection; +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesPersistBlacklist.ts b/invokeai/frontend/web/src/features/nodes/store/nodesPersistBlacklist.ts new file mode 100644 index 0000000000..5648283ddd --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/nodesPersistBlacklist.ts @@ -0,0 +1,10 @@ +import { NodesState } from './nodesSlice'; + +/** + * Nodes slice persist blacklist + */ +const itemsToBlacklist: (keyof NodesState)[] = ['schema', 'invocations']; + +export const nodesBlacklist = itemsToBlacklist.map( + (blacklistItem) => `nodes.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts new file mode 100644 index 0000000000..1ce806de57 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -0,0 +1,103 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { OpenAPIV3 } from 'openapi-types'; +import { + addEdge, + applyEdgeChanges, + applyNodeChanges, + Connection, + Edge, + EdgeChange, + Node, + NodeChange, + OnConnectStartParams, +} from 'reactflow'; +import { Graph, ImageField } from 'services/api'; +import { receivedOpenAPISchema } from 'services/thunks/schema'; +import { isFulfilledAnyGraphBuilt } from 'services/thunks/session'; +import { InvocationTemplate, InvocationValue } from '../types/types'; +import { parseSchema } from '../util/parseSchema'; + +export type NodesState = { + nodes: Node[]; + edges: Edge[]; + schema: OpenAPIV3.Document | null; + invocationTemplates: Record; + connectionStartParams: OnConnectStartParams | null; + lastGraph: Graph | null; +}; + +export const initialNodesState: NodesState = { + nodes: [], + edges: [], + schema: null, + invocationTemplates: {}, + connectionStartParams: null, + lastGraph: null, +}; + +const nodesSlice = createSlice({ + name: 'nodes', + initialState: initialNodesState, + reducers: { + nodesChanged: (state, action: PayloadAction) => { + state.nodes = applyNodeChanges(action.payload, state.nodes); + }, + nodeAdded: (state, action: PayloadAction>) => { + state.nodes.push(action.payload); + }, + edgesChanged: (state, action: PayloadAction) => { + state.edges = applyEdgeChanges(action.payload, state.edges); + }, + connectionStarted: (state, action: PayloadAction) => { + state.connectionStartParams = action.payload; + }, + connectionMade: (state, action: PayloadAction) => { + state.edges = addEdge(action.payload, state.edges); + }, + connectionEnded: (state) => { + state.connectionStartParams = null; + }, + fieldValueChanged: ( + state, + action: PayloadAction<{ + nodeId: string; + fieldName: string; + value: + | string + | number + | boolean + | Pick + | undefined; + }> + ) => { + const { nodeId, fieldName, value } = action.payload; + const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId); + + if (nodeIndex > -1) { + state.nodes[nodeIndex].data.inputs[fieldName].value = value; + } + }, + }, + extraReducers(builder) { + builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => { + state.schema = action.payload; + state.invocationTemplates = parseSchema(action.payload); + }); + + builder.addMatcher(isFulfilledAnyGraphBuilt, (state, action) => { + state.lastGraph = action.payload; + }); + }, +}); + +export const { + nodesChanged, + edgesChanged, + nodeAdded, + fieldValueChanged, + connectionMade, + connectionStarted, + connectionEnded, +} = nodesSlice.actions; + +export default nodesSlice.reducer; diff --git a/invokeai/frontend/web/src/features/nodes/store/selectors/invocationTemplatesSelector.ts b/invokeai/frontend/web/src/features/nodes/store/selectors/invocationTemplatesSelector.ts new file mode 100644 index 0000000000..0ffc4ac5bb --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/selectors/invocationTemplatesSelector.ts @@ -0,0 +1,7 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from 'app/store'; + +export const invocationTemplatesSelector = createSelector( + (state: RootState) => state.nodes, + (nodes) => nodes.invocationTemplates +); diff --git a/invokeai/frontend/web/src/features/nodes/types/constants.ts b/invokeai/frontend/web/src/features/nodes/types/constants.ts new file mode 100644 index 0000000000..0fb8acf5bf --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/types/constants.ts @@ -0,0 +1,69 @@ +import { getCSSVar } from '@chakra-ui/utils'; +import { FieldType, FieldUIConfig } from './types'; + +export const HANDLE_TOOLTIP_OPEN_DELAY = 500; + +export const FIELD_TYPE_MAP: Record = { + integer: 'integer', + number: 'float', + string: 'string', + boolean: 'boolean', + enum: 'enum', + ImageField: 'image', + LatentsField: 'latents', + model: 'model', + array: 'array', +}; + +const COLOR_TOKEN_VALUE = 500; + +const getColorTokenCssVariable = (color: string) => + `var(--invokeai-colors-${color}-${COLOR_TOKEN_VALUE})`; + +export const FIELDS: Record = { + integer: { + colorCssVar: getColorTokenCssVariable('red'), + title: 'Integer', + description: 'Integers are whole numbers, without a decimal point.', + }, + float: { + colorCssVar: getColorTokenCssVariable('orange'), + title: 'Float', + description: 'Floats are numbers with a decimal point.', + }, + string: { + colorCssVar: getColorTokenCssVariable('yellow'), + title: 'String', + description: 'Strings are text.', + }, + boolean: { + colorCssVar: getColorTokenCssVariable('green'), + title: 'Boolean', + description: 'Booleans are true or false.', + }, + enum: { + colorCssVar: getColorTokenCssVariable('blue'), + title: 'Enum', + description: 'Enums are values that may be one of a number of options.', + }, + image: { + colorCssVar: getColorTokenCssVariable('purple'), + title: 'Image', + description: 'Images may be passed between nodes.', + }, + latents: { + colorCssVar: getColorTokenCssVariable('pink'), + title: 'Latents', + description: 'Latents may be passed between nodes.', + }, + model: { + colorCssVar: getColorTokenCssVariable('teal'), + title: 'Model', + description: 'Models are models.', + }, + array: { + colorCssVar: getColorTokenCssVariable('gray'), + title: 'Array', + description: 'TODO: Array type description.', + }, +}; diff --git a/invokeai/frontend/web/src/features/nodes/types/typeGuards.ts b/invokeai/frontend/web/src/features/nodes/types/typeGuards.ts new file mode 100644 index 0000000000..99c9a28150 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/types/typeGuards.ts @@ -0,0 +1,9 @@ +import { OpenAPIV3 } from 'openapi-types'; + +export const isReferenceObject = ( + obj: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject +): obj is OpenAPIV3.ReferenceObject => '$ref' in obj; + +export const isSchemaObject = ( + obj: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject +): obj is OpenAPIV3.SchemaObject => !('$ref' in obj); diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts new file mode 100644 index 0000000000..1f35712d39 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/types/types.ts @@ -0,0 +1,296 @@ +import { OpenAPIV3 } from 'openapi-types'; +import { ImageField } from 'services/api'; +import { AnyInvocationType } from 'services/events/types'; + +export type InvocationValue = { + id: string; + type: AnyInvocationType; + inputs: Record; + outputs: Record; +}; + +export type InvocationTemplate = { + /** + * Unique type of the invocation + */ + type: AnyInvocationType; + /** + * Display name of the invocation + */ + title: string; + /** + * Description of the invocation + */ + description: string; + /** + * Invocation tags + */ + tags: string[]; + /** + * Array of invocation inputs + */ + inputs: Record; + // inputs: InputField[]; + /** + * Array of the invocation outputs + */ + outputs: Record; + // outputs: OutputField[]; +}; + +export type FieldUIConfig = { + colorCssVar: string; + title: string; + description: string; +}; + +/** + * The valid invocation field types + */ +export type FieldType = + | 'integer' + | 'float' + | 'string' + | 'boolean' + | 'enum' + | 'image' + | 'latents' + | 'model' + | 'array'; + +/** + * An input field is persisted across reloads as part of the user's local state. + * + * An input field has three properties: + * - `id` a unique identifier + * - `name` the name of the field, which comes from the python dataclass + * - `value` the field's value + */ +export type InputFieldValue = + | IntegerInputFieldValue + | FloatInputFieldValue + | StringInputFieldValue + | BooleanInputFieldValue + | ImageInputFieldValue + | LatentsInputFieldValue + | EnumInputFieldValue + | ModelInputFieldValue + | ArrayInputFieldValue; + +/** + * An input field template is generated on each page load from the OpenAPI schema. + * + * The template provides the field type and other field metadata (e.g. title, description, + * maximum length, pattern to match, etc). + */ +export type InputFieldTemplate = + | IntegerInputFieldTemplate + | FloatInputFieldTemplate + | StringInputFieldTemplate + | BooleanInputFieldTemplate + | ImageInputFieldTemplate + | LatentsInputFieldTemplate + | EnumInputFieldTemplate + | ModelInputFieldTemplate + | ArrayInputFieldTemplate; + +/** + * An output field is persisted across as part of the user's local state. + * + * An output field has two properties: + * - `id` a unique identifier + * - `name` the name of the field, which comes from the python dataclass + */ +export type OutputFieldValue = FieldValueBase; + +/** + * An output field template is generated on each page load from the OpenAPI schema. + * + * The template provides the output field's name, type, title, and description. + */ +export type OutputFieldTemplate = { + name: string; + type: FieldType; + title: string; + description: string; +}; + +/** + * Indicates when/if this field needs an input. + */ +export type InputRequirement = 'always' | 'never' | 'optional'; + +/** + * Indicates the kind of input(s) this field may have. + */ +export type InputKind = 'connection' | 'direct' | 'any'; + +export type FieldValueBase = { + id: string; + name: string; + type: FieldType; +}; + +export type IntegerInputFieldValue = FieldValueBase & { + type: 'integer'; + value?: number; +}; + +export type FloatInputFieldValue = FieldValueBase & { + type: 'float'; + value?: number; +}; + +export type StringInputFieldValue = FieldValueBase & { + type: 'string'; + value?: string; +}; + +export type BooleanInputFieldValue = FieldValueBase & { + type: 'boolean'; + value?: boolean; +}; + +export type EnumInputFieldValue = FieldValueBase & { + type: 'enum'; + value?: number | string; +}; + +export type LatentsInputFieldValue = FieldValueBase & { + type: 'latents'; + value?: undefined; +}; + +export type ImageInputFieldValue = FieldValueBase & { + type: 'image'; + value?: Pick; +}; + +export type ModelInputFieldValue = FieldValueBase & { + type: 'model'; + value?: string; +}; + +export type ArrayInputFieldValue = FieldValueBase & { + type: 'array'; + value?: (string | number)[]; +}; + +export type InputFieldTemplateBase = { + name: string; + title: string; + description: string; + type: FieldType; + inputRequirement: InputRequirement; + inputKind: InputKind; +}; + +export type IntegerInputFieldTemplate = InputFieldTemplateBase & { + type: 'integer'; + default: number; + multipleOf?: number; + maximum?: number; + exclusiveMaximum?: boolean; + minimum?: number; + exclusiveMinimum?: boolean; +}; + +export type FloatInputFieldTemplate = InputFieldTemplateBase & { + type: 'float'; + default: number; + multipleOf?: number; + maximum?: number; + exclusiveMaximum?: boolean; + minimum?: number; + exclusiveMinimum?: boolean; +}; + +export type StringInputFieldTemplate = InputFieldTemplateBase & { + type: 'string'; + default: string; + maxLength?: number; + minLength?: number; + pattern?: string; +}; + +export type BooleanInputFieldTemplate = InputFieldTemplateBase & { + default: boolean; + type: 'boolean'; +}; + +export type ImageInputFieldTemplate = InputFieldTemplateBase & { + default: Pick; + type: 'image'; +}; + +export type LatentsInputFieldTemplate = InputFieldTemplateBase & { + default: undefined; + type: 'latents'; +}; + +export type EnumInputFieldTemplate = InputFieldTemplateBase & { + default: string | number; + type: 'enum'; + enumType: 'string' | 'number'; + options: Array; +}; + +export type ModelInputFieldTemplate = InputFieldTemplateBase & { + default: string; + type: 'model'; +}; + +export type ArrayInputFieldTemplate = InputFieldTemplateBase & { + default: (string | number)[]; + type: 'array'; +}; + +/** + * JANKY CUSTOMISATION OF OpenAPI SCHEMA TYPES + */ + +export type TypeHints = { + [fieldName: string]: FieldType; +}; + +export type InvocationSchemaExtra = { + output: OpenAPIV3.ReferenceObject; // the output of the invocation + ui?: { + tags?: string[]; + type_hints?: TypeHints; + title?: string; + }; + title: string; + properties: Omit< + NonNullable, + 'type' + > & { + type: Omit & { + default: AnyInvocationType; + }; + }; +}; + +export type InvocationSchemaType = { + default: string; // the type of the invocation +}; + +export type InvocationBaseSchemaObject = Omit< + OpenAPIV3.BaseSchemaObject, + 'title' | 'type' | 'properties' +> & + InvocationSchemaExtra; + +export interface ArraySchemaObject extends InvocationBaseSchemaObject { + type: OpenAPIV3.ArraySchemaObjectType; + items: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject; +} +export interface NonArraySchemaObject extends InvocationBaseSchemaObject { + type?: OpenAPIV3.NonArraySchemaObjectType; +} + +export type InvocationSchemaObject = ArraySchemaObject | NonArraySchemaObject; + +export const isInvocationSchemaObject = ( + obj: OpenAPIV3.ReferenceObject | InvocationSchemaObject +): obj is InvocationSchemaObject => !('$ref' in obj); diff --git a/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts b/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts new file mode 100644 index 0000000000..e37f446e00 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts @@ -0,0 +1,336 @@ +import { reduce } from 'lodash'; +import { OpenAPIV3 } from 'openapi-types'; +import { FIELD_TYPE_MAP } from '../types/constants'; +import { isSchemaObject } from '../types/typeGuards'; +import { + BooleanInputFieldTemplate, + EnumInputFieldTemplate, + FloatInputFieldTemplate, + ImageInputFieldTemplate, + IntegerInputFieldTemplate, + LatentsInputFieldTemplate, + StringInputFieldTemplate, + ModelInputFieldTemplate, + InputFieldTemplateBase, + OutputFieldTemplate, + TypeHints, + FieldType, +} from '../types/types'; + +export type BaseFieldProperties = 'name' | 'title' | 'description'; + +export type BuildInputFieldArg = { + schemaObject: OpenAPIV3.SchemaObject; + baseField: Omit< + InputFieldTemplateBase, + 'type' | 'inputRequirement' | 'inputKind' + >; +}; + +/** + * Transforms an invocation output ref object to field type. + * @param ref The ref string to transform + * @returns The field type. + * + * @example + * refObjectToFieldType({ "$ref": "#/components/schemas/ImageField" }) --> 'ImageField' + */ +export const refObjectToFieldType = ( + refObject: OpenAPIV3.ReferenceObject +): keyof typeof FIELD_TYPE_MAP => refObject.$ref.split('/').slice(-1)[0]; + +const buildIntegerInputFieldTemplate = ({ + schemaObject, + baseField, +}: BuildInputFieldArg): IntegerInputFieldTemplate => { + const template: IntegerInputFieldTemplate = { + ...baseField, + type: 'integer', + inputRequirement: 'always', + inputKind: 'any', + default: schemaObject.default ?? 0, + }; + + if (schemaObject.multipleOf !== undefined) { + template.multipleOf = schemaObject.multipleOf; + } + + if (schemaObject.maximum !== undefined) { + template.maximum = schemaObject.maximum; + } + + if (schemaObject.exclusiveMaximum !== undefined) { + template.exclusiveMaximum = schemaObject.exclusiveMaximum; + } + + if (schemaObject.minimum !== undefined) { + template.minimum = schemaObject.minimum; + } + + if (schemaObject.exclusiveMinimum !== undefined) { + template.exclusiveMinimum = schemaObject.exclusiveMinimum; + } + + return template; +}; + +const buildFloatInputFieldTemplate = ({ + schemaObject, + baseField, +}: BuildInputFieldArg): FloatInputFieldTemplate => { + const template: FloatInputFieldTemplate = { + ...baseField, + type: 'float', + inputRequirement: 'always', + inputKind: 'any', + default: schemaObject.default ?? 0, + }; + + if (schemaObject.multipleOf !== undefined) { + template.multipleOf = schemaObject.multipleOf; + } + + if (schemaObject.maximum !== undefined) { + template.maximum = schemaObject.maximum; + } + + if (schemaObject.exclusiveMaximum !== undefined) { + template.exclusiveMaximum = schemaObject.exclusiveMaximum; + } + + if (schemaObject.minimum !== undefined) { + template.minimum = schemaObject.minimum; + } + + if (schemaObject.exclusiveMinimum !== undefined) { + template.exclusiveMinimum = schemaObject.exclusiveMinimum; + } + + return template; +}; + +const buildStringInputFieldTemplate = ({ + schemaObject, + baseField, +}: BuildInputFieldArg): StringInputFieldTemplate => { + const template: StringInputFieldTemplate = { + ...baseField, + type: 'string', + inputRequirement: 'always', + inputKind: 'any', + default: schemaObject.default ?? '', + }; + + if (schemaObject.minLength !== undefined) { + template.minLength = schemaObject.minLength; + } + + if (schemaObject.maxLength !== undefined) { + template.maxLength = schemaObject.maxLength; + } + + if (schemaObject.pattern !== undefined) { + template.pattern = schemaObject.pattern; + } + + return template; +}; + +const buildBooleanInputFieldTemplate = ({ + schemaObject, + baseField, +}: BuildInputFieldArg): BooleanInputFieldTemplate => { + const template: BooleanInputFieldTemplate = { + ...baseField, + type: 'boolean', + inputRequirement: 'always', + inputKind: 'any', + default: schemaObject.default ?? false, + }; + + return template; +}; + +const buildModelInputFieldTemplate = ({ + schemaObject, + baseField, +}: BuildInputFieldArg): ModelInputFieldTemplate => { + const template: ModelInputFieldTemplate = { + ...baseField, + type: 'model', + inputRequirement: 'always', + inputKind: 'direct', + default: schemaObject.default ?? undefined, + }; + + return template; +}; + +const buildImageInputFieldTemplate = ({ + schemaObject, + baseField, +}: BuildInputFieldArg): ImageInputFieldTemplate => { + const template: ImageInputFieldTemplate = { + ...baseField, + type: 'image', + inputRequirement: 'always', + inputKind: 'any', + default: schemaObject.default ?? undefined, + }; + + return template; +}; + +const buildLatentsInputFieldTemplate = ({ + schemaObject, + baseField, +}: BuildInputFieldArg): LatentsInputFieldTemplate => { + const template: LatentsInputFieldTemplate = { + ...baseField, + type: 'latents', + inputRequirement: 'always', + inputKind: 'connection', + default: schemaObject.default ?? undefined, + }; + + return template; +}; + +const buildEnumInputFieldTemplate = ({ + schemaObject, + baseField, +}: BuildInputFieldArg): EnumInputFieldTemplate => { + const options = schemaObject.enum ?? []; + const template: EnumInputFieldTemplate = { + ...baseField, + type: 'enum', + enumType: (schemaObject.type as 'string' | 'number') ?? 'string', // TODO: dangerous? + options: options, + inputRequirement: 'always', + inputKind: 'direct', + default: schemaObject.default ?? options[0], + }; + + return template; +}; + +export const getFieldType = ( + schemaObject: OpenAPIV3.SchemaObject, + name: string, + typeHints?: TypeHints +): FieldType => { + let rawFieldType = ''; + + if (typeHints && name in typeHints) { + rawFieldType = typeHints[name]; + } else if (!schemaObject.type) { + rawFieldType = refObjectToFieldType( + schemaObject.allOf![0] as OpenAPIV3.ReferenceObject + ); + } else if (schemaObject.enum) { + rawFieldType = 'enum'; + } else if (schemaObject.type) { + rawFieldType = schemaObject.type; + } + + const fieldType = FIELD_TYPE_MAP[rawFieldType]; + + if (!fieldType) { + throw `Field type "${rawFieldType}" is unknown!`; + } + + return fieldType; +}; + +/** + * Builds an input field from an invocation schema property. + * @param schemaObject The schema object + * @returns An input field + */ +export const buildInputFieldTemplate = ( + schemaObject: OpenAPIV3.SchemaObject, + name: string, + typeHints?: TypeHints +) => { + const fieldType = getFieldType(schemaObject, name, typeHints); + + const baseField = { + name, + title: schemaObject.title ?? '', + description: schemaObject.description ?? '', + }; + + if (['image'].includes(fieldType)) { + return buildImageInputFieldTemplate({ schemaObject, baseField }); + } + if (['latents'].includes(fieldType)) { + return buildLatentsInputFieldTemplate({ schemaObject, baseField }); + } + if (['model'].includes(fieldType)) { + return buildModelInputFieldTemplate({ schemaObject, baseField }); + } + if (['enum'].includes(fieldType)) { + return buildEnumInputFieldTemplate({ schemaObject, baseField }); + } + if (['integer'].includes(fieldType)) { + return buildIntegerInputFieldTemplate({ schemaObject, baseField }); + } + if (['number', 'float'].includes(fieldType)) { + return buildFloatInputFieldTemplate({ schemaObject, baseField }); + } + if (['string'].includes(fieldType)) { + return buildStringInputFieldTemplate({ schemaObject, baseField }); + } + if (['boolean'].includes(fieldType)) { + return buildBooleanInputFieldTemplate({ schemaObject, baseField }); + } + + return; +}; + +/** + * Builds invocation output fields from an invocation's output reference object. + * @param openAPI The OpenAPI schema + * @param refObject The output reference object + * @returns A record of outputs + */ +export const buildOutputFieldTemplates = ( + refObject: OpenAPIV3.ReferenceObject, + openAPI: OpenAPIV3.Document, + typeHints?: TypeHints +): Record => { + // extract output schema name from ref + const outputSchemaName = refObject.$ref.split('/').slice(-1)[0]; + + // get the output schema itself + const outputSchema = openAPI.components!.schemas![outputSchemaName]; + + if (isSchemaObject(outputSchema)) { + const outputFields = reduce( + outputSchema.properties as OpenAPIV3.SchemaObject, + (outputsAccumulator, property, propertyName) => { + if ( + !['type', 'id'].includes(propertyName) && + !['object'].includes(property.type) && // TODO: handle objects? + isSchemaObject(property) + ) { + const fieldType = getFieldType(property, propertyName, typeHints); + + outputsAccumulator[propertyName] = { + name: propertyName, + title: property.title ?? '', + description: property.description ?? '', + type: fieldType, + }; + } + + return outputsAccumulator; + }, + {} as Record + ); + + return outputFields; + } + + return {}; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts b/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts new file mode 100644 index 0000000000..f2db2b5dc4 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts @@ -0,0 +1,57 @@ +import { InputFieldTemplate, InputFieldValue } from '../types/types'; + +export const buildInputFieldValue = ( + id: string, + template: InputFieldTemplate +): InputFieldValue => { + const fieldValue: InputFieldValue = { + id, + name: template.name, + type: template.type, + }; + + if (template.inputRequirement !== 'never') { + if (template.type === 'string') { + fieldValue.value = template.default ?? ''; + } + + if (template.type === 'integer') { + fieldValue.value = template.default ?? 0; + } + + if (template.type === 'float') { + fieldValue.value = template.default ?? 0; + } + + if (template.type === 'boolean') { + fieldValue.value = template.default ?? false; + } + + if (template.type === 'enum') { + if (template.enumType === 'number') { + fieldValue.value = template.default ?? 0; + } + if (template.enumType === 'string') { + fieldValue.value = template.default ?? ''; + } + } + + if (template.type === 'array') { + fieldValue.value = template.default ?? 1; + } + + if (template.type === 'image') { + fieldValue.value = undefined; + } + + if (template.type === 'latents') { + fieldValue.value = undefined; + } + + if (template.type === 'model') { + fieldValue.value = undefined; + } + } + + return fieldValue; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildEdges.ts b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildEdges.ts new file mode 100644 index 0000000000..873dba3ac3 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildEdges.ts @@ -0,0 +1,39 @@ +import { + Edge, + ImageToImageInvocation, + IterateInvocation, + RandomRangeInvocation, + RangeInvocation, + TextToImageInvocation, +} from 'services/api'; + +export const buildEdges = ( + baseNode: TextToImageInvocation | ImageToImageInvocation, + rangeNode: RangeInvocation | RandomRangeInvocation, + iterateNode: IterateInvocation +): Edge[] => { + const edges: Edge[] = [ + { + source: { + node_id: rangeNode.id, + field: 'collection', + }, + destination: { + node_id: iterateNode.id, + field: 'collection', + }, + }, + { + source: { + node_id: iterateNode.id, + field: 'item', + }, + destination: { + node_id: baseNode.id, + field: 'seed', + }, + }, + ]; + + return edges; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildImageToImageNode.ts b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildImageToImageNode.ts new file mode 100644 index 0000000000..f04f177d5b --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildImageToImageNode.ts @@ -0,0 +1,99 @@ +import { v4 as uuidv4 } from 'uuid'; +import { RootState } from 'app/store'; +import { + Edge, + ImageToImageInvocation, + TextToImageInvocation, +} from 'services/api'; +import { _Image } from 'app/invokeai'; +import { initialImageSelector } from 'features/parameters/store/generationSelectors'; + +export const buildImg2ImgNode = (state: RootState): ImageToImageInvocation => { + const nodeId = uuidv4(); + const { generation, system, models } = state; + + const { selectedModelName } = models; + + const { + prompt, + seed, + steps, + width, + height, + cfgScale, + sampler, + seamless, + img2imgStrength: strength, + shouldFitToWidthHeight: fit, + shouldRandomizeSeed, + } = generation; + + const initialImage = initialImageSelector(state); + + if (!initialImage) { + // TODO: handle this + throw 'no initial image'; + } + + const imageToImageNode: ImageToImageInvocation = { + id: nodeId, + type: 'img2img', + prompt, + steps, + width, + height, + cfg_scale: cfgScale, + scheduler: sampler as ImageToImageInvocation['scheduler'], + seamless, + model: selectedModelName, + progress_images: true, + image: { + image_name: initialImage.name, + image_type: initialImage.type, + }, + strength, + fit, + }; + + if (!shouldRandomizeSeed) { + imageToImageNode.seed = seed; + } + + return imageToImageNode; +}; + +type hiresReturnType = { + node: Record; + edge: Edge; +}; + +export const buildHiResNode = ( + baseNode: Record, + strength?: number +): hiresReturnType => { + const nodeId = uuidv4(); + const baseNodeId = Object.keys(baseNode)[0]; + const baseNodeValues = Object.values(baseNode)[0]; + + return { + node: { + [nodeId]: { + ...baseNodeValues, + id: nodeId, + type: 'img2img', + strength, + fit: true, + }, + }, + edge: { + source: { + field: 'image', + node_id: baseNodeId, + }, + destination: { + field: 'image', + node_id: nodeId, + }, + }, + }; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildIterateNode.ts b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildIterateNode.ts new file mode 100644 index 0000000000..6764038da4 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildIterateNode.ts @@ -0,0 +1,13 @@ +import { v4 as uuidv4 } from 'uuid'; + +import { IterateInvocation } from 'services/api'; + +export const buildIterateNode = (): IterateInvocation => { + const nodeId = uuidv4(); + return { + id: nodeId, + type: 'iterate', + collection: [], + index: 0, + }; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildLinearGraph.ts new file mode 100644 index 0000000000..9667dfa2b3 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildLinearGraph.ts @@ -0,0 +1,39 @@ +import { RootState } from 'app/store'; +import { Graph } from 'services/api'; +import { buildImg2ImgNode } from './buildImageToImageNode'; +import { buildTxt2ImgNode } from './buildTextToImageNode'; +import { buildRangeNode } from './buildRangeNode'; +import { buildIterateNode } from './buildIterateNode'; +import { buildEdges } from './buildEdges'; + +/** + * Builds the Linear workflow graph. + */ +export const buildLinearGraph = (state: RootState): Graph => { + // The base node is either a txt2img or img2img node + const baseNode = state.generation.isImageToImageEnabled + ? buildImg2ImgNode(state) + : buildTxt2ImgNode(state); + + // We always range and iterate nodes, no matter the iteration count + // This is required to provide the correct seeds to the backend engine + const rangeNode = buildRangeNode(state); + const iterateNode = buildIterateNode(); + + // Build the edges for the nodes selected. + const edges = buildEdges(baseNode, rangeNode, iterateNode); + + // Assemble! + const graph = { + nodes: { + [rangeNode.id]: rangeNode, + [iterateNode.id]: iterateNode, + [baseNode.id]: baseNode, + }, + edges, + }; + + // TODO: hires fix requires latent space upscaling; we don't have nodes for this yet + + return graph; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildRangeNode.ts b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildRangeNode.ts new file mode 100644 index 0000000000..1f87ec785e --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildRangeNode.ts @@ -0,0 +1,26 @@ +import { v4 as uuidv4 } from 'uuid'; + +import { RootState } from 'app/store'; +import { RandomRangeInvocation, RangeInvocation } from 'services/api'; + +export const buildRangeNode = ( + state: RootState +): RangeInvocation | RandomRangeInvocation => { + const nodeId = uuidv4(); + const { shouldRandomizeSeed, iterations, seed } = state.generation; + + if (shouldRandomizeSeed) { + return { + id: nodeId, + type: 'random_range', + size: iterations, + }; + } + + return { + id: nodeId, + type: 'range', + start: seed, + stop: seed + iterations, + }; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildTextToImageNode.ts b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildTextToImageNode.ts new file mode 100644 index 0000000000..057d35dbcb --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildTextToImageNode.ts @@ -0,0 +1,42 @@ +import { v4 as uuidv4 } from 'uuid'; +import { RootState } from 'app/store'; +import { TextToImageInvocation } from 'services/api'; + +export const buildTxt2ImgNode = (state: RootState): TextToImageInvocation => { + const nodeId = uuidv4(); + const { generation, models } = state; + + const { selectedModelName } = models; + + const { + prompt, + seed, + steps, + width, + height, + cfgScale: cfg_scale, + sampler, + seamless, + shouldRandomizeSeed, + } = generation; + + const textToImageNode: NonNullable = { + id: nodeId, + type: 'txt2img', + prompt, + steps, + width, + height, + cfg_scale, + scheduler: sampler as TextToImageInvocation['scheduler'], + seamless, + model: selectedModelName, + progress_images: true, + }; + + if (!shouldRandomizeSeed) { + textToImageNode.seed = seed; + } + + return textToImageNode; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/nodesGraphBuilder/buildNodesGraph.ts b/invokeai/frontend/web/src/features/nodes/util/nodesGraphBuilder/buildNodesGraph.ts new file mode 100644 index 0000000000..848615277d --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/nodesGraphBuilder/buildNodesGraph.ts @@ -0,0 +1,77 @@ +import { Graph } from 'services/api'; +import { v4 as uuidv4 } from 'uuid'; +import { reduce } from 'lodash'; +import { RootState } from 'app/store'; +import { AnyInvocation } from 'services/events/types'; + +/** + * Builds a graph from the node editor state. + */ +export const buildNodesGraph = (state: RootState): Graph => { + const { nodes, edges } = state.nodes; + + // Reduce the node editor nodes into invocation graph nodes + const parsedNodes = nodes.reduce>( + (nodesAccumulator, node, nodeIndex) => { + const { id, data } = node; + const { type, inputs } = data; + + // Transform each node's inputs to simple key-value pairs + const transformedInputs = reduce( + inputs, + (inputsAccumulator, input, name) => { + inputsAccumulator[name] = input.value; + + return inputsAccumulator; + }, + {} as Record, any> + ); + + // Build this specific node + const graphNode = { + type, + id, + ...transformedInputs, + }; + + // Add it to the nodes object + Object.assign(nodesAccumulator, { + [id]: graphNode, + }); + + return nodesAccumulator; + }, + {} + ); + + // Reduce the node editor edges into invocation graph edges + const parsedEdges = edges.reduce>( + (edgesAccumulator, edge, edgeIndex) => { + const { source, target, sourceHandle, targetHandle } = edge; + + // Format the edges and add to the edges array + edgesAccumulator.push({ + source: { + node_id: source, + field: sourceHandle as string, + }, + destination: { + node_id: target, + field: targetHandle as string, + }, + }); + + return edgesAccumulator; + }, + [] + ); + + // Assemble! + const graph = { + id: uuidv4(), + nodes: parsedNodes, + edges: parsedEdges, + }; + + return graph; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts b/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts new file mode 100644 index 0000000000..c4c2b8fcf1 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts @@ -0,0 +1,120 @@ +import { filter, reduce } from 'lodash'; +import { OpenAPIV3 } from 'openapi-types'; +import { isSchemaObject } from '../types/typeGuards'; +import { + InputFieldTemplate, + InvocationSchemaObject, + InvocationTemplate, + isInvocationSchemaObject, + OutputFieldTemplate, +} from '../types/types'; +import { + buildInputFieldTemplate, + buildOutputFieldTemplates, +} from './fieldTemplateBuilders'; + +const invocationBlacklist = ['Graph', 'Collect', 'LoadImage']; + +export const parseSchema = (openAPI: OpenAPIV3.Document) => { + // filter out non-invocation schemas, plus some tricky invocations for now + const filteredSchemas = filter( + openAPI.components!.schemas, + (schema, key) => + key.includes('Invocation') && + !key.includes('InvocationOutput') && + !invocationBlacklist.some((blacklistItem) => key.includes(blacklistItem)) + ) as (OpenAPIV3.ReferenceObject | InvocationSchemaObject)[]; + + const invocations = filteredSchemas.reduce< + Record + >((acc, schema) => { + // only want SchemaObjects + if (isInvocationSchemaObject(schema)) { + const type = schema.properties.type.default; + + const title = + schema.ui?.title ?? + schema.title + .replace('Invocation', '') + .split(/(?=[A-Z])/) // split PascalCase into array + .join(' '); + + const typeHints = schema.ui?.type_hints; + + const inputs = reduce( + schema.properties, + (inputsAccumulator, property, propertyName) => { + if ( + // `type` and `id` are not valid inputs/outputs + !['type', 'id'].includes(propertyName) && + isSchemaObject(property) + ) { + let field: InputFieldTemplate | undefined; + if (propertyName === 'collection') { + field = { + default: property.default ?? [], + name: 'collection', + title: property.title ?? '', + description: property.description ?? '', + type: 'array', + inputRequirement: 'always', + inputKind: 'connection', + }; + } else { + field = buildInputFieldTemplate( + property, + propertyName, + typeHints + ); + } + if (field) { + inputsAccumulator[propertyName] = field; + } + } + return inputsAccumulator; + }, + {} as Record + ); + + const rawOutput = (schema as InvocationSchemaObject).output; + + let outputs: Record; + + // some special handling is needed for collect, iterate and range nodes + if (type === 'iterate') { + // this is guaranteed to be a SchemaObject + const iterationOutput = openAPI.components!.schemas![ + 'IterateInvocationOutput' + ] as OpenAPIV3.SchemaObject; + + outputs = { + item: { + name: 'item', + title: iterationOutput.title ?? '', + description: iterationOutput.description ?? '', + type: 'array', + }, + }; + } else { + outputs = buildOutputFieldTemplates(rawOutput, openAPI, typeHints); + } + + const invocation: InvocationTemplate = { + title, + type, + tags: schema.ui?.tags ?? [], + description: schema.description ?? '', + inputs, + outputs, + }; + + Object.assign(acc, { [type]: invocation }); + } + + return acc; + }, {}); + + console.debug('Generated invocations: ', invocations); + + return invocations; +}; diff --git a/invokeai/frontend/web/src/features/parameters/components/AccordionItems/InvokeAccordionItem.tsx b/invokeai/frontend/web/src/features/parameters/components/AccordionItems/InvokeAccordionItem.tsx index b742f5a37e..a4f9be7918 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AccordionItems/InvokeAccordionItem.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/AccordionItems/InvokeAccordionItem.tsx @@ -6,19 +6,18 @@ import { Box, Flex, } from '@chakra-ui/react'; -import { Feature } from 'app/features'; import GuideIcon from 'common/components/GuideIcon'; -import { ReactNode } from 'react'; +import { ParametersAccordionItem } from '../ParametersAccordion'; -export interface InvokeAccordionItemProps { - header: string; - content: ReactNode; - feature?: Feature; - additionalHeaderComponents?: ReactNode; -} +type InvokeAccordionItemProps = { + accordionItem: ParametersAccordionItem; +}; -export default function InvokeAccordionItem(props: InvokeAccordionItemProps) { - const { header, feature, content, additionalHeaderComponents } = props; +export default function InvokeAccordionItem({ + accordionItem, +}: InvokeAccordionItemProps) { + const { header, feature, content, additionalHeaderComponents } = + accordionItem; return ( @@ -32,7 +31,7 @@ export default function InvokeAccordionItem(props: InvokeAccordionItemProps) { - {content} + {content} ); } diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/InfillAndScalingSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/InfillAndScalingSettings.tsx index 187b23cdff..866038c993 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/InfillAndScalingSettings.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/InfillAndScalingSettings.tsx @@ -115,9 +115,7 @@ const InfillAndScalingSettings = () => { onChange={handleChangeBoundingBoxScaleMethod} /> { handleReset={handleResetScaledWidth} /> { onChange={(e) => dispatch(setInfillMethod(e.target.value))} /> state.generation.shouldFitToWidthHeight ); + const isImageToImageEnabled = useAppSelector( + (state: RootState) => state.generation.isImageToImageEnabled + ); + const handleChangeFit = (e: ChangeEvent) => dispatch(setShouldFitToWidthHeight(e.target.checked)); @@ -19,6 +23,7 @@ export default function ImageFit() { return ( + diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageStrength.tsx index e4d5a9174d..503b364f1a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageStrength.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageStrength.tsx @@ -14,6 +14,9 @@ export default function ImageToImageStrength(props: ImageToImageStrengthProps) { const img2imgStrength = useAppSelector( (state: RootState) => state.generation.img2imgStrength ); + const isImageToImageEnabled = useAppSelector( + (state: RootState) => state.generation.isImageToImageEnabled + ); const dispatch = useAppDispatch(); @@ -37,6 +40,7 @@ export default function ImageToImageStrength(props: ImageToImageStrengthProps) { inputWidth={22} withReset handleReset={handleImg2ImgStrengthReset} + isDisabled={!isImageToImageEnabled} /> ); } diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageToggle.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageToggle.tsx new file mode 100644 index 0000000000..ad449a5ff3 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageToggle.tsx @@ -0,0 +1,24 @@ +import { RootState } from 'app/store'; +import { useAppDispatch, useAppSelector } from 'app/storeHooks'; +import IAISwitch from 'common/components/IAISwitch'; +import { isImageToImageEnabledChanged } from 'features/parameters/store/generationSlice'; +import { ChangeEvent } from 'react'; + +export default function ImageToImageToggle() { + const isImageToImageEnabled = useAppSelector( + (state: RootState) => state.generation.isImageToImageEnabled + ); + + const dispatch = useAppDispatch(); + + const handleChange = (e: ChangeEvent) => + dispatch(isImageToImageEnabledChanged(e.target.checked)); + + return ( + + ); +} diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/InitialImagePreview.tsx new file mode 100644 index 0000000000..0a3fd34c95 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/InitialImagePreview.tsx @@ -0,0 +1,155 @@ +import { Box, Flex, Image, Spinner, Text } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from 'app/store'; +import { useAppDispatch, useAppSelector } from 'app/storeHooks'; +import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder'; +import { useGetUrl } from 'common/util/getUrl'; +import useGetImageByNameAndType from 'features/gallery/hooks/useGetImageByName'; +import { selectResultsById } from 'features/gallery/store/resultsSlice'; +import { + clearInitialImage, + initialImageSelected, +} from 'features/parameters/store/generationSlice'; +import { addToast } from 'features/system/store/systemSlice'; +import { isEqual } from 'lodash'; +import { DragEvent, useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ImageType } from 'services/api'; +import ImageToImageOverlay from 'common/components/ImageToImageOverlay'; + +const initialImagePreviewSelector = createSelector( + [(state: RootState) => state], + (state) => { + const { initialImage } = state.generation; + const image = selectResultsById(state, initialImage as string); + + return { + initialImage: image, + }; + }, + { memoizeOptions: { resultEqualityCheck: isEqual } } +); + +const InitialImagePreview = () => { + const isImageToImageEnabled = useAppSelector( + (state: RootState) => state.generation.isImageToImageEnabled + ); + const { initialImage } = useAppSelector(initialImagePreviewSelector); + const { getUrl } = useGetUrl(); + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + + const [isLoaded, setIsLoaded] = useState(false); + const getImageByNameAndType = useGetImageByNameAndType(); + + const onError = () => { + dispatch( + addToast({ + title: t('toast.parametersFailed'), + description: t('toast.parametersFailedDesc'), + status: 'error', + isClosable: true, + }) + ); + dispatch(clearInitialImage()); + setIsLoaded(false); + }; + + const handleDrop = useCallback( + (e: DragEvent) => { + setIsLoaded(false); + const name = e.dataTransfer.getData('invokeai/imageName'); + const type = e.dataTransfer.getData('invokeai/imageType') as ImageType; + + if (!name || !type) { + return; + } + + const image = getImageByNameAndType(name, type); + + if (!image) { + return; + } + + dispatch(initialImageSelected(image.name)); + }, + [getImageByNameAndType, dispatch] + ); + + return ( + + + {initialImage?.url && ( + <> + { + setIsLoaded(true); + }} + fallback={ + + + + } + /> + {isLoaded && ( + + )} + + )} + + {!initialImage?.url && } + + {!isImageToImageEnabled && ( + + + Image to Image is Disabled + + + )} + + ); +}; + +export default InitialImagePreview; diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/HiresSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/HiresSettings.tsx index 97705da9cc..172acaab68 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/HiresSettings.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/HiresSettings.tsx @@ -51,9 +51,7 @@ export const HiresStrength = () => { // inputWidth={22} withReset handleReset={handleHiResStrengthReset} - isSliderDisabled={!hiresFix} - isInputDisabled={!hiresFix} - isResetDisabled={!hiresFix} + isDisabled={!hiresFix} /> ); }; diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleDenoisingStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleDenoisingStrength.tsx index 908c4b5527..0cb5a12524 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleDenoisingStrength.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleDenoisingStrength.tsx @@ -30,9 +30,7 @@ export default function UpscaleDenoisingStrength() { withSliderMarks withInput withReset - isSliderDisabled={!isESRGANAvailable} - isInputDisabled={!isESRGANAvailable} - isResetDisabled={!isESRGANAvailable} + isDisabled={!isESRGANAvailable} /> ); } diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleStrength.tsx index 0f67a3a053..819c4fda57 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleStrength.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleStrength.tsx @@ -27,9 +27,7 @@ export default function UpscaleStrength() { withSliderMarks withInput withReset - isSliderDisabled={!isESRGANAvailable} - isInputDisabled={!isESRGANAvailable} - isResetDisabled={!isESRGANAvailable} + isDisabled={!isESRGANAvailable} /> ); } diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/VariationAmount.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/VariationAmount.tsx index eef4c8728c..27a39757f9 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/VariationAmount.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/VariationAmount.tsx @@ -24,9 +24,7 @@ export default function VariationAmount() { step={0.01} min={0} max={1} - isSliderDisabled={!shouldGenerateVariations} - isInputDisabled={!shouldGenerateVariations} - isResetDisabled={!shouldGenerateVariations} + isDisabled={!shouldGenerateVariations} onChange={(v) => dispatch(setVariationAmount(v))} handleReset={() => dispatch(setVariationAmount(0.1))} withInput diff --git a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainHeight.tsx index 0068568402..8dbf70eab5 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainHeight.tsx @@ -19,9 +19,7 @@ export default function MainHeight() { return shouldUseSliders ? ( { + const { + activeTab, + openLinearAccordionItems, + openUnifiedCanvasAccordionItems, + disabledParameterPanels, + } = uiSlice; + + let openAccordions: number[] = []; + + if (tabMap[activeTab] === 'linear') { + openAccordions = openLinearAccordionItems; + } + + if (tabMap[activeTab] === 'unifiedCanvas') { + openAccordions = openUnifiedCanvasAccordionItems; + } + + return { + openAccordions, + disabledParameterPanels, + }; +}); + +export type ParametersAccordionItem = { + name: string; + header: string; + content: ReactNode; + feature?: Feature; + additionalHeaderComponents?: ReactNode; }; -type ParametersAccordionsType = { - accordionInfo: ParametersAccordionType; +export type ParametersAccordionItems = { + [parametersAccordionKey: string]: ParametersAccordionItem; +}; + +type ParametersAccordionProps = { + accordionItems: ParametersAccordionItems; }; /** * Main container for generation and processing parameters. */ -const ParametersAccordion = (props: ParametersAccordionsType) => { - const { accordionInfo } = props; - - const openAccordions = useAppSelector( - (state: RootState) => state.system.openAccordions +const ParametersAccordion = ({ accordionItems }: ParametersAccordionProps) => { + const { openAccordions, disabledParameterPanels } = useAppSelector( + parametersAccordionSelector ); const dispatch = useAppDispatch(); - /** - * Stores accordion state in redux so preferred UI setup is retained. - */ - const handleChangeAccordionState = (openAccordions: number | number[]) => - dispatch(setOpenAccordions(openAccordions)); - - const renderAccordions = () => { - const accordionsToRender: ReactElement[] = []; - if (accordionInfo) { - Object.keys(accordionInfo).forEach((key) => { - const { header, feature, content, additionalHeaderComponents } = - accordionInfo[key]; - accordionsToRender.push( - - ); - }); - } - return accordionsToRender; + const handleChangeAccordionState = (openAccordions: number | number[]) => { + dispatch( + openAccordionItemsChanged( + Array.isArray(openAccordions) ? openAccordions : [openAccordions] + ) + ); }; + // Render function for accordion items + const renderAccordionItems = useCallback(() => { + // Filter out disabled accordions + const filteredAccordionItems = filter( + accordionItems, + (item) => disabledParameterPanels.indexOf(item.name) === -1 + ); + + return filteredAccordionItems.map((accordionItem) => ( + + )); + }, [disabledParameterPanels, accordionItems]); + return ( { gap: 2, }} > - {renderAccordions()} + {renderAccordionItems()} ); }; diff --git a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx index 0992e63ee3..2fb81ae9a0 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx @@ -1,5 +1,4 @@ import { createSelector } from '@reduxjs/toolkit'; -import { cancelProcessing } from 'app/socketio/actions'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import IAIIconButton, { IAIIconButtonProps, @@ -9,16 +8,36 @@ import { SystemState, setCancelAfter, setCancelType, + cancelScheduled, + cancelTypeChanged, + CancelType, } from 'features/system/store/systemSlice'; import { isEqual } from 'lodash'; import { useEffect, useCallback, memo } from 'react'; -import { ButtonSpinner, ButtonGroup } from '@chakra-ui/react'; +import { + ButtonSpinner, + ButtonGroup, + Menu, + MenuButton, + MenuList, + MenuOptionGroup, + MenuItemOption, + IconButton, +} from '@chakra-ui/react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; -import { MdCancel, MdCancelScheduleSend } from 'react-icons/md'; +import { + MdArrowDropDown, + MdArrowDropUp, + MdCancel, + MdCancelScheduleSend, +} from 'react-icons/md'; import IAISimpleMenu from 'common/components/IAISimpleMenu'; +import { sessionCanceled } from 'services/thunks/session'; +import { FaChevronDown } from 'react-icons/fa'; +import { BiChevronDown } from 'react-icons/bi'; const cancelButtonSelector = createSelector( systemSelector, @@ -29,8 +48,11 @@ const cancelButtonSelector = createSelector( isCancelable: system.isCancelable, currentIteration: system.currentIteration, totalIterations: system.totalIterations, - cancelType: system.cancelOptions.cancelType, - cancelAfter: system.cancelOptions.cancelAfter, + // cancelType: system.cancelOptions.cancelType, + // cancelAfter: system.cancelOptions.cancelAfter, + sessionId: system.sessionId, + cancelType: system.cancelType, + isCancelScheduled: system.isCancelScheduled, }; }, { @@ -56,16 +78,34 @@ const CancelButton = ( currentIteration, totalIterations, cancelType, - cancelAfter, + isCancelScheduled, + // cancelAfter, + sessionId, } = useAppSelector(cancelButtonSelector); + const handleClickCancel = useCallback(() => { - dispatch(cancelProcessing()); - dispatch(setCancelAfter(null)); - }, [dispatch]); + if (!sessionId) { + return; + } + + if (cancelType === 'scheduled') { + dispatch(cancelScheduled()); + return; + } + + dispatch(sessionCanceled({ sessionId })); + }, [dispatch, sessionId, cancelType]); const { t } = useTranslation(); - const isCancelScheduled = cancelAfter === null ? false : true; + const handleCancelTypeChanged = useCallback( + (value: string | string[]) => { + const newCancelType = Array.isArray(value) ? value[0] : value; + dispatch(cancelTypeChanged(newCancelType as CancelType)); + }, + [dispatch] + ); + // const isCancelScheduled = cancelAfter === null ? false : true; useHotkeys( 'shift+x', @@ -77,22 +117,22 @@ const CancelButton = ( [isConnected, isProcessing, isCancelable] ); - useEffect(() => { - if (cancelAfter !== null && cancelAfter < currentIteration) { - handleClickCancel(); - } - }, [cancelAfter, currentIteration, handleClickCancel]); + // useEffect(() => { + // if (cancelAfter !== null && cancelAfter < currentIteration) { + // handleClickCancel(); + // } + // }, [cancelAfter, currentIteration, handleClickCancel]); - const cancelMenuItems = [ - { - item: t('parameters.cancel.immediate'), - onClick: () => dispatch(setCancelType('immediate')), - }, - { - item: t('parameters.cancel.schedule'), - onClick: () => dispatch(setCancelType('scheduled')), - }, - ]; + // const cancelMenuItems = [ + // { + // item: t('parameters.cancel.immediate'), + // onClick: () => dispatch(cancelTypeChanged('immediate')), + // }, + // { + // item: t('parameters.cancel.schedule'), + // onClick: () => dispatch(cancelTypeChanged('scheduled')), + // }, + // ]; return ( @@ -121,29 +161,40 @@ const CancelButton = ( ? t('parameters.cancel.isScheduled') : t('parameters.cancel.schedule') } - isDisabled={ - !isConnected || - !isProcessing || - !isCancelable || - currentIteration === totalIterations - } - onClick={() => { - // If a cancel request has already been made, and the user clicks again before the next iteration has been processed, stop the request. - if (isCancelScheduled) dispatch(setCancelAfter(null)); - else dispatch(setCancelAfter(currentIteration)); - }} + isDisabled={!isConnected || !isProcessing || !isCancelable} + onClick={handleClickCancel} colorScheme="error" {...rest} /> )} - + + + } + paddingX={0} + paddingY={0} + colorScheme="error" + minWidth={5} + /> + + + + {t('parameters.cancel.immediate')} + + + {t('parameters.cancel.schedule')} + + + + ); }; diff --git a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx index b68f245044..d4293c0938 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx @@ -11,6 +11,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { FaPlay } from 'react-icons/fa'; +import { linearGraphBuilt, sessionCreated } from 'services/thunks/session'; interface InvokeButton extends Omit { @@ -24,7 +25,8 @@ export default function InvokeButton(props: InvokeButton) { const activeTabName = useAppSelector(activeTabNameSelector); const handleClickGenerate = () => { - dispatch(generateImage(activeTabName)); + // dispatch(generateImage(activeTabName)); + dispatch(linearGraphBuilt()); }; const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/store/generationPersistBlacklist.ts b/invokeai/frontend/web/src/features/parameters/store/generationPersistBlacklist.ts new file mode 100644 index 0000000000..884ed0e079 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/store/generationPersistBlacklist.ts @@ -0,0 +1,10 @@ +import { GenerationState } from './generationSlice'; + +/** + * Generation slice persist blacklist + */ +const itemsToBlacklist: (keyof GenerationState)[] = []; + +export const generationBlacklist = itemsToBlacklist.map( + (blacklistItem) => `generation.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSelectors.ts b/invokeai/frontend/web/src/features/parameters/store/generationSelectors.ts index 5cc8e1a592..39550c5ad6 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSelectors.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSelectors.ts @@ -1,5 +1,11 @@ import { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'app/store'; +import { gallerySelector } from 'features/gallery/store/gallerySelectors'; +import { + selectResultsById, + selectResultsEntities, +} from 'features/gallery/store/resultsSlice'; +import { selectUploadsById } from 'features/gallery/store/uploadsSlice'; import { isEqual } from 'lodash'; export const generationSelector = (state: RootState) => state.generation; @@ -15,3 +21,15 @@ export const mayGenerateMultipleImagesSelector = createSelector( }, } ); + +export const initialImageSelector = createSelector( + [(state: RootState) => state, generationSelector], + (state, generation) => { + const { initialImage: initialImageName } = generation; + + return ( + selectResultsById(state, initialImageName as string) ?? + selectUploadsById(state, initialImageName as string) + ); + } +); diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index 1cb3a98204..e4a92f0b10 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -11,7 +11,7 @@ export interface GenerationState { height: number; img2imgStrength: number; infillMethod: string; - initialImage?: InvokeAI.Image | string; // can be an Image or url + initialImage?: InvokeAI._Image | string; // can be an Image or url iterations: number; maskPath: string; perlin: number; @@ -36,6 +36,7 @@ export interface GenerationState { shouldUseSymmetry: boolean; horizontalSymmetrySteps: number; verticalSymmetrySteps: number; + isImageToImageEnabled: boolean; } const initialGenerationState: GenerationState = { @@ -67,6 +68,7 @@ const initialGenerationState: GenerationState = { shouldUseSymmetry: false, horizontalSymmetrySteps: 0, verticalSymmetrySteps: 0, + isImageToImageEnabled: false, }; const initialState: GenerationState = initialGenerationState; @@ -317,12 +319,12 @@ export const generationSlice = createSlice({ setShouldRandomizeSeed: (state, action: PayloadAction) => { state.shouldRandomizeSeed = action.payload; }, - setInitialImage: ( - state, - action: PayloadAction - ) => { - state.initialImage = action.payload; - }, + // setInitialImage: ( + // state, + // action: PayloadAction + // ) => { + // state.initialImage = action.payload; + // }, clearInitialImage: (state) => { state.initialImage = undefined; }, @@ -353,6 +355,13 @@ export const generationSlice = createSlice({ setVerticalSymmetrySteps: (state, action: PayloadAction) => { state.verticalSymmetrySteps = action.payload; }, + initialImageSelected: (state, action: PayloadAction) => { + state.initialImage = action.payload; + state.isImageToImageEnabled = true; + }, + isImageToImageEnabledChanged: (state, action: PayloadAction) => { + state.isImageToImageEnabled = action.payload; + }, }, }); @@ -368,7 +377,7 @@ export const { setHeight, setImg2imgStrength, setInfillMethod, - setInitialImage, + // setInitialImage, setIterations, setMaskPath, setParameter, @@ -394,6 +403,8 @@ export const { setShouldUseSymmetry, setHorizontalSymmetrySteps, setVerticalSymmetrySteps, + initialImageSelected, + isImageToImageEnabledChanged, } = generationSlice.actions; export default generationSlice.reducer; diff --git a/invokeai/frontend/web/src/features/parameters/store/postprocessingPersistBlacklist.ts b/invokeai/frontend/web/src/features/parameters/store/postprocessingPersistBlacklist.ts new file mode 100644 index 0000000000..9b8b3bb475 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/store/postprocessingPersistBlacklist.ts @@ -0,0 +1,10 @@ +import { PostprocessingState } from './postprocessingSlice'; + +/** + * Postprocessing slice persist blacklist + */ +const itemsToBlacklist: (keyof PostprocessingState)[] = []; + +export const postprocessingBlacklist = itemsToBlacklist.map( + (blacklistItem) => `postprocessing.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx b/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx index 761bce8f98..31d228df35 100644 --- a/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx +++ b/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx @@ -1,20 +1,27 @@ import { Flex } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; -import { requestModelChange } from 'app/socketio/actions'; +import { ChangeEvent } from 'react'; +import { isEqual } from 'lodash'; +import { useTranslation } from 'react-i18next'; + import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import IAISelect from 'common/components/IAISelect'; -import { isEqual, map } from 'lodash'; - -import { ChangeEvent } from 'react'; -import { useTranslation } from 'react-i18next'; -import { activeModelSelector, systemSelector } from '../store/systemSelectors'; +import { + modelSelected, + selectedModelSelector, + selectModelsIds, +} from '../store/modelSlice'; +import { RootState } from 'app/store'; const selector = createSelector( - [systemSelector], - (system) => { - const { isProcessing, model_list } = system; - const models = map(model_list, (model, key) => key); - return { models, isProcessing }; + [(state: RootState) => state], + (state) => { + const selectedModel = selectedModelSelector(state); + const allModelNames = selectModelsIds(state); + return { + allModelNames, + selectedModel, + }; }, { memoizeOptions: { @@ -26,10 +33,9 @@ const selector = createSelector( const ModelSelect = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const { models, isProcessing } = useAppSelector(selector); - const activeModel = useAppSelector(activeModelSelector); + const { allModelNames, selectedModel } = useAppSelector(selector); const handleChangeModel = (e: ChangeEvent) => { - dispatch(requestModelChange(e.target.value)); + dispatch(modelSelected(e.target.value)); }; return ( @@ -41,10 +47,9 @@ const ModelSelect = () => { diff --git a/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx b/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx index f47730d221..a8a87bc39a 100644 --- a/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx +++ b/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx @@ -80,7 +80,7 @@ const StatusIndicator = () => { cursor={statusIndicatorCursor} onClick={handleClickStatusIndicator} sx={{ - fontSize: 'xs', + fontSize: 'sm', fontWeight: '600', color: `${statusIdentifier}.400`, }} diff --git a/invokeai/frontend/web/src/features/system/hooks/useToastWatcher.ts b/invokeai/frontend/web/src/features/system/hooks/useToastWatcher.ts index 62d9179ec2..0c99eec0a4 100644 --- a/invokeai/frontend/web/src/features/system/hooks/useToastWatcher.ts +++ b/invokeai/frontend/web/src/features/system/hooks/useToastWatcher.ts @@ -1,9 +1,24 @@ -import { useToast } from '@chakra-ui/react'; +import { useToast, UseToastOptions } from '@chakra-ui/react'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { toastQueueSelector } from 'features/system/store/systemSelectors'; import { clearToastQueue } from 'features/system/store/systemSlice'; import { useEffect } from 'react'; +export type MakeToastArg = string | UseToastOptions; + +export const makeToast = (arg: MakeToastArg): UseToastOptions => { + if (typeof arg === 'string') { + return { + title: arg, + status: 'info', + isClosable: true, + duration: 2500, + }; + } + + return { status: 'info', isClosable: true, duration: 2500, ...arg }; +}; + const useToastWatcher = () => { const dispatch = useAppDispatch(); const toastQueue = useAppSelector(toastQueueSelector); diff --git a/invokeai/frontend/web/src/features/system/store/modelSelectors.ts b/invokeai/frontend/web/src/features/system/store/modelSelectors.ts new file mode 100644 index 0000000000..74027d631b --- /dev/null +++ b/invokeai/frontend/web/src/features/system/store/modelSelectors.ts @@ -0,0 +1,5 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from 'app/store'; +import { reduce } from 'lodash'; + +export const modelSelector = (state: RootState) => state.models; diff --git a/invokeai/frontend/web/src/features/system/store/modelSlice.ts b/invokeai/frontend/web/src/features/system/store/modelSlice.ts new file mode 100644 index 0000000000..843e27a435 --- /dev/null +++ b/invokeai/frontend/web/src/features/system/store/modelSlice.ts @@ -0,0 +1,80 @@ +import { createEntityAdapter, PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import { RootState } from 'app/store'; +import { keys, sample } from 'lodash'; +import { CkptModelInfo, DiffusersModelInfo } from 'services/api'; +import { receivedModels } from 'services/thunks/model'; + +export type Model = (CkptModelInfo | DiffusersModelInfo) & { + name: string; +}; + +export const modelsAdapter = createEntityAdapter({ + selectId: (model) => model.name, + sortComparer: (a, b) => a.name.localeCompare(b.name), +}); + +type AdditionalModelsState = { + selectedModelName: string; +}; + +export const initialModelsState = + modelsAdapter.getInitialState({ + selectedModelName: '', + }); + +export type ModelsState = typeof initialModelsState; + +export const modelsSlice = createSlice({ + name: 'models', + initialState: initialModelsState, + reducers: { + modelAdded: modelsAdapter.upsertOne, + modelSelected: (state, action: PayloadAction) => { + state.selectedModelName = action.payload; + }, + }, + extraReducers(builder) { + /** + * Received Models - FULFILLED + */ + builder.addCase(receivedModels.fulfilled, (state, action) => { + const models = action.payload; + modelsAdapter.setAll(state, models); + + // If the current selected model is `''` or isn't actually in the list of models, + // choose a random model + if ( + !state.selectedModelName || + !keys(models).includes(state.selectedModelName) + ) { + const randomModel = sample(models); + + if (randomModel) { + state.selectedModelName = randomModel.name; + } else { + state.selectedModelName = ''; + } + } + }); + }, +}); + +export const selectedModelSelector = (state: RootState) => { + const { selectedModelName } = state.models; + const selectedModel = selectModelsById(state, selectedModelName); + + return selectedModel ?? null; +}; + +export const { + selectAll: selectModelsAll, + selectById: selectModelsById, + selectEntities: selectModelsEntities, + selectIds: selectModelsIds, + selectTotal: selectModelsTotal, +} = modelsAdapter.getSelectors((state) => state.models); + +export const { modelAdded, modelSelected } = modelsSlice.actions; + +export default modelsSlice.reducer; diff --git a/invokeai/frontend/web/src/features/system/store/modelsPersistBlacklist.ts b/invokeai/frontend/web/src/features/system/store/modelsPersistBlacklist.ts new file mode 100644 index 0000000000..26b61f5a9b --- /dev/null +++ b/invokeai/frontend/web/src/features/system/store/modelsPersistBlacklist.ts @@ -0,0 +1,10 @@ +import { ModelsState } from './modelSlice'; + +/** + * Models slice persist blacklist + */ +const itemsToBlacklist: (keyof ModelsState)[] = ['entities', 'ids']; + +export const modelsBlacklist = itemsToBlacklist.map( + (blacklistItem) => `models.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/system/store/systemPersistsBlacklist.ts b/invokeai/frontend/web/src/features/system/store/systemPersistsBlacklist.ts new file mode 100644 index 0000000000..31ac7a43e6 --- /dev/null +++ b/invokeai/frontend/web/src/features/system/store/systemPersistsBlacklist.ts @@ -0,0 +1,26 @@ +import { SystemState } from './systemSlice'; + +/** + * System slice persist blacklist + */ +const itemsToBlacklist: (keyof SystemState)[] = [ + 'currentIteration', + 'currentStatus', + 'currentStep', + 'isCancelable', + 'isConnected', + 'isESRGANAvailable', + 'isGFPGANAvailable', + 'isProcessing', + 'socketId', + 'totalIterations', + 'totalSteps', + 'openModel', + 'isCancelScheduled', + 'sessionId', + 'progressImage', +]; + +export const systemBlacklist = itemsToBlacklist.map( + (blacklistItem) => `system.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts index 3293869d58..f52a199aec 100644 --- a/invokeai/frontend/web/src/features/system/store/systemSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/systemSlice.ts @@ -2,7 +2,23 @@ import { ExpandedIndex, UseToastOptions } from '@chakra-ui/react'; import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import * as InvokeAI from 'app/invokeai'; +import { + generatorProgress, + invocationComplete, + invocationError, + invocationStarted, + socketConnected, + socketDisconnected, + socketSubscribed, + socketUnsubscribed, +} from 'services/events/actions'; + import i18n from 'i18n'; +import { isImageOutput } from 'services/types/guards'; +import { ProgressImage } from 'services/events/types'; +import { initialImageSelected } from 'features/parameters/store/generationSlice'; +import { makeToast } from '../hooks/useToastWatcher'; +import { sessionCanceled, sessionInvoked } from 'services/thunks/session'; export type LogLevel = 'info' | 'warning' | 'error'; @@ -56,6 +72,30 @@ export interface SystemState cancelType: CancelType; cancelAfter: number | null; }; + /** + * The current progress image + */ + progressImage: ProgressImage | null; + /** + * The current socket session id + */ + sessionId: string | null; + /** + * Cancel strategy + */ + cancelType: CancelType; + /** + * Whether or not a scheduled cancelation is pending + */ + isCancelScheduled: boolean; + /** + * Array of node IDs that we want to handle when events received + */ + subscribedNodeIds: string[]; + /** + * Whether or not URLs should be transformed to use a different host + */ + shouldTransformUrls: boolean; } const initialSystemState: SystemState = { @@ -98,6 +138,12 @@ const initialSystemState: SystemState = { cancelType: 'immediate', cancelAfter: null, }, + progressImage: null, + sessionId: null, + cancelType: 'immediate', + isCancelScheduled: false, + subscribedNodeIds: [], + shouldTransformUrls: false, }; export const systemSlice = createSlice({ @@ -271,6 +317,203 @@ export const systemSlice = createSlice({ setCancelAfter: (state, action: PayloadAction) => { state.cancelOptions.cancelAfter = action.payload; }, + /** + * A cancel was scheduled + */ + cancelScheduled: (state) => { + state.isCancelScheduled = true; + }, + /** + * The scheduled cancel was aborted + */ + scheduledCancelAborted: (state) => { + state.isCancelScheduled = false; + }, + /** + * The cancel type was changed + */ + cancelTypeChanged: (state, action: PayloadAction) => { + state.cancelType = action.payload; + }, + /** + * The array of subscribed node ids was changed + */ + subscribedNodeIdsSet: (state, action: PayloadAction) => { + state.subscribedNodeIds = action.payload; + }, + /** + * `shouldTransformUrls` was changed + */ + shouldTransformUrlsChanged: (state, action: PayloadAction) => { + state.shouldTransformUrls = action.payload; + }, + }, + extraReducers(builder) { + /** + * Socket Subscribed + */ + builder.addCase(socketSubscribed, (state, action) => { + state.sessionId = action.payload.sessionId; + }); + + /** + * Socket Unsubscribed + */ + builder.addCase(socketUnsubscribed, (state) => { + state.sessionId = null; + }); + + /** + * Socket Connected + */ + builder.addCase(socketConnected, (state, action) => { + const { timestamp } = action.payload; + + state.isConnected = true; + state.currentStatus = i18n.t('common.statusConnected'); + state.log.push({ + timestamp, + message: `Connected to server`, + level: 'info', + }); + state.toastQueue.push( + makeToast({ title: i18n.t('toast.connected'), status: 'success' }) + ); + }); + + /** + * Socket Disconnected + */ + builder.addCase(socketDisconnected, (state, action) => { + const { timestamp } = action.payload; + + state.isConnected = false; + state.currentStatus = i18n.t('common.statusDisconnected'); + state.log.push({ + timestamp, + message: `Disconnected from server`, + level: 'error', + }); + state.toastQueue.push( + makeToast({ title: i18n.t('toast.disconnected'), status: 'error' }) + ); + }); + + /** + * Invocation Started + */ + builder.addCase(invocationStarted, (state) => { + state.isProcessing = true; + state.isCancelable = true; + state.currentStatusHasSteps = false; + state.currentStatus = i18n.t('common.statusGenerating'); + }); + + /** + * Generator Progress + */ + builder.addCase(generatorProgress, (state, action) => { + const { + step, + total_steps, + progress_image, + invocation, + graph_execution_state_id, + } = action.payload.data; + + state.currentStatusHasSteps = true; + state.currentStep = step + 1; // TODO: step starts at -1, think this is a bug + state.totalSteps = total_steps; + state.progressImage = progress_image ?? null; + }); + + /** + * Invocation Complete + */ + builder.addCase(invocationComplete, (state, action) => { + const { data, timestamp } = action.payload; + + state.isProcessing = false; + state.currentStep = 0; + state.totalSteps = 0; + state.progressImage = null; + state.currentStatus = i18n.t('common.statusProcessingComplete'); + + // TODO: handle logging for other invocation types + if (isImageOutput(data.result)) { + state.log.push({ + timestamp, + message: `Generated: ${data.result.image.image_name}`, + level: 'info', + }); + } + }); + + /** + * Invocation Error + */ + builder.addCase(invocationError, (state, action) => { + const { data, timestamp } = action.payload; + + state.log.push({ + timestamp, + message: `Server error: ${data.error}`, + level: 'error', + }); + + state.wasErrorSeen = true; + state.progressImage = null; + state.isProcessing = false; + + state.toastQueue.push( + makeToast({ title: i18n.t('toast.serverError'), status: 'error' }) + ); + + state.log.push({ + timestamp, + message: `Server error: ${data.error}`, + level: 'error', + }); + }); + + /** + * Session Invoked - PENDING + */ + + builder.addCase(sessionInvoked.pending, (state) => { + state.currentStatus = i18n.t('common.statusPreparing'); + }); + + /** + * Session Canceled + */ + builder.addCase(sessionCanceled.fulfilled, (state, action) => { + const { timestamp } = action.payload; + + state.isProcessing = false; + state.isCancelable = false; + state.isCancelScheduled = false; + state.currentStep = 0; + state.totalSteps = 0; + state.progressImage = null; + + state.toastQueue.push( + makeToast({ title: i18n.t('toast.canceled'), status: 'warning' }) + ); + + state.log.push({ + timestamp, + message: `Processing canceled`, + level: 'warning', + }); + }); + + /** + * Initial Image Selected + */ + builder.addCase(initialImageSelected, (state) => { + state.toastQueue.push(makeToast(i18n.t('toast.sentToImageToImage'))); + }); }, }); @@ -306,6 +549,11 @@ export const { setOpenModel, setCancelType, setCancelAfter, + cancelScheduled, + scheduledCancelAborted, + cancelTypeChanged, + subscribedNodeIdsSet, + shouldTransformUrlsChanged, } = systemSlice.actions; export default systemSlice.reducer; diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx index 6076b0944f..e0dd3eadd3 100644 --- a/invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx +++ b/invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx @@ -15,9 +15,7 @@ const floatingGalleryButtonSelector = createSelector( return { shouldPinGallery, - shouldShowGalleryButton: - (!shouldPinGallery || !shouldShowGallery) && - ['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName), + shouldShowGalleryButton: !shouldPinGallery || !shouldShowGallery, }; }, { memoizeOptions: { resultEqualityCheck: isEqual } } diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx index cec9ab2918..06ac904bb1 100644 --- a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx +++ b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx @@ -39,7 +39,7 @@ export const floatingParametersPanelButtonSelector = createSelector( const shouldShowParametersPanelButton = !canvasBetaLayoutCheck && (!shouldPinParametersPanel || !shouldShowParametersPanel) && - ['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName); + ['linear', 'unifiedCanvas'].includes(activeTabName); return { shouldPinParametersPanel, diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index 18db74791b..8603d33d06 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -11,29 +11,20 @@ import { } from '@chakra-ui/react'; import { RootState } from 'app/store'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; -import NodesWIP from 'common/components/WorkInProgress/NodesWIP'; -import { PostProcessingWIP } from 'common/components/WorkInProgress/PostProcessingWIP'; -import TrainingWIP from 'common/components/WorkInProgress/Training'; import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice'; import { InvokeTabName } from 'features/ui/store/tabMap'; import { setActiveTab, togglePanels } from 'features/ui/store/uiSlice'; import { ReactNode, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; -import { - MdDeviceHub, - MdFlashOn, - MdGridOn, - MdPhotoFilter, - MdPhotoLibrary, - MdTextFields, -} from 'react-icons/md'; +import { MdDeviceHub, MdGridOn } from 'react-icons/md'; import { activeTabIndexSelector } from '../store/uiSelectors'; -import ImageToImageWorkarea from 'features/ui/components/tabs/ImageToImage/ImageToImageWorkarea'; -import TextToImageWorkarea from 'features/ui/components/tabs/TextToImage/TextToImageWorkarea'; import UnifiedCanvasWorkarea from 'features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasWorkarea'; import { useTranslation } from 'react-i18next'; import { ResourceKey } from 'i18next'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; +import NodeEditor from 'features/nodes/components/NodeEditor'; +import LinearWorkarea from './tabs/Linear/LinearWorkarea'; +import { FaImage } from 'react-icons/fa'; export interface InvokeTabInfo { id: InvokeTabName; @@ -45,38 +36,26 @@ const tabIconStyles: ChakraProps['sx'] = { boxSize: 6, }; -const tabInfo: InvokeTabInfo[] = [ - { - id: 'txt2img', - icon: , - workarea: , - }, - { - id: 'img2img', - icon: , - workarea: , - }, - { - id: 'unifiedCanvas', - icon: , - workarea: , - }, - { - id: 'nodes', - icon: , - workarea: , - }, - { - id: 'postprocessing', - icon: , - workarea: , - }, - { - id: 'training', - icon: , - workarea: , - }, -]; +const buildTabs = (disabledTabs: InvokeTabName[]): InvokeTabInfo[] => { + const tabs: InvokeTabInfo[] = [ + { + id: 'linear', + icon: , + workarea: , + }, + { + id: 'unifiedCanvas', + icon: , + workarea: , + }, + { + id: 'nodes', + icon: , + workarea: , + }, + ]; + return tabs.filter((tab) => !disabledTabs.includes(tab.id)); +}; export default function InvokeTabs() { const activeTab = useAppSelector(activeTabIndexSelector); @@ -85,13 +64,10 @@ export default function InvokeTabs() { (state: RootState) => state.lightbox.isLightboxOpen ); - const shouldPinGallery = useAppSelector( - (state: RootState) => state.ui.shouldPinGallery - ); + const { shouldPinGallery, disabledTabs, shouldPinParametersPanel } = + useAppSelector((state: RootState) => state.ui); - const shouldPinParametersPanel = useAppSelector( - (state: RootState) => state.ui.shouldPinParametersPanel - ); + const activeTabs = buildTabs(disabledTabs); const { t } = useTranslation(); @@ -109,18 +85,6 @@ export default function InvokeTabs() { dispatch(setActiveTab(2)); }); - useHotkeys('4', () => { - dispatch(setActiveTab(3)); - }); - - useHotkeys('5', () => { - dispatch(setActiveTab(4)); - }); - - useHotkeys('6', () => { - dispatch(setActiveTab(5)); - }); - // Lightbox Hotkey useHotkeys( 'z', @@ -142,7 +106,7 @@ export default function InvokeTabs() { const tabs = useMemo( () => - tabInfo.map((tab) => ( + activeTabs.map((tab) => ( )), - [t] + [t, activeTabs] ); const tabPanels = useMemo( () => - tabInfo.map((tab) => {tab.workarea}), - [] + activeTabs.map((tab) => {tab.workarea}), + [activeTabs] ); return ( @@ -174,8 +138,11 @@ export default function InvokeTabs() { dispatch(setActiveTab(index)); }} flexGrow={1} + isLazy > - {tabs} + + {tabs} + {tabPanels} ); diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeWorkarea.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeWorkarea.tsx index ea4cd5ba1a..13f551e904 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeWorkarea.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeWorkarea.tsx @@ -1,7 +1,7 @@ import { Box, BoxProps, Flex } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; -import { setInitialImage } from 'features/parameters/store/generationSlice'; +import { initialImageSelected } from 'features/parameters/store/generationSlice'; import { activeTabNameSelector, uiSelector, @@ -46,9 +46,7 @@ const InvokeWorkarea = (props: InvokeWorkareaProps) => { const uuid = e.dataTransfer.getData('invokeai/imageUuid'); const image = getImageByUuid(uuid); if (!image) return; - if (activeTabName === 'img2img') { - dispatch(setInitialImage(image)); - } else if (activeTabName === 'unifiedCanvas') { + if (activeTabName === 'unifiedCanvas') { dispatch(setInitialCanvasImage(image)); } }; diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx index 1f0bcaead3..77cffa814a 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx @@ -96,7 +96,6 @@ const ParametersPanel = ({ children }: ParametersPanelProps) => { onClose={closeParametersPanel} isPinned={shouldPinParametersPanel || isLightboxOpen} sx={{ - borderColor: 'base.700', p: shouldPinParametersPanel ? 0 : 4, bg: 'base.900', }} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageContent.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageContent.tsx deleted file mode 100644 index db62d2ed80..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageContent.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { ChakraProps, Flex, Grid } from '@chakra-ui/react'; -import { RootState } from 'app/store'; -import { useAppSelector } from 'app/storeHooks'; -import ImageUploadButton from 'common/components/ImageUploaderButton'; -import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay'; -import InitImagePreview from './InitImagePreview'; - -const workareaSplitViewStyle: ChakraProps['sx'] = { - flexDirection: 'column', - height: '100%', - width: '100%', - gap: 4, - - padding: 4, -}; - -const ImageToImageContent = () => { - const initialImage = useAppSelector( - (state: RootState) => state.generation.initialImage - ); - - const imageToImageComponent = initialImage ? ( - - - - ) : ( - - ); - - return ( - - - {imageToImageComponent} - - - - - - ); -}; - -export default ImageToImageContent; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageParameters.tsx deleted file mode 100644 index 4989ef034b..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageParameters.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Flex } from '@chakra-ui/react'; -import { Feature } from 'app/features'; -import FaceRestoreSettings from 'features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreSettings'; -import FaceRestoreToggle from 'features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreToggle'; -import ImageToImageOutputSettings from 'features/parameters/components/AdvancedParameters/Output/ImageToImageOutputSettings'; -import SymmetrySettings from 'features/parameters/components/AdvancedParameters/Output/SymmetrySettings'; -import SymmetryToggle from 'features/parameters/components/AdvancedParameters/Output/SymmetryToggle'; -import SeedSettings from 'features/parameters/components/AdvancedParameters/Seed/SeedSettings'; -import UpscaleSettings from 'features/parameters/components/AdvancedParameters/Upscale/UpscaleSettings'; -import UpscaleToggle from 'features/parameters/components/AdvancedParameters/Upscale/UpscaleToggle'; -import GenerateVariationsToggle from 'features/parameters/components/AdvancedParameters/Variations/GenerateVariations'; -import VariationsSettings from 'features/parameters/components/AdvancedParameters/Variations/VariationsSettings'; -import MainSettings from 'features/parameters/components/MainParameters/MainSettings'; -import ParametersAccordion from 'features/parameters/components/ParametersAccordion'; -import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons'; -import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput'; -import PromptInput from 'features/parameters/components/PromptInput/PromptInput'; -import { useTranslation } from 'react-i18next'; -import ImageToImageSettings from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageSettings'; -import { memo } from 'react'; - -const ImageToImageParameters = () => { - const { t } = useTranslation(); - - const imageToImageAccordions = { - general: { - header: `${t('parameters.general')}`, - feature: undefined, - content: , - }, - imageToImage: { - header: `${t('parameters.imageToImage')}`, - feature: undefined, - content: , - }, - seed: { - header: `${t('parameters.seed')}`, - feature: Feature.SEED, - content: , - }, - variations: { - header: `${t('parameters.variations')}`, - feature: Feature.VARIATIONS, - content: , - additionalHeaderComponents: , - }, - face_restore: { - header: `${t('parameters.faceRestoration')}`, - feature: Feature.FACE_CORRECTION, - content: , - additionalHeaderComponents: , - }, - upscale: { - header: `${t('parameters.upscaling')}`, - feature: Feature.UPSCALE, - content: , - additionalHeaderComponents: , - }, - symmetry: { - header: `${t('parameters.symmetry')}`, - content: , - additionalHeaderComponents: , - }, - other: { - header: `${t('parameters.otherOptions')}`, - feature: Feature.OTHER, - content: , - }, - }; - - return ( - - - - - - - ); -}; - -export default memo(ImageToImageParameters); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageWorkarea.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageWorkarea.tsx deleted file mode 100644 index ebc6f50ff2..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageWorkarea.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import InvokeWorkarea from 'features/ui/components/InvokeWorkarea'; -import ImageToImageContent from './ImageToImageContent'; -import ImageToImageParameters from './ImageToImageParameters'; - -export default function ImageToImageWorkarea() { - return ( - }> - - - ); -} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/InitImagePreview.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/InitImagePreview.tsx deleted file mode 100644 index 3add963c01..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/InitImagePreview.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { Flex, Image, Text, useToast } from '@chakra-ui/react'; -import { RootState } from 'app/store'; -import { useAppDispatch, useAppSelector } from 'app/storeHooks'; -import ImageUploaderIconButton from 'common/components/ImageUploaderIconButton'; -import CurrentImageHidden from 'features/gallery/components/CurrentImageHidden'; -import { clearInitialImage } from 'features/parameters/store/generationSlice'; -import { useTranslation } from 'react-i18next'; - -export default function InitImagePreview() { - const initialImage = useAppSelector( - (state: RootState) => state.generation.initialImage - ); - - const { shouldHidePreview } = useAppSelector((state: RootState) => state.ui); - - const { t } = useTranslation(); - - const dispatch = useAppDispatch(); - - const toast = useToast(); - - const alertMissingInitImage = () => { - toast({ - title: t('toast.parametersFailed'), - description: t('toast.parametersFailedDesc'), - status: 'error', - isClosable: true, - }); - dispatch(clearInitialImage()); - }; - - return ( - <> - - - {t('parameters.initialImage')} - - - - {initialImage && ( - - } - onError={alertMissingInitImage} - /> - - )} - - ); -} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageContent.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearContent.tsx similarity index 85% rename from invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageContent.tsx rename to invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearContent.tsx index 886e3a5331..8860956aeb 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageContent.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearContent.tsx @@ -1,7 +1,7 @@ import { Box, Flex } from '@chakra-ui/react'; import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay'; -const TextToImageContent = () => { +const LinearContent = () => { return ( { ); }; -export default TextToImageContent; +export default LinearContent; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearParameters.tsx similarity index 65% rename from invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageParameters.tsx rename to invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearParameters.tsx index 126dd10228..67ec737a12 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearParameters.tsx @@ -1,61 +1,61 @@ import { Flex } from '@chakra-ui/react'; import { Feature } from 'app/features'; -import FaceRestoreSettings from 'features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreSettings'; -import FaceRestoreToggle from 'features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreToggle'; +import ImageToImageSettings from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageSettings'; +import ImageToImageToggle from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageToggle'; import OutputSettings from 'features/parameters/components/AdvancedParameters/Output/OutputSettings'; import SymmetrySettings from 'features/parameters/components/AdvancedParameters/Output/SymmetrySettings'; import SymmetryToggle from 'features/parameters/components/AdvancedParameters/Output/SymmetryToggle'; import SeedSettings from 'features/parameters/components/AdvancedParameters/Seed/SeedSettings'; -import UpscaleSettings from 'features/parameters/components/AdvancedParameters/Upscale/UpscaleSettings'; -import UpscaleToggle from 'features/parameters/components/AdvancedParameters/Upscale/UpscaleToggle'; import GenerateVariationsToggle from 'features/parameters/components/AdvancedParameters/Variations/GenerateVariations'; import VariationsSettings from 'features/parameters/components/AdvancedParameters/Variations/VariationsSettings'; import MainSettings from 'features/parameters/components/MainParameters/MainSettings'; -import ParametersAccordion from 'features/parameters/components/ParametersAccordion'; +import ParametersAccordion, { + ParametersAccordionItems, +} from 'features/parameters/components/ParametersAccordion'; import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons'; import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput'; import PromptInput from 'features/parameters/components/PromptInput/PromptInput'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -const TextToImageParameters = () => { +const LinearParameters = () => { const { t } = useTranslation(); - const textToImageAccordions = { + const linearAccordions: ParametersAccordionItems = { general: { + name: 'general', header: `${t('parameters.general')}`, feature: undefined, content: , }, seed: { + name: 'seed', header: `${t('parameters.seed')}`, feature: Feature.SEED, content: , }, + imageToImage: { + name: 'imageToImage', + header: `${t('parameters.imageToImage')}`, + feature: undefined, + content: , + additionalHeaderComponents: , + }, variations: { + name: 'variations', header: `${t('parameters.variations')}`, feature: Feature.VARIATIONS, content: , additionalHeaderComponents: , }, - face_restore: { - header: `${t('parameters.faceRestoration')}`, - feature: Feature.FACE_CORRECTION, - content: , - additionalHeaderComponents: , - }, - upscale: { - header: `${t('parameters.upscaling')}`, - feature: Feature.UPSCALE, - content: , - additionalHeaderComponents: , - }, symmetry: { + name: 'symmetry', header: `${t('parameters.symmetry')}`, content: , additionalHeaderComponents: , }, other: { + name: 'other', header: `${t('parameters.otherOptions')}`, feature: Feature.OTHER, content: , @@ -67,9 +67,9 @@ const TextToImageParameters = () => { - + ); }; -export default memo(TextToImageParameters); +export default memo(LinearParameters); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearWorkarea.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearWorkarea.tsx new file mode 100644 index 0000000000..f75065b6a3 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearWorkarea.tsx @@ -0,0 +1,11 @@ +import InvokeWorkarea from 'features/ui/components/InvokeWorkarea'; +import LinearContent from './LinearContent'; +import LinearParameters from './LinearParameters'; + +export default function LinearWorkarea() { + return ( + }> + + + ); +} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageWorkarea.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageWorkarea.tsx deleted file mode 100644 index eb95c96be5..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageWorkarea.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import InvokeWorkarea from 'features/ui/components/InvokeWorkarea'; -import TextToImageContent from './TextToImageContent'; -import TextToImageParameters from './TextToImageParameters'; - -export default function TextToImageWorkarea() { - return ( - }> - - - ); -} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx index 0d49eafebd..a8c917d675 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx @@ -10,7 +10,9 @@ import SeedSettings from 'features/parameters/components/AdvancedParameters/Seed import GenerateVariationsToggle from 'features/parameters/components/AdvancedParameters/Variations/GenerateVariations'; import VariationsSettings from 'features/parameters/components/AdvancedParameters/Variations/VariationsSettings'; import MainSettings from 'features/parameters/components/MainParameters/MainSettings'; -import ParametersAccordion from 'features/parameters/components/ParametersAccordion'; +import ParametersAccordion, { + ParametersAccordionItems, +} from 'features/parameters/components/ParametersAccordion'; import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons'; import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput'; import PromptInput from 'features/parameters/components/PromptInput/PromptInput'; @@ -19,44 +21,52 @@ import { useTranslation } from 'react-i18next'; export default function UnifiedCanvasParameters() { const { t } = useTranslation(); - const unifiedCanvasAccordions = { + const unifiedCanvasAccordions: ParametersAccordionItems = { general: { + name: 'general', header: `${t('parameters.general')}`, feature: undefined, content: , }, unifiedCanvasImg2Img: { + name: 'unifiedCanvasImg2Img', header: `${t('parameters.imageToImage')}`, feature: undefined, content: , }, seed: { + name: 'seed', header: `${t('parameters.seed')}`, feature: Feature.SEED, content: , }, boundingBox: { + name: 'boundingBox', header: `${t('parameters.boundingBoxHeader')}`, feature: Feature.BOUNDING_BOX, content: , }, seamCorrection: { + name: 'seamCorrection', header: `${t('parameters.seamCorrectionHeader')}`, feature: Feature.SEAM_CORRECTION, content: , }, infillAndScaling: { + name: 'infillAndScaling', header: `${t('parameters.infillScalingHeader')}`, feature: Feature.INFILL_AND_SCALING, content: , }, variations: { + name: 'variations', header: `${t('parameters.variations')}`, feature: Feature.VARIATIONS, content: , additionalHeaderComponents: , }, symmetry: { + name: 'symmetry', header: `${t('parameters.symmetry')}`, content: , additionalHeaderComponents: , @@ -68,7 +78,7 @@ export default function UnifiedCanvasParameters() { - + ); } diff --git a/invokeai/frontend/web/src/features/ui/store/extraReducers.ts b/invokeai/frontend/web/src/features/ui/store/extraReducers.ts new file mode 100644 index 0000000000..9b134e1476 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/store/extraReducers.ts @@ -0,0 +1,13 @@ +import { InvokeTabName, tabMap } from './tabMap'; +import { UIState } from './uiTypes'; + +export const setActiveTabReducer = ( + state: UIState, + newActiveTab: number | InvokeTabName +) => { + if (typeof newActiveTab === 'number') { + state.activeTab = newActiveTab; + } else { + state.activeTab = tabMap.indexOf(newActiveTab); + } +}; diff --git a/invokeai/frontend/web/src/features/ui/store/tabMap.ts b/invokeai/frontend/web/src/features/ui/store/tabMap.ts index d30799a80f..7584878e02 100644 --- a/invokeai/frontend/web/src/features/ui/store/tabMap.ts +++ b/invokeai/frontend/web/src/features/ui/store/tabMap.ts @@ -1,10 +1,11 @@ export const tabMap = [ - 'txt2img', - 'img2img', + // 'txt2img', + // 'img2img', + 'linear', 'unifiedCanvas', 'nodes', - 'postprocessing', - 'training', + // 'postprocessing', + // 'training', ] as const; export type InvokeTabName = (typeof tabMap)[number]; diff --git a/invokeai/frontend/web/src/features/ui/store/uiPersistBlacklist.ts b/invokeai/frontend/web/src/features/ui/store/uiPersistBlacklist.ts new file mode 100644 index 0000000000..64516c6372 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/store/uiPersistBlacklist.ts @@ -0,0 +1,10 @@ +import { UIState } from './uiTypes'; + +/** + * UI slice persist blacklist + */ +const itemsToBlacklist: (keyof UIState)[] = []; + +export const uiBlacklist = itemsToBlacklist.map( + (blacklistItem) => `ui.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index f90e339697..9cdf26c042 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -1,5 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; +import { initialImageSelected } from 'features/parameters/store/generationSlice'; +import { setActiveTabReducer } from './extraReducers'; import { InvokeTabName, tabMap } from './tabMap'; import { AddNewModelType, UIState } from './uiTypes'; @@ -17,6 +19,10 @@ const initialtabsState: UIState = { shouldPinGallery: true, shouldShowGallery: true, shouldHidePreview: false, + disabledParameterPanels: [], + disabledTabs: [], + openLinearAccordionItems: [], + openUnifiedCanvasAccordionItems: [], }; const initialState: UIState = initialtabsState; @@ -26,11 +32,7 @@ export const uiSlice = createSlice({ initialState, reducers: { setActiveTab: (state, action: PayloadAction) => { - if (typeof action.payload === 'number') { - state.activeTab = action.payload; - } else { - state.activeTab = tabMap.indexOf(action.payload); - } + setActiveTabReducer(state, action.payload); }, setCurrentTheme: (state, action: PayloadAction) => { state.currentTheme = action.payload; @@ -96,6 +98,21 @@ export const uiSlice = createSlice({ state.shouldShowParametersPanel = true; } }, + setDisabledPanels: (state, action: PayloadAction) => { + state.disabledParameterPanels = action.payload; + }, + setDisabledTabs: (state, action: PayloadAction) => { + state.disabledTabs = action.payload; + }, + openAccordionItemsChanged: (state, action: PayloadAction) => { + if (tabMap[state.activeTab] === 'linear') { + state.openLinearAccordionItems = action.payload; + } + + if (tabMap[state.activeTab] === 'unifiedCanvas') { + state.openUnifiedCanvasAccordionItems = action.payload; + } + }, }, }); @@ -118,6 +135,9 @@ export const { togglePinParametersPanel, toggleParametersPanel, toggleGalleryPanel, + setDisabledPanels, + setDisabledTabs, + openAccordionItemsChanged, } = uiSlice.actions; export default uiSlice.reducer; diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index 2a8e8f4231..a1cba603bb 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -1,3 +1,5 @@ +import { InvokeTabName } from './tabMap'; + export type AddNewModelType = 'ckpt' | 'diffusers' | null; export interface UIState { @@ -14,4 +16,8 @@ export interface UIState { shouldHidePreview: boolean; shouldPinGallery: boolean; shouldShowGallery: boolean; + disabledParameterPanels: string[]; + disabledTabs: InvokeTabName[]; + openLinearAccordionItems: number[]; + openUnifiedCanvasAccordionItems: number[]; } diff --git a/invokeai/frontend/web/src/services/api/core/ApiError.ts b/invokeai/frontend/web/src/services/api/core/ApiError.ts new file mode 100644 index 0000000000..41a9605a3a --- /dev/null +++ b/invokeai/frontend/web/src/services/api/core/ApiError.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ApiRequestOptions } from './ApiRequestOptions'; +import type { ApiResult } from './ApiResult'; + +export class ApiError extends Error { + public readonly url: string; + public readonly status: number; + public readonly statusText: string; + public readonly body: any; + public readonly request: ApiRequestOptions; + + constructor(request: ApiRequestOptions, response: ApiResult, message: string) { + super(message); + + this.name = 'ApiError'; + this.url = response.url; + this.status = response.status; + this.statusText = response.statusText; + this.body = response.body; + this.request = request; + } +} diff --git a/invokeai/frontend/web/src/services/api/core/ApiRequestOptions.ts b/invokeai/frontend/web/src/services/api/core/ApiRequestOptions.ts new file mode 100644 index 0000000000..c9350406a1 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/core/ApiRequestOptions.ts @@ -0,0 +1,16 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ApiRequestOptions = { + readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH'; + readonly url: string; + readonly path?: Record; + readonly cookies?: Record; + readonly headers?: Record; + readonly query?: Record; + readonly formData?: Record; + readonly body?: any; + readonly mediaType?: string; + readonly responseHeader?: string; + readonly errors?: Record; +}; diff --git a/invokeai/frontend/web/src/services/api/core/ApiResult.ts b/invokeai/frontend/web/src/services/api/core/ApiResult.ts new file mode 100644 index 0000000000..91f60ae082 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/core/ApiResult.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ApiResult = { + readonly url: string; + readonly ok: boolean; + readonly status: number; + readonly statusText: string; + readonly body: any; +}; diff --git a/invokeai/frontend/web/src/services/api/core/CancelablePromise.ts b/invokeai/frontend/web/src/services/api/core/CancelablePromise.ts new file mode 100644 index 0000000000..b923479fea --- /dev/null +++ b/invokeai/frontend/web/src/services/api/core/CancelablePromise.ts @@ -0,0 +1,128 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export class CancelError extends Error { + + constructor(message: string) { + super(message); + this.name = 'CancelError'; + } + + public get isCancelled(): boolean { + return true; + } +} + +export interface OnCancel { + readonly isResolved: boolean; + readonly isRejected: boolean; + readonly isCancelled: boolean; + + (cancelHandler: () => void): void; +} + +export class CancelablePromise implements Promise { + readonly [Symbol.toStringTag]!: string; + + private _isResolved: boolean; + private _isRejected: boolean; + private _isCancelled: boolean; + private readonly _cancelHandlers: (() => void)[]; + private readonly _promise: Promise; + private _resolve?: (value: T | PromiseLike) => void; + private _reject?: (reason?: any) => void; + + constructor( + executor: ( + resolve: (value: T | PromiseLike) => void, + reject: (reason?: any) => void, + onCancel: OnCancel + ) => void + ) { + this._isResolved = false; + this._isRejected = false; + this._isCancelled = false; + this._cancelHandlers = []; + this._promise = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + + const onResolve = (value: T | PromiseLike): void => { + if (this._isResolved || this._isRejected || this._isCancelled) { + return; + } + this._isResolved = true; + this._resolve?.(value); + }; + + const onReject = (reason?: any): void => { + if (this._isResolved || this._isRejected || this._isCancelled) { + return; + } + this._isRejected = true; + this._reject?.(reason); + }; + + const onCancel = (cancelHandler: () => void): void => { + if (this._isResolved || this._isRejected || this._isCancelled) { + return; + } + this._cancelHandlers.push(cancelHandler); + }; + + Object.defineProperty(onCancel, 'isResolved', { + get: (): boolean => this._isResolved, + }); + + Object.defineProperty(onCancel, 'isRejected', { + get: (): boolean => this._isRejected, + }); + + Object.defineProperty(onCancel, 'isCancelled', { + get: (): boolean => this._isCancelled, + }); + + return executor(onResolve, onReject, onCancel as OnCancel); + }); + } + + public then( + onFulfilled?: ((value: T) => TResult1 | PromiseLike) | null, + onRejected?: ((reason: any) => TResult2 | PromiseLike) | null + ): Promise { + return this._promise.then(onFulfilled, onRejected); + } + + public catch( + onRejected?: ((reason: any) => TResult | PromiseLike) | null + ): Promise { + return this._promise.catch(onRejected); + } + + public finally(onFinally?: (() => void) | null): Promise { + return this._promise.finally(onFinally); + } + + public cancel(): void { + if (this._isResolved || this._isRejected || this._isCancelled) { + return; + } + this._isCancelled = true; + if (this._cancelHandlers.length) { + try { + for (const cancelHandler of this._cancelHandlers) { + cancelHandler(); + } + } catch (error) { + console.warn('Cancellation threw an error', error); + return; + } + } + this._cancelHandlers.length = 0; + this._reject?.(new CancelError('Request aborted')); + } + + public get isCancelled(): boolean { + return this._isCancelled; + } +} diff --git a/invokeai/frontend/web/src/services/api/core/OpenAPI.ts b/invokeai/frontend/web/src/services/api/core/OpenAPI.ts new file mode 100644 index 0000000000..ba65dcd55d --- /dev/null +++ b/invokeai/frontend/web/src/services/api/core/OpenAPI.ts @@ -0,0 +1,31 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ApiRequestOptions } from './ApiRequestOptions'; + +type Resolver = (options: ApiRequestOptions) => Promise; +type Headers = Record; + +export type OpenAPIConfig = { + BASE: string; + VERSION: string; + WITH_CREDENTIALS: boolean; + CREDENTIALS: 'include' | 'omit' | 'same-origin'; + TOKEN?: string | Resolver; + USERNAME?: string | Resolver; + PASSWORD?: string | Resolver; + HEADERS?: Headers | Resolver; + ENCODE_PATH?: (path: string) => string; +}; + +export const OpenAPI: OpenAPIConfig = { + BASE: '', + VERSION: '1.0.0', + WITH_CREDENTIALS: false, + CREDENTIALS: 'include', + TOKEN: undefined, + USERNAME: undefined, + PASSWORD: undefined, + HEADERS: undefined, + ENCODE_PATH: undefined, +}; diff --git a/invokeai/frontend/web/src/services/api/core/request.ts b/invokeai/frontend/web/src/services/api/core/request.ts new file mode 100644 index 0000000000..745f687743 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/core/request.ts @@ -0,0 +1,351 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Custom `request.ts` file for OpenAPI code generator. + * + * Patches the request logic in such a way that we can extract headers from requests. + * + * Copied from https://github.com/ferdikoomen/openapi-typescript-codegen/issues/829#issuecomment-1228224477 + * + * This file should be excluded in `tsconfig.json` and ignored by prettier/eslint! + */ + +import axios from 'axios'; +import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; +import FormData from 'form-data'; + +import { ApiError } from './ApiError'; +import type { ApiRequestOptions } from './ApiRequestOptions'; +import type { ApiResult } from './ApiResult'; +import { CancelablePromise } from './CancelablePromise'; +import type { OnCancel } from './CancelablePromise'; +import type { OpenAPIConfig } from './OpenAPI'; + +export const HEADERS = Symbol('HEADERS'); + +const isDefined = ( + value: T | null | undefined +): value is Exclude => { + return value !== undefined && value !== null; +}; + +const isString = (value: any): value is string => { + return typeof value === 'string'; +}; + +const isStringWithValue = (value: any): value is string => { + return isString(value) && value !== ''; +}; + +const isBlob = (value: any): value is Blob => { + return ( + typeof value === 'object' && + typeof value.type === 'string' && + typeof value.stream === 'function' && + typeof value.arrayBuffer === 'function' && + typeof value.constructor === 'function' && + typeof value.constructor.name === 'string' && + /^(Blob|File)$/.test(value.constructor.name) && + /^(Blob|File)$/.test(value[Symbol.toStringTag]) + ); +}; + +const isFormData = (value: any): value is FormData => { + return value instanceof FormData; +}; + +const isSuccess = (status: number): boolean => { + return status >= 200 && status < 300; +}; + +const base64 = (str: string): string => { + try { + return btoa(str); + } catch (err) { + // @ts-ignore + return Buffer.from(str).toString('base64'); + } +}; + +const getQueryString = (params: Record): string => { + const qs: string[] = []; + + const append = (key: string, value: any) => { + qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); + }; + + const process = (key: string, value: any) => { + if (isDefined(value)) { + if (Array.isArray(value)) { + value.forEach((v) => { + process(key, v); + }); + } else if (typeof value === 'object') { + Object.entries(value).forEach(([k, v]) => { + process(`${key}[${k}]`, v); + }); + } else { + append(key, value); + } + } + }; + + Object.entries(params).forEach(([key, value]) => { + process(key, value); + }); + + if (qs.length > 0) { + return `?${qs.join('&')}`; + } + + return ''; +}; + +const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => { + const encoder = config.ENCODE_PATH || encodeURI; + + const path = options.url + .replace('{api-version}', config.VERSION) + .replace(/{(.*?)}/g, (substring: string, group: string) => { + if (options.path?.hasOwnProperty(group)) { + return encoder(String(options.path[group])); + } + return substring; + }); + + const url = `${config.BASE}${path}`; + if (options.query) { + return `${url}${getQueryString(options.query)}`; + } + return url; +}; + +const getFormData = (options: ApiRequestOptions): FormData | undefined => { + if (options.formData) { + const formData = new FormData(); + + const process = (key: string, value: any) => { + if (isString(value) || isBlob(value)) { + formData.append(key, value); + } else { + formData.append(key, JSON.stringify(value)); + } + }; + + Object.entries(options.formData) + .filter(([_, value]) => isDefined(value)) + .forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach((v) => process(key, v)); + } else { + process(key, value); + } + }); + + return formData; + } + return undefined; +}; + +type Resolver = (options: ApiRequestOptions) => Promise; + +const resolve = async ( + options: ApiRequestOptions, + resolver?: T | Resolver +): Promise => { + if (typeof resolver === 'function') { + return (resolver as Resolver)(options); + } + return resolver; +}; + +const getHeaders = async ( + config: OpenAPIConfig, + options: ApiRequestOptions, + formData?: FormData +): Promise> => { + const token = await resolve(options, config.TOKEN); + const username = await resolve(options, config.USERNAME); + const password = await resolve(options, config.PASSWORD); + const additionalHeaders = await resolve(options, config.HEADERS); + const formHeaders = + (typeof formData?.getHeaders === 'function' && formData?.getHeaders()) || + {}; + + const headers = Object.entries({ + Accept: 'application/json', + ...additionalHeaders, + ...options.headers, + ...formHeaders, + }) + .filter(([_, value]) => isDefined(value)) + .reduce( + (headers, [key, value]) => ({ + ...headers, + [key]: String(value), + }), + {} as Record + ); + + if (isStringWithValue(token)) { + headers['Authorization'] = `Bearer ${token}`; + } + + if (isStringWithValue(username) && isStringWithValue(password)) { + const credentials = base64(`${username}:${password}`); + headers['Authorization'] = `Basic ${credentials}`; + } + + if (options.body) { + if (options.mediaType) { + headers['Content-Type'] = options.mediaType; + } else if (isBlob(options.body)) { + headers['Content-Type'] = options.body.type || 'application/octet-stream'; + } else if (isString(options.body)) { + headers['Content-Type'] = 'text/plain'; + } else if (!isFormData(options.body)) { + headers['Content-Type'] = 'application/json'; + } + } + + return headers; +}; + +const getRequestBody = (options: ApiRequestOptions): any => { + if (options.body) { + return options.body; + } + return undefined; +}; + +const sendRequest = async ( + config: OpenAPIConfig, + options: ApiRequestOptions, + url: string, + body: any, + formData: FormData | undefined, + headers: Record, + onCancel: OnCancel +): Promise> => { + const source = axios.CancelToken.source(); + + const requestConfig: AxiosRequestConfig = { + url, + headers, + data: body ?? formData, + method: options.method, + withCredentials: config.WITH_CREDENTIALS, + cancelToken: source.token, + }; + + onCancel(() => source.cancel('The user aborted a request.')); + + try { + return await axios.request(requestConfig); + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.response) { + return axiosError.response; + } + throw error; + } +}; + +const getResponseHeader = ( + response: AxiosResponse, + responseHeader?: string +): string | undefined => { + if (responseHeader) { + const content = response.headers[responseHeader]; + if (isString(content)) { + return content; + } + } + return undefined; +}; + +const getResponseBody = (response: AxiosResponse): any => { + if (response.status !== 204) { + return response.data; + } + return undefined; +}; + +const catchErrorCodes = ( + options: ApiRequestOptions, + result: ApiResult +): void => { + const errors: Record = { + 400: 'Bad Request', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 500: 'Internal Server Error', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + ...options.errors, + }; + + const error = errors[result.status]; + if (error) { + throw new ApiError(options, result, error); + } + + if (!result.ok) { + throw new ApiError(options, result, 'Generic Error'); + } +}; + +/** + * Request method + * @param config The OpenAPI configuration object + * @param options The request options from the service + * @returns CancelablePromise + * @throws ApiError + */ +export const request = ( + config: OpenAPIConfig, + options: ApiRequestOptions +): CancelablePromise => { + return new CancelablePromise(async (resolve, reject, onCancel) => { + try { + const url = getUrl(config, options); + const formData = getFormData(options); + const body = getRequestBody(options); + const headers = await getHeaders(config, options, formData); + + if (!onCancel.isCancelled) { + const response = await sendRequest( + config, + options, + url, + body, + formData, + headers, + onCancel + ); + const responseBody = getResponseBody(response); + const responseHeader = getResponseHeader( + response, + options.responseHeader + ); + + const result: ApiResult = { + url, + ok: isSuccess(response.status), + status: response.status, + statusText: response.statusText, + body: responseHeader ?? responseBody, + }; + + catchErrorCodes(options, result); + + resolve({ ...result.body, [HEADERS]: response.headers }); + } + } catch (error) { + reject(error); + } + }); +}; diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts new file mode 100644 index 0000000000..f1b84f8465 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -0,0 +1,133 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export { ApiError } from './core/ApiError'; +export { CancelablePromise, CancelError } from './core/CancelablePromise'; +export { OpenAPI } from './core/OpenAPI'; +export type { OpenAPIConfig } from './core/OpenAPI'; + +export type { AddInvocation } from './models/AddInvocation'; +export type { BlurInvocation } from './models/BlurInvocation'; +export type { Body_upload_image } from './models/Body_upload_image'; +export type { CkptModelInfo } from './models/CkptModelInfo'; +export type { CollectInvocation } from './models/CollectInvocation'; +export type { CollectInvocationOutput } from './models/CollectInvocationOutput'; +export type { CreateModelRequest } from './models/CreateModelRequest'; +export type { CropImageInvocation } from './models/CropImageInvocation'; +export type { CvInpaintInvocation } from './models/CvInpaintInvocation'; +export type { DiffusersModelInfo } from './models/DiffusersModelInfo'; +export type { DivideInvocation } from './models/DivideInvocation'; +export type { Edge } from './models/Edge'; +export type { EdgeConnection } from './models/EdgeConnection'; +export type { Graph } from './models/Graph'; +export type { GraphExecutionState } from './models/GraphExecutionState'; +export type { GraphInvocation } from './models/GraphInvocation'; +export type { GraphInvocationOutput } from './models/GraphInvocationOutput'; +export type { HTTPValidationError } from './models/HTTPValidationError'; +export type { ImageField } from './models/ImageField'; +export type { ImageOutput } from './models/ImageOutput'; +export type { ImageResponse } from './models/ImageResponse'; +export type { ImageResponseMetadata } from './models/ImageResponseMetadata'; +export type { ImageToImageInvocation } from './models/ImageToImageInvocation'; +export type { ImageType } from './models/ImageType'; +export type { InpaintInvocation } from './models/InpaintInvocation'; +export type { IntCollectionOutput } from './models/IntCollectionOutput'; +export type { IntOutput } from './models/IntOutput'; +export type { InverseLerpInvocation } from './models/InverseLerpInvocation'; +export type { InvokeAIMetadata } from './models/InvokeAIMetadata'; +export type { IterateInvocation } from './models/IterateInvocation'; +export type { IterateInvocationOutput } from './models/IterateInvocationOutput'; +export type { LatentsField } from './models/LatentsField'; +export type { LatentsOutput } from './models/LatentsOutput'; +export type { LatentsToImageInvocation } from './models/LatentsToImageInvocation'; +export type { LatentsToLatentsInvocation } from './models/LatentsToLatentsInvocation'; +export type { LerpInvocation } from './models/LerpInvocation'; +export type { LoadImageInvocation } from './models/LoadImageInvocation'; +export type { MaskFromAlphaInvocation } from './models/MaskFromAlphaInvocation'; +export type { MaskOutput } from './models/MaskOutput'; +export type { MetadataImageField } from './models/MetadataImageField'; +export type { MetadataLatentsField } from './models/MetadataLatentsField'; +export type { ModelsList } from './models/ModelsList'; +export type { MultiplyInvocation } from './models/MultiplyInvocation'; +export type { NoiseInvocation } from './models/NoiseInvocation'; +export type { NoiseOutput } from './models/NoiseOutput'; +export type { PaginatedResults_GraphExecutionState_ } from './models/PaginatedResults_GraphExecutionState_'; +export type { PaginatedResults_ImageResponse_ } from './models/PaginatedResults_ImageResponse_'; +export type { ParamIntInvocation } from './models/ParamIntInvocation'; +export type { PasteImageInvocation } from './models/PasteImageInvocation'; +export type { PromptOutput } from './models/PromptOutput'; +export type { RandomRangeInvocation } from './models/RandomRangeInvocation'; +export type { RangeInvocation } from './models/RangeInvocation'; +export type { RestoreFaceInvocation } from './models/RestoreFaceInvocation'; +export type { ShowImageInvocation } from './models/ShowImageInvocation'; +export type { SubtractInvocation } from './models/SubtractInvocation'; +export type { TextToImageInvocation } from './models/TextToImageInvocation'; +export type { TextToLatentsInvocation } from './models/TextToLatentsInvocation'; +export type { UpscaleInvocation } from './models/UpscaleInvocation'; +export type { VaeRepo } from './models/VaeRepo'; +export type { ValidationError } from './models/ValidationError'; + +export { $AddInvocation } from './schemas/$AddInvocation'; +export { $BlurInvocation } from './schemas/$BlurInvocation'; +export { $Body_upload_image } from './schemas/$Body_upload_image'; +export { $CkptModelInfo } from './schemas/$CkptModelInfo'; +export { $CollectInvocation } from './schemas/$CollectInvocation'; +export { $CollectInvocationOutput } from './schemas/$CollectInvocationOutput'; +export { $CreateModelRequest } from './schemas/$CreateModelRequest'; +export { $CropImageInvocation } from './schemas/$CropImageInvocation'; +export { $CvInpaintInvocation } from './schemas/$CvInpaintInvocation'; +export { $DiffusersModelInfo } from './schemas/$DiffusersModelInfo'; +export { $DivideInvocation } from './schemas/$DivideInvocation'; +export { $Edge } from './schemas/$Edge'; +export { $EdgeConnection } from './schemas/$EdgeConnection'; +export { $Graph } from './schemas/$Graph'; +export { $GraphExecutionState } from './schemas/$GraphExecutionState'; +export { $GraphInvocation } from './schemas/$GraphInvocation'; +export { $GraphInvocationOutput } from './schemas/$GraphInvocationOutput'; +export { $HTTPValidationError } from './schemas/$HTTPValidationError'; +export { $ImageField } from './schemas/$ImageField'; +export { $ImageOutput } from './schemas/$ImageOutput'; +export { $ImageResponse } from './schemas/$ImageResponse'; +export { $ImageResponseMetadata } from './schemas/$ImageResponseMetadata'; +export { $ImageToImageInvocation } from './schemas/$ImageToImageInvocation'; +export { $ImageType } from './schemas/$ImageType'; +export { $InpaintInvocation } from './schemas/$InpaintInvocation'; +export { $IntCollectionOutput } from './schemas/$IntCollectionOutput'; +export { $IntOutput } from './schemas/$IntOutput'; +export { $InverseLerpInvocation } from './schemas/$InverseLerpInvocation'; +export { $InvokeAIMetadata } from './schemas/$InvokeAIMetadata'; +export { $IterateInvocation } from './schemas/$IterateInvocation'; +export { $IterateInvocationOutput } from './schemas/$IterateInvocationOutput'; +export { $LatentsField } from './schemas/$LatentsField'; +export { $LatentsOutput } from './schemas/$LatentsOutput'; +export { $LatentsToImageInvocation } from './schemas/$LatentsToImageInvocation'; +export { $LatentsToLatentsInvocation } from './schemas/$LatentsToLatentsInvocation'; +export { $LerpInvocation } from './schemas/$LerpInvocation'; +export { $LoadImageInvocation } from './schemas/$LoadImageInvocation'; +export { $MaskFromAlphaInvocation } from './schemas/$MaskFromAlphaInvocation'; +export { $MaskOutput } from './schemas/$MaskOutput'; +export { $MetadataImageField } from './schemas/$MetadataImageField'; +export { $MetadataLatentsField } from './schemas/$MetadataLatentsField'; +export { $ModelsList } from './schemas/$ModelsList'; +export { $MultiplyInvocation } from './schemas/$MultiplyInvocation'; +export { $NoiseInvocation } from './schemas/$NoiseInvocation'; +export { $NoiseOutput } from './schemas/$NoiseOutput'; +export { $PaginatedResults_GraphExecutionState_ } from './schemas/$PaginatedResults_GraphExecutionState_'; +export { $PaginatedResults_ImageResponse_ } from './schemas/$PaginatedResults_ImageResponse_'; +export { $ParamIntInvocation } from './schemas/$ParamIntInvocation'; +export { $PasteImageInvocation } from './schemas/$PasteImageInvocation'; +export { $PromptOutput } from './schemas/$PromptOutput'; +export { $RandomRangeInvocation } from './schemas/$RandomRangeInvocation'; +export { $RangeInvocation } from './schemas/$RangeInvocation'; +export { $RestoreFaceInvocation } from './schemas/$RestoreFaceInvocation'; +export { $ShowImageInvocation } from './schemas/$ShowImageInvocation'; +export { $SubtractInvocation } from './schemas/$SubtractInvocation'; +export { $TextToImageInvocation } from './schemas/$TextToImageInvocation'; +export { $TextToLatentsInvocation } from './schemas/$TextToLatentsInvocation'; +export { $UpscaleInvocation } from './schemas/$UpscaleInvocation'; +export { $VaeRepo } from './schemas/$VaeRepo'; +export { $ValidationError } from './schemas/$ValidationError'; + +export { ImagesService } from './services/ImagesService'; +export { ModelsService } from './services/ModelsService'; +export { SessionsService } from './services/SessionsService'; diff --git a/invokeai/frontend/web/src/services/api/models/AddInvocation.ts b/invokeai/frontend/web/src/services/api/models/AddInvocation.ts new file mode 100644 index 0000000000..1ff7b010c2 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/AddInvocation.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Adds two numbers + */ +export type AddInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'add'; + /** + * The first number + */ + 'a'?: number; + /** + * The second number + */ + 'b'?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/BlurInvocation.ts b/invokeai/frontend/web/src/services/api/models/BlurInvocation.ts new file mode 100644 index 0000000000..0643e4b309 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/BlurInvocation.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Blurs an image + */ +export type BlurInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'blur'; + /** + * The image to blur + */ + image?: ImageField; + /** + * The blur radius + */ + radius?: number; + /** + * The type of blur + */ + blur_type?: 'gaussian' | 'box'; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/Body_upload_image.ts b/invokeai/frontend/web/src/services/api/models/Body_upload_image.ts new file mode 100644 index 0000000000..b81146d3ab --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/Body_upload_image.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type Body_upload_image = { + file: Blob; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/CkptModelInfo.ts b/invokeai/frontend/web/src/services/api/models/CkptModelInfo.ts new file mode 100644 index 0000000000..2ae7c09674 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/CkptModelInfo.ts @@ -0,0 +1,32 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type CkptModelInfo = { + /** + * A description of the model + */ + description?: string; + format?: 'ckpt'; + /** + * The path to the model config + */ + config: string; + /** + * The path to the model weights + */ + weights: string; + /** + * The path to the model VAE + */ + vae: string; + /** + * The width of the model + */ + width?: number; + /** + * The height of the model + */ + height?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/CollectInvocation.ts b/invokeai/frontend/web/src/services/api/models/CollectInvocation.ts new file mode 100644 index 0000000000..d250ae4450 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/CollectInvocation.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Collects values into a collection + */ +export type CollectInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'collect'; + /** + * The item to collect (all inputs must be of the same type) + */ + item?: any; + /** + * The collection, will be provided on execution + */ + collection?: Array; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/CollectInvocationOutput.ts b/invokeai/frontend/web/src/services/api/models/CollectInvocationOutput.ts new file mode 100644 index 0000000000..a5976242ea --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/CollectInvocationOutput.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Base class for all invocation outputs + */ +export type CollectInvocationOutput = { + type: 'collect_output'; + /** + * The collection of input items + */ + collection: Array; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/CreateModelRequest.ts b/invokeai/frontend/web/src/services/api/models/CreateModelRequest.ts new file mode 100644 index 0000000000..0b0f52b8fe --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/CreateModelRequest.ts @@ -0,0 +1,18 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { CkptModelInfo } from './CkptModelInfo'; +import type { DiffusersModelInfo } from './DiffusersModelInfo'; + +export type CreateModelRequest = { + /** + * The name of the model + */ + name: string; + /** + * The model info + */ + info: (CkptModelInfo | DiffusersModelInfo); +}; + diff --git a/invokeai/frontend/web/src/services/api/models/CropImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/CropImageInvocation.ts new file mode 100644 index 0000000000..2676f5cb87 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/CropImageInvocation.ts @@ -0,0 +1,37 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Crops an image to a specified box. The box can be outside of the image. + */ +export type CropImageInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'crop'; + /** + * The image to crop + */ + image?: ImageField; + /** + * The left x coordinate of the crop rectangle + */ + 'x'?: number; + /** + * The top y coordinate of the crop rectangle + */ + 'y'?: number; + /** + * The width of the crop rectangle + */ + width?: number; + /** + * The height of the crop rectangle + */ + height?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/CvInpaintInvocation.ts b/invokeai/frontend/web/src/services/api/models/CvInpaintInvocation.ts new file mode 100644 index 0000000000..19342acf8f --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/CvInpaintInvocation.ts @@ -0,0 +1,25 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Simple inpaint using opencv. + */ +export type CvInpaintInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'cv_inpaint'; + /** + * The image to inpaint + */ + image?: ImageField; + /** + * The mask to use when inpainting + */ + mask?: ImageField; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/DiffusersModelInfo.ts b/invokeai/frontend/web/src/services/api/models/DiffusersModelInfo.ts new file mode 100644 index 0000000000..5be4801cdd --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/DiffusersModelInfo.ts @@ -0,0 +1,26 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { VaeRepo } from './VaeRepo'; + +export type DiffusersModelInfo = { + /** + * A description of the model + */ + description?: string; + format?: 'diffusers'; + /** + * The VAE repo to use for this model + */ + vae?: VaeRepo; + /** + * The repo ID to use for this model + */ + repo_id?: string; + /** + * The path to the model + */ + path?: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/DivideInvocation.ts b/invokeai/frontend/web/src/services/api/models/DivideInvocation.ts new file mode 100644 index 0000000000..3cb262e9af --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/DivideInvocation.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Divides two numbers + */ +export type DivideInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'div'; + /** + * The first number + */ + 'a'?: number; + /** + * The second number + */ + 'b'?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/Edge.ts b/invokeai/frontend/web/src/services/api/models/Edge.ts new file mode 100644 index 0000000000..bba275cb26 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/Edge.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { EdgeConnection } from './EdgeConnection'; + +export type Edge = { + /** + * The connection for the edge's from node and field + */ + source: EdgeConnection; + /** + * The connection for the edge's to node and field + */ + destination: EdgeConnection; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/EdgeConnection.ts b/invokeai/frontend/web/src/services/api/models/EdgeConnection.ts new file mode 100644 index 0000000000..ecbddccd76 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/EdgeConnection.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type EdgeConnection = { + /** + * The id of the node for this edge connection + */ + node_id: string; + /** + * The field for this connection + */ + field: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/Graph.ts b/invokeai/frontend/web/src/services/api/models/Graph.ts new file mode 100644 index 0000000000..1e590e4ba9 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/Graph.ts @@ -0,0 +1,49 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { AddInvocation } from './AddInvocation'; +import type { BlurInvocation } from './BlurInvocation'; +import type { CollectInvocation } from './CollectInvocation'; +import type { CropImageInvocation } from './CropImageInvocation'; +import type { CvInpaintInvocation } from './CvInpaintInvocation'; +import type { DivideInvocation } from './DivideInvocation'; +import type { Edge } from './Edge'; +import type { GraphInvocation } from './GraphInvocation'; +import type { ImageToImageInvocation } from './ImageToImageInvocation'; +import type { InpaintInvocation } from './InpaintInvocation'; +import type { InverseLerpInvocation } from './InverseLerpInvocation'; +import type { IterateInvocation } from './IterateInvocation'; +import type { LatentsToImageInvocation } from './LatentsToImageInvocation'; +import type { LatentsToLatentsInvocation } from './LatentsToLatentsInvocation'; +import type { LerpInvocation } from './LerpInvocation'; +import type { LoadImageInvocation } from './LoadImageInvocation'; +import type { MaskFromAlphaInvocation } from './MaskFromAlphaInvocation'; +import type { MultiplyInvocation } from './MultiplyInvocation'; +import type { NoiseInvocation } from './NoiseInvocation'; +import type { ParamIntInvocation } from './ParamIntInvocation'; +import type { PasteImageInvocation } from './PasteImageInvocation'; +import type { RandomRangeInvocation } from './RandomRangeInvocation'; +import type { RangeInvocation } from './RangeInvocation'; +import type { RestoreFaceInvocation } from './RestoreFaceInvocation'; +import type { ShowImageInvocation } from './ShowImageInvocation'; +import type { SubtractInvocation } from './SubtractInvocation'; +import type { TextToImageInvocation } from './TextToImageInvocation'; +import type { TextToLatentsInvocation } from './TextToLatentsInvocation'; +import type { UpscaleInvocation } from './UpscaleInvocation'; + +export type Graph = { + /** + * The id of this graph + */ + id?: string; + /** + * The nodes in this graph + */ + nodes?: Record; + /** + * The connections between nodes and their fields in this graph + */ + edges?: Array; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/GraphExecutionState.ts b/invokeai/frontend/web/src/services/api/models/GraphExecutionState.ts new file mode 100644 index 0000000000..2243542480 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/GraphExecutionState.ts @@ -0,0 +1,58 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { CollectInvocationOutput } from './CollectInvocationOutput'; +import type { Graph } from './Graph'; +import type { GraphInvocationOutput } from './GraphInvocationOutput'; +import type { ImageOutput } from './ImageOutput'; +import type { IntCollectionOutput } from './IntCollectionOutput'; +import type { IntOutput } from './IntOutput'; +import type { IterateInvocationOutput } from './IterateInvocationOutput'; +import type { LatentsOutput } from './LatentsOutput'; +import type { MaskOutput } from './MaskOutput'; +import type { NoiseOutput } from './NoiseOutput'; +import type { PromptOutput } from './PromptOutput'; + +/** + * Tracks the state of a graph execution + */ +export type GraphExecutionState = { + /** + * The id of the execution state + */ + id: string; + /** + * The graph being executed + */ + graph: Graph; + /** + * The expanded graph of activated and executed nodes + */ + execution_graph: Graph; + /** + * The set of node ids that have been executed + */ + executed: Array; + /** + * The list of node ids that have been executed, in order of execution + */ + executed_history: Array; + /** + * The results of node executions + */ + results: Record; + /** + * Errors raised when executing nodes + */ + errors: Record; + /** + * The map of prepared nodes to original graph nodes + */ + prepared_source_mapping: Record; + /** + * The map of original graph nodes to prepared nodes + */ + source_prepared_mapping: Record>; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/GraphInvocation.ts b/invokeai/frontend/web/src/services/api/models/GraphInvocation.ts new file mode 100644 index 0000000000..5109a49a68 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/GraphInvocation.ts @@ -0,0 +1,22 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { Graph } from './Graph'; + +/** + * A node to process inputs and produce outputs. + * May use dependency injection in __init__ to receive providers. + */ +export type GraphInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'graph'; + /** + * The graph to run + */ + graph?: Graph; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/GraphInvocationOutput.ts b/invokeai/frontend/web/src/services/api/models/GraphInvocationOutput.ts new file mode 100644 index 0000000000..af0aae3edb --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/GraphInvocationOutput.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Base class for all invocation outputs + */ +export type GraphInvocationOutput = { + type: 'graph_output'; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/HTTPValidationError.ts b/invokeai/frontend/web/src/services/api/models/HTTPValidationError.ts new file mode 100644 index 0000000000..5e13adc4e5 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/HTTPValidationError.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ValidationError } from './ValidationError'; + +export type HTTPValidationError = { + detail?: Array; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ImageField.ts b/invokeai/frontend/web/src/services/api/models/ImageField.ts new file mode 100644 index 0000000000..fa22ae8007 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageField.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageType } from './ImageType'; + +/** + * An image field used for passing image objects between invocations + */ +export type ImageField = { + /** + * The type of the image + */ + image_type: ImageType; + /** + * The name of the image + */ + image_name: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ImageOutput.ts b/invokeai/frontend/web/src/services/api/models/ImageOutput.ts new file mode 100644 index 0000000000..09b842de26 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageOutput.ts @@ -0,0 +1,25 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Base class for invocations that output an image + */ +export type ImageOutput = { + type: 'image'; + /** + * The output image + */ + image: ImageField; + /** + * The width of the image in pixels + */ + width: number; + /** + * The height of the image in pixels + */ + height: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ImageResponse.ts b/invokeai/frontend/web/src/services/api/models/ImageResponse.ts new file mode 100644 index 0000000000..688f29bfef --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageResponse.ts @@ -0,0 +1,33 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageResponseMetadata } from './ImageResponseMetadata'; +import type { ImageType } from './ImageType'; + +/** + * The response type for images + */ +export type ImageResponse = { + /** + * The type of the image + */ + image_type: ImageType; + /** + * The name of the image + */ + image_name: string; + /** + * The url of the image + */ + image_url: string; + /** + * The url of the image's thumbnail + */ + thumbnail_url: string; + /** + * The image's metadata + */ + metadata: ImageResponseMetadata; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ImageResponseMetadata.ts b/invokeai/frontend/web/src/services/api/models/ImageResponseMetadata.ts new file mode 100644 index 0000000000..50acf364df --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageResponseMetadata.ts @@ -0,0 +1,28 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { InvokeAIMetadata } from './InvokeAIMetadata'; + +/** + * An image's metadata. Used only in HTTP responses. + */ +export type ImageResponseMetadata = { + /** + * The creation timestamp of the image + */ + created: number; + /** + * The width of the image in pixels + */ + width: number; + /** + * The height of the image in pixels + */ + height: number; + /** + * The image's InvokeAI-specific metadata + */ + invokeai?: InvokeAIMetadata; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ImageToImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/ImageToImageInvocation.ts new file mode 100644 index 0000000000..d65ceeee3a --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageToImageInvocation.ts @@ -0,0 +1,69 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Generates an image using img2img. + */ +export type ImageToImageInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'img2img'; + /** + * The prompt to generate an image from + */ + prompt?: string; + /** + * The seed to use (-1 for a random seed) + */ + seed?: number; + /** + * The number of steps to use to generate the image + */ + steps?: number; + /** + * The width of the resulting image + */ + width?: number; + /** + * The height of the resulting image + */ + height?: number; + /** + * The Classifier-Free Guidance, higher values may result in a result closer to the prompt + */ + cfg_scale?: number; + /** + * 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'; + /** + * Whether or not to generate an image that can tile without seams + */ + seamless?: boolean; + /** + * The model to use (currently ignored) + */ + model?: string; + /** + * Whether or not to produce progress images during generation + */ + progress_images?: boolean; + /** + * The input image + */ + image?: ImageField; + /** + * The strength of the original image + */ + strength?: number; + /** + * Whether or not the result should be fit to the aspect ratio of the input image + */ + fit?: boolean; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ImageType.ts b/invokeai/frontend/web/src/services/api/models/ImageType.ts new file mode 100644 index 0000000000..b6468a1ed0 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageType.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * An enumeration. + */ +export type ImageType = 'results' | 'intermediates' | 'uploads'; diff --git a/invokeai/frontend/web/src/services/api/models/InpaintInvocation.ts b/invokeai/frontend/web/src/services/api/models/InpaintInvocation.ts new file mode 100644 index 0000000000..7ea6a89f62 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/InpaintInvocation.ts @@ -0,0 +1,77 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Generates an image using inpaint. + */ +export type InpaintInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'inpaint'; + /** + * The prompt to generate an image from + */ + prompt?: string; + /** + * The seed to use (-1 for a random seed) + */ + seed?: number; + /** + * The number of steps to use to generate the image + */ + steps?: number; + /** + * The width of the resulting image + */ + width?: number; + /** + * The height of the resulting image + */ + height?: number; + /** + * The Classifier-Free Guidance, higher values may result in a result closer to the prompt + */ + cfg_scale?: number; + /** + * 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'; + /** + * Whether or not to generate an image that can tile without seams + */ + seamless?: boolean; + /** + * The model to use (currently ignored) + */ + model?: string; + /** + * Whether or not to produce progress images during generation + */ + progress_images?: boolean; + /** + * The input image + */ + image?: ImageField; + /** + * The strength of the original image + */ + strength?: number; + /** + * Whether or not the result should be fit to the aspect ratio of the input image + */ + fit?: boolean; + /** + * The mask + */ + mask?: ImageField; + /** + * The amount by which to replace masked areas with latent noise + */ + inpaint_replace?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/IntCollectionOutput.ts b/invokeai/frontend/web/src/services/api/models/IntCollectionOutput.ts new file mode 100644 index 0000000000..93a115f980 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/IntCollectionOutput.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * A collection of integers + */ +export type IntCollectionOutput = { + type?: 'int_collection'; + /** + * The int collection + */ + collection?: Array; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/IntOutput.ts b/invokeai/frontend/web/src/services/api/models/IntOutput.ts new file mode 100644 index 0000000000..eeea6c68b4 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/IntOutput.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * An integer output + */ +export type IntOutput = { + type?: 'int_output'; + /** + * The output integer + */ + 'a'?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/InverseLerpInvocation.ts b/invokeai/frontend/web/src/services/api/models/InverseLerpInvocation.ts new file mode 100644 index 0000000000..33c59b7bac --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/InverseLerpInvocation.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Inverse linear interpolation of all pixels of an image + */ +export type InverseLerpInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'ilerp'; + /** + * The image to lerp + */ + image?: ImageField; + /** + * The minimum input value + */ + min?: number; + /** + * The maximum input value + */ + max?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/InvokeAIMetadata.ts b/invokeai/frontend/web/src/services/api/models/InvokeAIMetadata.ts new file mode 100644 index 0000000000..a6bc3f7744 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/InvokeAIMetadata.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { MetadataImageField } from './MetadataImageField'; +import type { MetadataLatentsField } from './MetadataLatentsField'; + +export type InvokeAIMetadata = { + session_id?: string; + node?: Record; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/IterateInvocation.ts b/invokeai/frontend/web/src/services/api/models/IterateInvocation.ts new file mode 100644 index 0000000000..0ff7a1258d --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/IterateInvocation.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * A node to process inputs and produce outputs. + * May use dependency injection in __init__ to receive providers. + */ +export type IterateInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'iterate'; + /** + * The list of items to iterate over + */ + collection?: Array; + /** + * The index, will be provided on executed iterators + */ + index?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/IterateInvocationOutput.ts b/invokeai/frontend/web/src/services/api/models/IterateInvocationOutput.ts new file mode 100644 index 0000000000..ce8d9f8c4b --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/IterateInvocationOutput.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Used to connect iteration outputs. Will be expanded to a specific output. + */ +export type IterateInvocationOutput = { + type: 'iterate_output'; + /** + * The item being iterated over + */ + item: any; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/LatentsField.ts b/invokeai/frontend/web/src/services/api/models/LatentsField.ts new file mode 100644 index 0000000000..bc6a525f7c --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/LatentsField.ts @@ -0,0 +1,14 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * A latents field used for passing latents between invocations + */ +export type LatentsField = { + /** + * The name of the latents + */ + latents_name: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/LatentsOutput.ts b/invokeai/frontend/web/src/services/api/models/LatentsOutput.ts new file mode 100644 index 0000000000..0d417c3db2 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/LatentsOutput.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { LatentsField } from './LatentsField'; + +/** + * Base class for invocations that output latents + */ +export type LatentsOutput = { + type?: 'latent_output'; + /** + * The output latents + */ + latents?: LatentsField; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/LatentsToImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/LatentsToImageInvocation.ts new file mode 100644 index 0000000000..8acd872e28 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/LatentsToImageInvocation.ts @@ -0,0 +1,25 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { LatentsField } from './LatentsField'; + +/** + * Generates an image from latents. + */ +export type LatentsToImageInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'l2i'; + /** + * The latents to generate an image from + */ + latents?: LatentsField; + /** + * The model to use + */ + model?: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts new file mode 100644 index 0000000000..8210f01bb6 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts @@ -0,0 +1,73 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { LatentsField } from './LatentsField'; + +/** + * Generates latents using latents as base image. + */ +export type LatentsToLatentsInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'l2l'; + /** + * The prompt to generate an image from + */ + prompt?: string; + /** + * The seed to use (-1 for a random seed) + */ + seed?: number; + /** + * The noise to use + */ + noise?: LatentsField; + /** + * The number of steps to use to generate the image + */ + steps?: number; + /** + * The width of the resulting image + */ + width?: number; + /** + * The height of the resulting image + */ + height?: number; + /** + * The Classifier-Free Guidance, higher values may result in a result closer to the prompt + */ + cfg_scale?: number; + /** + * 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'; + /** + * 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 model to use (currently ignored) + */ + model?: string; + /** + * Whether or not to produce progress images during generation + */ + progress_images?: boolean; + /** + * The latents to use as a base image + */ + latents?: LatentsField; + /** + * The strength of the latents to use + */ + strength?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/LerpInvocation.ts b/invokeai/frontend/web/src/services/api/models/LerpInvocation.ts new file mode 100644 index 0000000000..f2406c2246 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/LerpInvocation.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Linear interpolation of all pixels of an image + */ +export type LerpInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'lerp'; + /** + * The image to lerp + */ + image?: ImageField; + /** + * The minimum output value + */ + min?: number; + /** + * The maximum output value + */ + max?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/LoadImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/LoadImageInvocation.ts new file mode 100644 index 0000000000..745a9b44e4 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/LoadImageInvocation.ts @@ -0,0 +1,25 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageType } from './ImageType'; + +/** + * Load an image and provide it as output. + */ +export type LoadImageInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'load_image'; + /** + * The type of the image + */ + image_type: ImageType; + /** + * The name of the image + */ + image_name: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/MaskFromAlphaInvocation.ts b/invokeai/frontend/web/src/services/api/models/MaskFromAlphaInvocation.ts new file mode 100644 index 0000000000..e71b1f464b --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/MaskFromAlphaInvocation.ts @@ -0,0 +1,25 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Extracts the alpha channel of an image as a mask. + */ +export type MaskFromAlphaInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'tomask'; + /** + * The image to create the mask from + */ + image?: ImageField; + /** + * Whether or not to invert the mask + */ + invert?: boolean; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/MaskOutput.ts b/invokeai/frontend/web/src/services/api/models/MaskOutput.ts new file mode 100644 index 0000000000..645fb8d1cb --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/MaskOutput.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Base class for invocations that output a mask + */ +export type MaskOutput = { + type: 'mask'; + /** + * The output mask + */ + mask: ImageField; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/MetadataImageField.ts b/invokeai/frontend/web/src/services/api/models/MetadataImageField.ts new file mode 100644 index 0000000000..0dcae1ccee --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/MetadataImageField.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageType } from './ImageType'; + +export type MetadataImageField = { + image_type: ImageType; + image_name: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/MetadataLatentsField.ts b/invokeai/frontend/web/src/services/api/models/MetadataLatentsField.ts new file mode 100644 index 0000000000..30b6aebeba --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/MetadataLatentsField.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type MetadataLatentsField = { + latents_name: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ModelsList.ts b/invokeai/frontend/web/src/services/api/models/ModelsList.ts new file mode 100644 index 0000000000..7a7449542d --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ModelsList.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { CkptModelInfo } from './CkptModelInfo'; +import type { DiffusersModelInfo } from './DiffusersModelInfo'; + +export type ModelsList = { + models: Record; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/MultiplyInvocation.ts b/invokeai/frontend/web/src/services/api/models/MultiplyInvocation.ts new file mode 100644 index 0000000000..eede8f18d7 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/MultiplyInvocation.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Multiplies two numbers + */ +export type MultiplyInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'mul'; + /** + * The first number + */ + 'a'?: number; + /** + * The second number + */ + 'b'?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/NoiseInvocation.ts b/invokeai/frontend/web/src/services/api/models/NoiseInvocation.ts new file mode 100644 index 0000000000..59e50b76f3 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/NoiseInvocation.ts @@ -0,0 +1,27 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Generates latent noise. + */ +export type NoiseInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'noise'; + /** + * The seed to use + */ + seed?: number; + /** + * The width of the resulting noise + */ + width?: number; + /** + * The height of the resulting noise + */ + height?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/NoiseOutput.ts b/invokeai/frontend/web/src/services/api/models/NoiseOutput.ts new file mode 100644 index 0000000000..ff87cb7277 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/NoiseOutput.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { LatentsField } from './LatentsField'; + +/** + * Invocation noise output + */ +export type NoiseOutput = { + type?: 'noise_output'; + /** + * The output noise + */ + noise?: LatentsField; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/PaginatedResults_GraphExecutionState_.ts b/invokeai/frontend/web/src/services/api/models/PaginatedResults_GraphExecutionState_.ts new file mode 100644 index 0000000000..dd9f50cd4a --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/PaginatedResults_GraphExecutionState_.ts @@ -0,0 +1,32 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { GraphExecutionState } from './GraphExecutionState'; + +/** + * Paginated results + */ +export type PaginatedResults_GraphExecutionState_ = { + /** + * Items + */ + items: Array; + /** + * Current Page + */ + page: number; + /** + * Total number of pages + */ + pages: number; + /** + * Number of items per page + */ + per_page: number; + /** + * Total number of items in result + */ + total: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/PaginatedResults_ImageResponse_.ts b/invokeai/frontend/web/src/services/api/models/PaginatedResults_ImageResponse_.ts new file mode 100644 index 0000000000..214c7c2f57 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/PaginatedResults_ImageResponse_.ts @@ -0,0 +1,32 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageResponse } from './ImageResponse'; + +/** + * Paginated results + */ +export type PaginatedResults_ImageResponse_ = { + /** + * Items + */ + items: Array; + /** + * Current Page + */ + page: number; + /** + * Total number of pages + */ + pages: number; + /** + * Number of items per page + */ + per_page: number; + /** + * Total number of items in result + */ + total: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ParamIntInvocation.ts b/invokeai/frontend/web/src/services/api/models/ParamIntInvocation.ts new file mode 100644 index 0000000000..7047310a87 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ParamIntInvocation.ts @@ -0,0 +1,19 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * An integer parameter + */ +export type ParamIntInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'param_int'; + /** + * The integer value + */ + 'a'?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/PasteImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/PasteImageInvocation.ts new file mode 100644 index 0000000000..8a181ccf07 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/PasteImageInvocation.ts @@ -0,0 +1,37 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Pastes an image into another image. + */ +export type PasteImageInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'paste'; + /** + * The base image + */ + base_image?: ImageField; + /** + * The image to paste + */ + image?: ImageField; + /** + * The mask to use when pasting + */ + mask?: ImageField; + /** + * The left x coordinate at which to paste the image + */ + 'x'?: number; + /** + * The top y coordinate at which to paste the image + */ + 'y'?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/PromptOutput.ts b/invokeai/frontend/web/src/services/api/models/PromptOutput.ts new file mode 100644 index 0000000000..5bca3f3037 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/PromptOutput.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Base class for invocations that output a prompt + */ +export type PromptOutput = { + type: 'prompt'; + /** + * The output prompt + */ + prompt: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/RandomRangeInvocation.ts b/invokeai/frontend/web/src/services/api/models/RandomRangeInvocation.ts new file mode 100644 index 0000000000..55a94ec46d --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/RandomRangeInvocation.ts @@ -0,0 +1,31 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Creates a collection of random numbers + */ +export type RandomRangeInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'random_range'; + /** + * The inclusive low value + */ + low?: number; + /** + * The exclusive high value + */ + high?: number; + /** + * The number of values to generate + */ + size?: number; + /** + * The seed for the RNG + */ + seed?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/RangeInvocation.ts b/invokeai/frontend/web/src/services/api/models/RangeInvocation.ts new file mode 100644 index 0000000000..72bc4806da --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/RangeInvocation.ts @@ -0,0 +1,27 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Creates a range + */ +export type RangeInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'range'; + /** + * The start of the range + */ + start?: number; + /** + * The stop of the range + */ + stop?: number; + /** + * The step of the range + */ + step?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/RestoreFaceInvocation.ts b/invokeai/frontend/web/src/services/api/models/RestoreFaceInvocation.ts new file mode 100644 index 0000000000..e03ed01c81 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/RestoreFaceInvocation.ts @@ -0,0 +1,25 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Restores faces in an image. + */ +export type RestoreFaceInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'restore_face'; + /** + * The input image + */ + image?: ImageField; + /** + * The strength of the restoration + */ + strength?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ShowImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/ShowImageInvocation.ts new file mode 100644 index 0000000000..145895ad75 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ShowImageInvocation.ts @@ -0,0 +1,21 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Displays a provided image, and passes it forward in the pipeline. + */ +export type ShowImageInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'show_image'; + /** + * The image to show + */ + image?: ImageField; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/SubtractInvocation.ts b/invokeai/frontend/web/src/services/api/models/SubtractInvocation.ts new file mode 100644 index 0000000000..6f2da116a2 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/SubtractInvocation.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Subtracts two numbers + */ +export type SubtractInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'sub'; + /** + * The first number + */ + 'a'?: number; + /** + * The second number + */ + 'b'?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/TextToImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/TextToImageInvocation.ts new file mode 100644 index 0000000000..b1ff7a3525 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/TextToImageInvocation.ts @@ -0,0 +1,55 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Generates an image using text2img. + */ +export type TextToImageInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'txt2img'; + /** + * The prompt to generate an image from + */ + prompt?: string; + /** + * The seed to use (-1 for a random seed) + */ + seed?: number; + /** + * The number of steps to use to generate the image + */ + steps?: number; + /** + * The width of the resulting image + */ + width?: number; + /** + * The height of the resulting image + */ + height?: number; + /** + * The Classifier-Free Guidance, higher values may result in a result closer to the prompt + */ + cfg_scale?: number; + /** + * 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'; + /** + * Whether or not to generate an image that can tile without seams + */ + seamless?: boolean; + /** + * The model to use (currently ignored) + */ + model?: string; + /** + * Whether or not to produce progress images during generation + */ + progress_images?: boolean; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts new file mode 100644 index 0000000000..63754db163 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts @@ -0,0 +1,65 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { LatentsField } from './LatentsField'; + +/** + * Generates latents from a prompt. + */ +export type TextToLatentsInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 't2l'; + /** + * The prompt to generate an image from + */ + prompt?: string; + /** + * The seed to use (-1 for a random seed) + */ + seed?: number; + /** + * The noise to use + */ + noise?: LatentsField; + /** + * The number of steps to use to generate the image + */ + steps?: number; + /** + * The width of the resulting image + */ + width?: number; + /** + * The height of the resulting image + */ + height?: number; + /** + * The Classifier-Free Guidance, higher values may result in a result closer to the prompt + */ + cfg_scale?: number; + /** + * 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'; + /** + * 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 model to use (currently ignored) + */ + model?: string; + /** + * Whether or not to produce progress images during generation + */ + progress_images?: boolean; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/UpscaleInvocation.ts b/invokeai/frontend/web/src/services/api/models/UpscaleInvocation.ts new file mode 100644 index 0000000000..8416c2454d --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/UpscaleInvocation.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Upscales an image. + */ +export type UpscaleInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'upscale'; + /** + * The input image + */ + image?: ImageField; + /** + * The strength + */ + strength?: number; + /** + * The upscale level + */ + level?: 2 | 4; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/VaeRepo.ts b/invokeai/frontend/web/src/services/api/models/VaeRepo.ts new file mode 100644 index 0000000000..0e233626c6 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/VaeRepo.ts @@ -0,0 +1,19 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type VaeRepo = { + /** + * The repo ID to use for this VAE + */ + repo_id: string; + /** + * The path to the VAE + */ + path?: string; + /** + * The subfolder to use for this VAE + */ + subfolder?: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ValidationError.ts b/invokeai/frontend/web/src/services/api/models/ValidationError.ts new file mode 100644 index 0000000000..14e1fdecd0 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ValidationError.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type ValidationError = { + loc: Array<(string | number)>; + msg: string; + type: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/schemas/$AddInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$AddInvocation.ts new file mode 100644 index 0000000000..3aa74aef3e --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$AddInvocation.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $AddInvocation = { + description: `Adds two numbers`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + 'a': { + type: 'number', + description: `The first number`, + }, + 'b': { + type: 'number', + description: `The second number`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$BlurInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$BlurInvocation.ts new file mode 100644 index 0000000000..69f5438583 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$BlurInvocation.ts @@ -0,0 +1,30 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $BlurInvocation = { + description: `Blurs an image`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The image to blur`, + contains: [{ + type: 'ImageField', + }], + }, + radius: { + type: 'number', + description: `The blur radius`, + }, + blur_type: { + type: 'Enum', + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$Body_upload_image.ts b/invokeai/frontend/web/src/services/api/schemas/$Body_upload_image.ts new file mode 100644 index 0000000000..7d6adf5a84 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$Body_upload_image.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $Body_upload_image = { + properties: { + file: { + type: 'binary', + isRequired: true, + format: 'binary', + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CkptModelInfo.ts b/invokeai/frontend/web/src/services/api/schemas/$CkptModelInfo.ts new file mode 100644 index 0000000000..aeac9a4200 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$CkptModelInfo.ts @@ -0,0 +1,37 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $CkptModelInfo = { + properties: { + description: { + type: 'string', + description: `A description of the model`, + }, + format: { + type: 'Enum', + }, + config: { + type: 'string', + description: `The path to the model config`, + isRequired: true, + }, + weights: { + type: 'string', + description: `The path to the model weights`, + isRequired: true, + }, + vae: { + type: 'string', + description: `The path to the model VAE`, + isRequired: true, + }, + width: { + type: 'number', + description: `The width of the model`, + }, + height: { + type: 'number', + description: `The height of the model`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CollectInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$CollectInvocation.ts new file mode 100644 index 0000000000..1ab0bb0b9b --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$CollectInvocation.ts @@ -0,0 +1,28 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $CollectInvocation = { + description: `Collects values into a collection`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + item: { + description: `The item to collect (all inputs must be of the same type)`, + properties: { + }, + }, + collection: { + type: 'array', + contains: { + properties: { + }, + }, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CollectInvocationOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$CollectInvocationOutput.ts new file mode 100644 index 0000000000..598ad94eff --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$CollectInvocationOutput.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $CollectInvocationOutput = { + description: `Base class for all invocation outputs`, + properties: { + type: { + type: 'Enum', + isRequired: true, + }, + collection: { + type: 'array', + contains: { + properties: { + }, + }, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CreateModelRequest.ts b/invokeai/frontend/web/src/services/api/schemas/$CreateModelRequest.ts new file mode 100644 index 0000000000..32593059d8 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$CreateModelRequest.ts @@ -0,0 +1,22 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $CreateModelRequest = { + properties: { + name: { + type: 'string', + description: `The name of the model`, + isRequired: true, + }, + info: { + type: 'one-of', + description: `The model info`, + contains: [{ + type: 'CkptModelInfo', + }, { + type: 'DiffusersModelInfo', + }], + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CropImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$CropImageInvocation.ts new file mode 100644 index 0000000000..f279efe286 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$CropImageInvocation.ts @@ -0,0 +1,39 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $CropImageInvocation = { + description: `Crops an image to a specified box. The box can be outside of the image.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The image to crop`, + contains: [{ + type: 'ImageField', + }], + }, + 'x': { + type: 'number', + description: `The left x coordinate of the crop rectangle`, + }, + 'y': { + type: 'number', + description: `The top y coordinate of the crop rectangle`, + }, + width: { + type: 'number', + description: `The width of the crop rectangle`, + }, + height: { + type: 'number', + description: `The height of the crop rectangle`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CvInpaintInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$CvInpaintInvocation.ts new file mode 100644 index 0000000000..959484f3ed --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$CvInpaintInvocation.ts @@ -0,0 +1,30 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $CvInpaintInvocation = { + description: `Simple inpaint using opencv.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The image to inpaint`, + contains: [{ + type: 'ImageField', + }], + }, + mask: { + type: 'all-of', + description: `The mask to use when inpainting`, + contains: [{ + type: 'ImageField', + }], + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$DiffusersModelInfo.ts b/invokeai/frontend/web/src/services/api/schemas/$DiffusersModelInfo.ts new file mode 100644 index 0000000000..b2e895b498 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$DiffusersModelInfo.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $DiffusersModelInfo = { + properties: { + description: { + type: 'string', + description: `A description of the model`, + }, + format: { + type: 'Enum', + }, + vae: { + type: 'all-of', + description: `The VAE repo to use for this model`, + contains: [{ + type: 'VaeRepo', + }], + }, + repo_id: { + type: 'string', + description: `The repo ID to use for this model`, + }, + path: { + type: 'string', + description: `The path to the model`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$DivideInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$DivideInvocation.ts new file mode 100644 index 0000000000..a6d5998591 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$DivideInvocation.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $DivideInvocation = { + description: `Divides two numbers`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + 'a': { + type: 'number', + description: `The first number`, + }, + 'b': { + type: 'number', + description: `The second number`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$Edge.ts b/invokeai/frontend/web/src/services/api/schemas/$Edge.ts new file mode 100644 index 0000000000..d7e7028bf1 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$Edge.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $Edge = { + properties: { + source: { + type: 'all-of', + description: `The connection for the edge's from node and field`, + contains: [{ + type: 'EdgeConnection', + }], + isRequired: true, + }, + destination: { + type: 'all-of', + description: `The connection for the edge's to node and field`, + contains: [{ + type: 'EdgeConnection', + }], + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$EdgeConnection.ts b/invokeai/frontend/web/src/services/api/schemas/$EdgeConnection.ts new file mode 100644 index 0000000000..a3f325888e --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$EdgeConnection.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $EdgeConnection = { + properties: { + node_id: { + type: 'string', + description: `The id of the node for this edge connection`, + isRequired: true, + }, + field: { + type: 'string', + description: `The field for this connection`, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$Graph.ts b/invokeai/frontend/web/src/services/api/schemas/$Graph.ts new file mode 100644 index 0000000000..b431011ba6 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$Graph.ts @@ -0,0 +1,80 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $Graph = { + properties: { + id: { + type: 'string', + description: `The id of this graph`, + }, + nodes: { + type: 'dictionary', + contains: { + type: 'one-of', + contains: [{ + type: 'LoadImageInvocation', + }, { + type: 'ShowImageInvocation', + }, { + type: 'CropImageInvocation', + }, { + type: 'PasteImageInvocation', + }, { + type: 'MaskFromAlphaInvocation', + }, { + type: 'BlurInvocation', + }, { + type: 'LerpInvocation', + }, { + type: 'InverseLerpInvocation', + }, { + type: 'NoiseInvocation', + }, { + type: 'TextToLatentsInvocation', + }, { + type: 'LatentsToImageInvocation', + }, { + type: 'AddInvocation', + }, { + type: 'SubtractInvocation', + }, { + type: 'MultiplyInvocation', + }, { + type: 'DivideInvocation', + }, { + type: 'ParamIntInvocation', + }, { + type: 'CvInpaintInvocation', + }, { + type: 'RangeInvocation', + }, { + type: 'RandomRangeInvocation', + }, { + type: 'UpscaleInvocation', + }, { + type: 'RestoreFaceInvocation', + }, { + type: 'TextToImageInvocation', + }, { + type: 'GraphInvocation', + }, { + type: 'IterateInvocation', + }, { + type: 'CollectInvocation', + }, { + type: 'LatentsToLatentsInvocation', + }, { + type: 'ImageToImageInvocation', + }, { + type: 'InpaintInvocation', + }], + }, + }, + edges: { + type: 'array', + contains: { + type: 'Edge', + }, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$GraphExecutionState.ts b/invokeai/frontend/web/src/services/api/schemas/$GraphExecutionState.ts new file mode 100644 index 0000000000..a21419a6a4 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$GraphExecutionState.ts @@ -0,0 +1,95 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $GraphExecutionState = { + description: `Tracks the state of a graph execution`, + properties: { + id: { + type: 'string', + description: `The id of the execution state`, + isRequired: true, + }, + graph: { + type: 'all-of', + description: `The graph being executed`, + contains: [{ + type: 'Graph', + }], + isRequired: true, + }, + execution_graph: { + type: 'all-of', + description: `The expanded graph of activated and executed nodes`, + contains: [{ + type: 'Graph', + }], + isRequired: true, + }, + executed: { + type: 'array', + contains: { + type: 'string', + }, + isRequired: true, + }, + executed_history: { + type: 'array', + contains: { + type: 'string', + }, + isRequired: true, + }, + results: { + type: 'dictionary', + contains: { + type: 'one-of', + contains: [{ + type: 'ImageOutput', + }, { + type: 'MaskOutput', + }, { + type: 'LatentsOutput', + }, { + type: 'NoiseOutput', + }, { + type: 'IntOutput', + }, { + type: 'PromptOutput', + }, { + type: 'IntCollectionOutput', + }, { + type: 'GraphInvocationOutput', + }, { + type: 'IterateInvocationOutput', + }, { + type: 'CollectInvocationOutput', + }], + }, + isRequired: true, + }, + errors: { + type: 'dictionary', + contains: { + type: 'string', + }, + isRequired: true, + }, + prepared_source_mapping: { + type: 'dictionary', + contains: { + type: 'string', + }, + isRequired: true, + }, + source_prepared_mapping: { + type: 'dictionary', + contains: { + type: 'array', + contains: { + type: 'string', + }, + }, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$GraphInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$GraphInvocation.ts new file mode 100644 index 0000000000..0b9e4322c8 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$GraphInvocation.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $GraphInvocation = { + description: `A node to process inputs and produce outputs. + May use dependency injection in __init__ to receive providers.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + graph: { + type: 'all-of', + description: `The graph to run`, + contains: [{ + type: 'Graph', + }], + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$GraphInvocationOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$GraphInvocationOutput.ts new file mode 100644 index 0000000000..c411e65a85 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$GraphInvocationOutput.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $GraphInvocationOutput = { + description: `Base class for all invocation outputs`, + properties: { + type: { + type: 'Enum', + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$HTTPValidationError.ts b/invokeai/frontend/web/src/services/api/schemas/$HTTPValidationError.ts new file mode 100644 index 0000000000..0d129d4b67 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$HTTPValidationError.ts @@ -0,0 +1,13 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $HTTPValidationError = { + properties: { + detail: { + type: 'array', + contains: { + type: 'ValidationError', + }, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageField.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageField.ts new file mode 100644 index 0000000000..968ac29a45 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ImageField.ts @@ -0,0 +1,21 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ImageField = { + description: `An image field used for passing image objects between invocations`, + properties: { + image_type: { + type: 'all-of', + description: `The type of the image`, + contains: [{ + type: 'ImageType', + }], + isRequired: true, + }, + image_name: { + type: 'string', + description: `The name of the image`, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageOutput.ts new file mode 100644 index 0000000000..6adbe0d8c1 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ImageOutput.ts @@ -0,0 +1,30 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ImageOutput = { + description: `Base class for invocations that output an image`, + properties: { + type: { + type: 'Enum', + isRequired: true, + }, + image: { + type: 'all-of', + description: `The output image`, + contains: [{ + type: 'ImageField', + }], + isRequired: true, + }, + width: { + type: 'number', + description: `The width of the image in pixels`, + isRequired: true, + }, + height: { + type: 'number', + description: `The height of the image in pixels`, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageResponse.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageResponse.ts new file mode 100644 index 0000000000..9a3d453536 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ImageResponse.ts @@ -0,0 +1,39 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ImageResponse = { + description: `The response type for images`, + properties: { + image_type: { + type: 'all-of', + description: `The type of the image`, + contains: [{ + type: 'ImageType', + }], + isRequired: true, + }, + image_name: { + type: 'string', + description: `The name of the image`, + isRequired: true, + }, + image_url: { + type: 'string', + description: `The url of the image`, + isRequired: true, + }, + thumbnail_url: { + type: 'string', + description: `The url of the image's thumbnail`, + isRequired: true, + }, + metadata: { + type: 'all-of', + description: `The image's metadata`, + contains: [{ + type: 'ImageResponseMetadata', + }], + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageResponseMetadata.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageResponseMetadata.ts new file mode 100644 index 0000000000..d215c8de58 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ImageResponseMetadata.ts @@ -0,0 +1,30 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ImageResponseMetadata = { + description: `An image's metadata. Used only in HTTP responses.`, + properties: { + created: { + type: 'number', + description: `The creation timestamp of the image`, + isRequired: true, + }, + width: { + type: 'number', + description: `The width of the image in pixels`, + isRequired: true, + }, + height: { + type: 'number', + description: `The height of the image in pixels`, + isRequired: true, + }, + invokeai: { + type: 'all-of', + description: `The image's InvokeAI-specific metadata`, + contains: [{ + type: 'InvokeAIMetadata', + }], + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageToImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageToImageInvocation.ts new file mode 100644 index 0000000000..4b77f03ca3 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ImageToImageInvocation.ts @@ -0,0 +1,75 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ImageToImageInvocation = { + description: `Generates an image using img2img.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + prompt: { + type: 'string', + description: `The prompt to generate an image from`, + }, + seed: { + type: 'number', + description: `The seed to use (-1 for a random seed)`, + maximum: 4294967295, + minimum: -1, + }, + steps: { + type: 'number', + description: `The number of steps to use to generate the image`, + }, + width: { + type: 'number', + description: `The width of the resulting image`, + multipleOf: 64, + }, + height: { + type: 'number', + description: `The height of the resulting image`, + multipleOf: 64, + }, + cfg_scale: { + type: 'number', + description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`, + }, + scheduler: { + type: 'Enum', + }, + seamless: { + type: 'boolean', + description: `Whether or not to generate an image that can tile without seams`, + }, + model: { + type: 'string', + description: `The model to use (currently ignored)`, + }, + progress_images: { + type: 'boolean', + description: `Whether or not to produce progress images during generation`, + }, + image: { + type: 'all-of', + description: `The input image`, + contains: [{ + type: 'ImageField', + }], + }, + strength: { + type: 'number', + description: `The strength of the original image`, + maximum: 1, + }, + fit: { + type: 'boolean', + description: `Whether or not the result should be fit to the aspect ratio of the input image`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageType.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageType.ts new file mode 100644 index 0000000000..92e1f2b218 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ImageType.ts @@ -0,0 +1,6 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ImageType = { + type: 'Enum', +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$InpaintInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$InpaintInvocation.ts new file mode 100644 index 0000000000..ab022825b3 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$InpaintInvocation.ts @@ -0,0 +1,87 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $InpaintInvocation = { + description: `Generates an image using inpaint.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + prompt: { + type: 'string', + description: `The prompt to generate an image from`, + }, + seed: { + type: 'number', + description: `The seed to use (-1 for a random seed)`, + maximum: 4294967295, + minimum: -1, + }, + steps: { + type: 'number', + description: `The number of steps to use to generate the image`, + }, + width: { + type: 'number', + description: `The width of the resulting image`, + multipleOf: 64, + }, + height: { + type: 'number', + description: `The height of the resulting image`, + multipleOf: 64, + }, + cfg_scale: { + type: 'number', + description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`, + }, + scheduler: { + type: 'Enum', + }, + seamless: { + type: 'boolean', + description: `Whether or not to generate an image that can tile without seams`, + }, + model: { + type: 'string', + description: `The model to use (currently ignored)`, + }, + progress_images: { + type: 'boolean', + description: `Whether or not to produce progress images during generation`, + }, + image: { + type: 'all-of', + description: `The input image`, + contains: [{ + type: 'ImageField', + }], + }, + strength: { + type: 'number', + description: `The strength of the original image`, + maximum: 1, + }, + fit: { + type: 'boolean', + description: `Whether or not the result should be fit to the aspect ratio of the input image`, + }, + mask: { + type: 'all-of', + description: `The mask`, + contains: [{ + type: 'ImageField', + }], + }, + inpaint_replace: { + type: 'number', + description: `The amount by which to replace masked areas with latent noise`, + maximum: 1, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$IntCollectionOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$IntCollectionOutput.ts new file mode 100644 index 0000000000..caffe0ac87 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$IntCollectionOutput.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $IntCollectionOutput = { + description: `A collection of integers`, + properties: { + type: { + type: 'Enum', + }, + collection: { + type: 'array', + contains: { + type: 'number', + }, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$IntOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$IntOutput.ts new file mode 100644 index 0000000000..dfb16c1473 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$IntOutput.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $IntOutput = { + description: `An integer output`, + properties: { + type: { + type: 'Enum', + }, + 'a': { + type: 'number', + description: `The output integer`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$InverseLerpInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$InverseLerpInvocation.ts new file mode 100644 index 0000000000..43dadca876 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$InverseLerpInvocation.ts @@ -0,0 +1,33 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $InverseLerpInvocation = { + description: `Inverse linear interpolation of all pixels of an image`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The image to lerp`, + contains: [{ + type: 'ImageField', + }], + }, + min: { + type: 'number', + description: `The minimum input value`, + maximum: 255, + }, + max: { + type: 'number', + description: `The maximum input value`, + maximum: 255, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$InvokeAIMetadata.ts b/invokeai/frontend/web/src/services/api/schemas/$InvokeAIMetadata.ts new file mode 100644 index 0000000000..2d0b8e2db1 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$InvokeAIMetadata.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $InvokeAIMetadata = { + properties: { + session_id: { + type: 'string', + }, + node: { + type: 'dictionary', + contains: { + type: 'any-of', + contains: [{ + type: 'string', + }, { + type: 'number', + }, { + type: 'number', + }, { + type: 'boolean', + }, { + type: 'MetadataImageField', + }, { + type: 'MetadataLatentsField', + }], + }, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$IterateInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$IterateInvocation.ts new file mode 100644 index 0000000000..b570b889e4 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$IterateInvocation.ts @@ -0,0 +1,28 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $IterateInvocation = { + description: `A node to process inputs and produce outputs. + May use dependency injection in __init__ to receive providers.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + collection: { + type: 'array', + contains: { + properties: { + }, + }, + }, + index: { + type: 'number', + description: `The index, will be provided on executed iterators`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$IterateInvocationOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$IterateInvocationOutput.ts new file mode 100644 index 0000000000..826e92346d --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$IterateInvocationOutput.ts @@ -0,0 +1,18 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $IterateInvocationOutput = { + description: `Used to connect iteration outputs. Will be expanded to a specific output.`, + properties: { + type: { + type: 'Enum', + isRequired: true, + }, + item: { + description: `The item being iterated over`, + properties: { + }, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$LatentsField.ts b/invokeai/frontend/web/src/services/api/schemas/$LatentsField.ts new file mode 100644 index 0000000000..6f81c42883 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$LatentsField.ts @@ -0,0 +1,13 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $LatentsField = { + description: `A latents field used for passing latents between invocations`, + properties: { + latents_name: { + type: 'string', + description: `The name of the latents`, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$LatentsOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$LatentsOutput.ts new file mode 100644 index 0000000000..2b44f5a438 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$LatentsOutput.ts @@ -0,0 +1,18 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $LatentsOutput = { + description: `Base class for invocations that output latents`, + properties: { + type: { + type: 'Enum', + }, + latents: { + type: 'all-of', + description: `The output latents`, + contains: [{ + type: 'LatentsField', + }], + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$LatentsToImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$LatentsToImageInvocation.ts new file mode 100644 index 0000000000..971fa3b675 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$LatentsToImageInvocation.ts @@ -0,0 +1,27 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $LatentsToImageInvocation = { + description: `Generates an image from latents.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + latents: { + type: 'all-of', + description: `The latents to generate an image from`, + contains: [{ + type: 'LatentsField', + }], + }, + model: { + type: 'string', + description: `The model to use`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts new file mode 100644 index 0000000000..d27fdc7c1f --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts @@ -0,0 +1,81 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $LatentsToLatentsInvocation = { + description: `Generates latents using latents as base image.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + prompt: { + type: 'string', + description: `The prompt to generate an image from`, + }, + seed: { + type: 'number', + description: `The seed to use (-1 for a random seed)`, + maximum: 4294967295, + minimum: -1, + }, + noise: { + type: 'all-of', + description: `The noise to use`, + contains: [{ + type: 'LatentsField', + }], + }, + steps: { + type: 'number', + description: `The number of steps to use to generate the image`, + }, + width: { + type: 'number', + description: `The width of the resulting image`, + multipleOf: 64, + }, + height: { + type: 'number', + description: `The height of the resulting image`, + multipleOf: 64, + }, + cfg_scale: { + type: 'number', + description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`, + }, + scheduler: { + type: 'Enum', + }, + 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'`, + }, + model: { + type: 'string', + description: `The model to use (currently ignored)`, + }, + progress_images: { + type: 'boolean', + description: `Whether or not to produce progress images during generation`, + }, + latents: { + type: 'all-of', + description: `The latents to use as a base image`, + contains: [{ + type: 'LatentsField', + }], + }, + strength: { + type: 'number', + description: `The strength of the latents to use`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$LerpInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$LerpInvocation.ts new file mode 100644 index 0000000000..bafac85817 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$LerpInvocation.ts @@ -0,0 +1,33 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $LerpInvocation = { + description: `Linear interpolation of all pixels of an image`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The image to lerp`, + contains: [{ + type: 'ImageField', + }], + }, + min: { + type: 'number', + description: `The minimum output value`, + maximum: 255, + }, + max: { + type: 'number', + description: `The maximum output value`, + maximum: 255, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$LoadImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$LoadImageInvocation.ts new file mode 100644 index 0000000000..7b7a0cdffe --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$LoadImageInvocation.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $LoadImageInvocation = { + description: `Load an image and provide it as output.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image_type: { + type: 'all-of', + description: `The type of the image`, + contains: [{ + type: 'ImageType', + }], + isRequired: true, + }, + image_name: { + type: 'string', + description: `The name of the image`, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$MaskFromAlphaInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$MaskFromAlphaInvocation.ts new file mode 100644 index 0000000000..88c2089816 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$MaskFromAlphaInvocation.ts @@ -0,0 +1,27 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $MaskFromAlphaInvocation = { + description: `Extracts the alpha channel of an image as a mask.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The image to create the mask from`, + contains: [{ + type: 'ImageField', + }], + }, + invert: { + type: 'boolean', + description: `Whether or not to invert the mask`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$MaskOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$MaskOutput.ts new file mode 100644 index 0000000000..cc9d107ab5 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$MaskOutput.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $MaskOutput = { + description: `Base class for invocations that output a mask`, + properties: { + type: { + type: 'Enum', + isRequired: true, + }, + mask: { + type: 'all-of', + description: `The output mask`, + contains: [{ + type: 'ImageField', + }], + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$MetadataImageField.ts b/invokeai/frontend/web/src/services/api/schemas/$MetadataImageField.ts new file mode 100644 index 0000000000..5e4b1307ed --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$MetadataImageField.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $MetadataImageField = { + properties: { + image_type: { + type: 'ImageType', + isRequired: true, + }, + image_name: { + type: 'string', + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$MetadataLatentsField.ts b/invokeai/frontend/web/src/services/api/schemas/$MetadataLatentsField.ts new file mode 100644 index 0000000000..c377f26e42 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$MetadataLatentsField.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $MetadataLatentsField = { + properties: { + latents_name: { + type: 'string', + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ModelsList.ts b/invokeai/frontend/web/src/services/api/schemas/$ModelsList.ts new file mode 100644 index 0000000000..6fa85f6329 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ModelsList.ts @@ -0,0 +1,19 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ModelsList = { + properties: { + models: { + type: 'dictionary', + contains: { + type: 'one-of', + contains: [{ + type: 'CkptModelInfo', + }, { + type: 'DiffusersModelInfo', + }], + }, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$MultiplyInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$MultiplyInvocation.ts new file mode 100644 index 0000000000..4e8c1d4bbb --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$MultiplyInvocation.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $MultiplyInvocation = { + description: `Multiplies two numbers`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + 'a': { + type: 'number', + description: `The first number`, + }, + 'b': { + type: 'number', + description: `The second number`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$NoiseInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$NoiseInvocation.ts new file mode 100644 index 0000000000..446e77e747 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$NoiseInvocation.ts @@ -0,0 +1,31 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $NoiseInvocation = { + description: `Generates latent noise.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + seed: { + type: 'number', + description: `The seed to use`, + maximum: 4294967295, + }, + width: { + type: 'number', + description: `The width of the resulting noise`, + multipleOf: 64, + }, + height: { + type: 'number', + description: `The height of the resulting noise`, + multipleOf: 64, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$NoiseOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$NoiseOutput.ts new file mode 100644 index 0000000000..b0c3cc1d02 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$NoiseOutput.ts @@ -0,0 +1,18 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $NoiseOutput = { + description: `Invocation noise output`, + properties: { + type: { + type: 'Enum', + }, + noise: { + type: 'all-of', + description: `The output noise`, + contains: [{ + type: 'LatentsField', + }], + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_GraphExecutionState_.ts b/invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_GraphExecutionState_.ts new file mode 100644 index 0000000000..ca574eb463 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_GraphExecutionState_.ts @@ -0,0 +1,35 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $PaginatedResults_GraphExecutionState_ = { + description: `Paginated results`, + properties: { + items: { + type: 'array', + contains: { + type: 'GraphExecutionState', + }, + isRequired: true, + }, + page: { + type: 'number', + description: `Current Page`, + isRequired: true, + }, + pages: { + type: 'number', + description: `Total number of pages`, + isRequired: true, + }, + per_page: { + type: 'number', + description: `Number of items per page`, + isRequired: true, + }, + total: { + type: 'number', + description: `Total number of items in result`, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_ImageResponse_.ts b/invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_ImageResponse_.ts new file mode 100644 index 0000000000..113a374f85 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_ImageResponse_.ts @@ -0,0 +1,35 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $PaginatedResults_ImageResponse_ = { + description: `Paginated results`, + properties: { + items: { + type: 'array', + contains: { + type: 'ImageResponse', + }, + isRequired: true, + }, + page: { + type: 'number', + description: `Current Page`, + isRequired: true, + }, + pages: { + type: 'number', + description: `Total number of pages`, + isRequired: true, + }, + per_page: { + type: 'number', + description: `Number of items per page`, + isRequired: true, + }, + total: { + type: 'number', + description: `Total number of items in result`, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ParamIntInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$ParamIntInvocation.ts new file mode 100644 index 0000000000..a8eac4c450 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ParamIntInvocation.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ParamIntInvocation = { + description: `An integer parameter`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + 'a': { + type: 'number', + description: `The integer value`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$PasteImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$PasteImageInvocation.ts new file mode 100644 index 0000000000..74bb1edfcb --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$PasteImageInvocation.ts @@ -0,0 +1,45 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $PasteImageInvocation = { + description: `Pastes an image into another image.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + base_image: { + type: 'all-of', + description: `The base image`, + contains: [{ + type: 'ImageField', + }], + }, + image: { + type: 'all-of', + description: `The image to paste`, + contains: [{ + type: 'ImageField', + }], + }, + mask: { + type: 'all-of', + description: `The mask to use when pasting`, + contains: [{ + type: 'ImageField', + }], + }, + 'x': { + type: 'number', + description: `The left x coordinate at which to paste the image`, + }, + 'y': { + type: 'number', + description: `The top y coordinate at which to paste the image`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$PromptOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$PromptOutput.ts new file mode 100644 index 0000000000..29b800452f --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$PromptOutput.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $PromptOutput = { + description: `Base class for invocations that output a prompt`, + properties: { + type: { + type: 'Enum', + isRequired: true, + }, + prompt: { + type: 'string', + description: `The output prompt`, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$RandomRangeInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$RandomRangeInvocation.ts new file mode 100644 index 0000000000..f13e1a8332 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$RandomRangeInvocation.ts @@ -0,0 +1,33 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $RandomRangeInvocation = { + description: `Creates a collection of random numbers`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + low: { + type: 'number', + description: `The inclusive low value`, + }, + high: { + type: 'number', + description: `The exclusive high value`, + }, + size: { + type: 'number', + description: `The number of values to generate`, + }, + seed: { + type: 'number', + description: `The seed for the RNG`, + maximum: 2147483647, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$RangeInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$RangeInvocation.ts new file mode 100644 index 0000000000..f05dae51d4 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$RangeInvocation.ts @@ -0,0 +1,28 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $RangeInvocation = { + description: `Creates a range`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + start: { + type: 'number', + description: `The start of the range`, + }, + stop: { + type: 'number', + description: `The stop of the range`, + }, + step: { + type: 'number', + description: `The step of the range`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$RestoreFaceInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$RestoreFaceInvocation.ts new file mode 100644 index 0000000000..a9d10c480b --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$RestoreFaceInvocation.ts @@ -0,0 +1,28 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $RestoreFaceInvocation = { + description: `Restores faces in an image.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The input image`, + contains: [{ + type: 'ImageField', + }], + }, + strength: { + type: 'number', + description: `The strength of the restoration`, + maximum: 1, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ShowImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$ShowImageInvocation.ts new file mode 100644 index 0000000000..99a8ce0068 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ShowImageInvocation.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ShowImageInvocation = { + description: `Displays a provided image, and passes it forward in the pipeline.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The image to show`, + contains: [{ + type: 'ImageField', + }], + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$SubtractInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$SubtractInvocation.ts new file mode 100644 index 0000000000..be835de13b --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$SubtractInvocation.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $SubtractInvocation = { + description: `Subtracts two numbers`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + 'a': { + type: 'number', + description: `The first number`, + }, + 'b': { + type: 'number', + description: `The second number`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$TextToImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$TextToImageInvocation.ts new file mode 100644 index 0000000000..70c5858012 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$TextToImageInvocation.ts @@ -0,0 +1,59 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $TextToImageInvocation = { + description: `Generates an image using text2img.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + prompt: { + type: 'string', + description: `The prompt to generate an image from`, + }, + seed: { + type: 'number', + description: `The seed to use (-1 for a random seed)`, + maximum: 4294967295, + minimum: -1, + }, + steps: { + type: 'number', + description: `The number of steps to use to generate the image`, + }, + width: { + type: 'number', + description: `The width of the resulting image`, + multipleOf: 64, + }, + height: { + type: 'number', + description: `The height of the resulting image`, + multipleOf: 64, + }, + cfg_scale: { + type: 'number', + description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`, + }, + scheduler: { + type: 'Enum', + }, + seamless: { + type: 'boolean', + description: `Whether or not to generate an image that can tile without seams`, + }, + model: { + type: 'string', + description: `The model to use (currently ignored)`, + }, + progress_images: { + type: 'boolean', + description: `Whether or not to produce progress images during generation`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts new file mode 100644 index 0000000000..7b6dd155ca --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts @@ -0,0 +1,70 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $TextToLatentsInvocation = { + description: `Generates latents from a prompt.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + prompt: { + type: 'string', + description: `The prompt to generate an image from`, + }, + seed: { + type: 'number', + description: `The seed to use (-1 for a random seed)`, + maximum: 4294967295, + minimum: -1, + }, + noise: { + type: 'all-of', + description: `The noise to use`, + contains: [{ + type: 'LatentsField', + }], + }, + steps: { + type: 'number', + description: `The number of steps to use to generate the image`, + }, + width: { + type: 'number', + description: `The width of the resulting image`, + multipleOf: 64, + }, + height: { + type: 'number', + description: `The height of the resulting image`, + multipleOf: 64, + }, + cfg_scale: { + type: 'number', + description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`, + }, + scheduler: { + type: 'Enum', + }, + 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'`, + }, + model: { + type: 'string', + description: `The model to use (currently ignored)`, + }, + progress_images: { + type: 'boolean', + description: `Whether or not to produce progress images during generation`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$UpscaleInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$UpscaleInvocation.ts new file mode 100644 index 0000000000..21f87f1fb7 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$UpscaleInvocation.ts @@ -0,0 +1,31 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $UpscaleInvocation = { + description: `Upscales an image.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The input image`, + contains: [{ + type: 'ImageField', + }], + }, + strength: { + type: 'number', + description: `The strength`, + maximum: 1, + }, + level: { + type: 'Enum', + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$VaeRepo.ts b/invokeai/frontend/web/src/services/api/schemas/$VaeRepo.ts new file mode 100644 index 0000000000..8b8fbf0968 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$VaeRepo.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $VaeRepo = { + properties: { + repo_id: { + type: 'string', + description: `The repo ID to use for this VAE`, + isRequired: true, + }, + path: { + type: 'string', + description: `The path to the VAE`, + }, + subfolder: { + type: 'string', + description: `The subfolder to use for this VAE`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ValidationError.ts b/invokeai/frontend/web/src/services/api/schemas/$ValidationError.ts new file mode 100644 index 0000000000..d4c5c3e471 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ValidationError.ts @@ -0,0 +1,27 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ValidationError = { + properties: { + loc: { + type: 'array', + contains: { + type: 'any-of', + contains: [{ + type: 'string', + }, { + type: 'number', + }], + }, + isRequired: true, + }, + msg: { + type: 'string', + isRequired: true, + }, + type: { + type: 'string', + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/services/ImagesService.ts b/invokeai/frontend/web/src/services/api/services/ImagesService.ts new file mode 100644 index 0000000000..2d0f9435e9 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/services/ImagesService.ts @@ -0,0 +1,139 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { Body_upload_image } from '../models/Body_upload_image'; +import type { ImageResponse } from '../models/ImageResponse'; +import type { ImageType } from '../models/ImageType'; +import type { PaginatedResults_ImageResponse_ } from '../models/PaginatedResults_ImageResponse_'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class ImagesService { + + /** + * Get Image + * Gets a result + * @returns any Successful Response + * @throws ApiError + */ + public static getImage({ + imageType, + imageName, + }: { + /** + * The type of image to get + */ + imageType: ImageType, + /** + * The name of the image to get + */ + imageName: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/images/{image_type}/{image_name}', + path: { + 'image_type': imageType, + 'image_name': imageName, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Get Thumbnail + * Gets a thumbnail + * @returns any Successful Response + * @throws ApiError + */ + public static getThumbnail({ + imageType, + imageName, + }: { + /** + * The type of image to get + */ + imageType: ImageType, + /** + * The name of the image to get + */ + imageName: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/images/{image_type}/thumbnails/{image_name}', + path: { + 'image_type': imageType, + 'image_name': imageName, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Upload Image + * @returns ImageResponse The image was uploaded successfully + * @throws ApiError + */ + public static uploadImage({ + formData, + }: { + formData: Body_upload_image, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/images/uploads/', + formData: formData, + mediaType: 'multipart/form-data', + errors: { + 415: `Image upload failed`, + 422: `Validation Error`, + }, + }); + } + + /** + * List Images + * Gets a list of images + * @returns PaginatedResults_ImageResponse_ Successful Response + * @throws ApiError + */ + public static listImages({ + imageType, + page, + perPage = 10, + }: { + /** + * The type of images to get + */ + imageType?: ImageType, + /** + * The page of images to get + */ + page?: number, + /** + * The number of images per page + */ + perPage?: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/images/', + query: { + 'image_type': imageType, + 'page': page, + 'per_page': perPage, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + +} diff --git a/invokeai/frontend/web/src/services/api/services/ModelsService.ts b/invokeai/frontend/web/src/services/api/services/ModelsService.ts new file mode 100644 index 0000000000..3f8ae6bf7b --- /dev/null +++ b/invokeai/frontend/web/src/services/api/services/ModelsService.ts @@ -0,0 +1,72 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CreateModelRequest } from '../models/CreateModelRequest'; +import type { ModelsList } from '../models/ModelsList'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class ModelsService { + + /** + * List Models + * Gets a list of models + * @returns ModelsList Successful Response + * @throws ApiError + */ + public static listModels(): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/models/', + }); + } + + /** + * Update Model + * Add Model + * @returns any Successful Response + * @throws ApiError + */ + public static updateModel({ + requestBody, + }: { + requestBody: CreateModelRequest, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/models/', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Delete Model + * Delete Model + * @returns any Successful Response + * @throws ApiError + */ + public static delModel({ + modelName, + }: { + modelName: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'DELETE', + url: '/api/v1/models/{model_name}', + path: { + 'model_name': modelName, + }, + errors: { + 404: `Model not found`, + 422: `Validation Error`, + }, + }); + } + +} diff --git a/invokeai/frontend/web/src/services/api/services/SessionsService.ts b/invokeai/frontend/web/src/services/api/services/SessionsService.ts new file mode 100644 index 0000000000..269092c6d9 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/services/SessionsService.ts @@ -0,0 +1,381 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { AddInvocation } from '../models/AddInvocation'; +import type { BlurInvocation } from '../models/BlurInvocation'; +import type { CollectInvocation } from '../models/CollectInvocation'; +import type { CropImageInvocation } from '../models/CropImageInvocation'; +import type { CvInpaintInvocation } from '../models/CvInpaintInvocation'; +import type { DivideInvocation } from '../models/DivideInvocation'; +import type { Edge } from '../models/Edge'; +import type { Graph } from '../models/Graph'; +import type { GraphExecutionState } from '../models/GraphExecutionState'; +import type { GraphInvocation } from '../models/GraphInvocation'; +import type { ImageToImageInvocation } from '../models/ImageToImageInvocation'; +import type { InpaintInvocation } from '../models/InpaintInvocation'; +import type { InverseLerpInvocation } from '../models/InverseLerpInvocation'; +import type { IterateInvocation } from '../models/IterateInvocation'; +import type { LatentsToImageInvocation } from '../models/LatentsToImageInvocation'; +import type { LatentsToLatentsInvocation } from '../models/LatentsToLatentsInvocation'; +import type { LerpInvocation } from '../models/LerpInvocation'; +import type { LoadImageInvocation } from '../models/LoadImageInvocation'; +import type { MaskFromAlphaInvocation } from '../models/MaskFromAlphaInvocation'; +import type { MultiplyInvocation } from '../models/MultiplyInvocation'; +import type { NoiseInvocation } from '../models/NoiseInvocation'; +import type { PaginatedResults_GraphExecutionState_ } from '../models/PaginatedResults_GraphExecutionState_'; +import type { ParamIntInvocation } from '../models/ParamIntInvocation'; +import type { PasteImageInvocation } from '../models/PasteImageInvocation'; +import type { RandomRangeInvocation } from '../models/RandomRangeInvocation'; +import type { RangeInvocation } from '../models/RangeInvocation'; +import type { RestoreFaceInvocation } from '../models/RestoreFaceInvocation'; +import type { ShowImageInvocation } from '../models/ShowImageInvocation'; +import type { SubtractInvocation } from '../models/SubtractInvocation'; +import type { TextToImageInvocation } from '../models/TextToImageInvocation'; +import type { TextToLatentsInvocation } from '../models/TextToLatentsInvocation'; +import type { UpscaleInvocation } from '../models/UpscaleInvocation'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class SessionsService { + + /** + * List Sessions + * Gets a list of sessions, optionally searching + * @returns PaginatedResults_GraphExecutionState_ Successful Response + * @throws ApiError + */ + public static listSessions({ + page, + perPage = 10, + query = '', + }: { + /** + * The page of results to get + */ + page?: number, + /** + * The number of results per page + */ + perPage?: number, + /** + * The query string to search for + */ + query?: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/sessions/', + query: { + 'page': page, + 'per_page': perPage, + 'query': query, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Create Session + * Creates a new session, optionally initializing it with an invocation graph + * @returns GraphExecutionState Successful Response + * @throws ApiError + */ + public static createSession({ + requestBody, + }: { + requestBody?: Graph, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/sessions/', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Invalid json`, + 422: `Validation Error`, + }, + }); + } + + /** + * Get Session + * Gets a session + * @returns GraphExecutionState Successful Response + * @throws ApiError + */ + public static getSession({ + sessionId, + }: { + /** + * The id of the session to get + */ + sessionId: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/sessions/{session_id}', + path: { + 'session_id': sessionId, + }, + errors: { + 404: `Session not found`, + 422: `Validation Error`, + }, + }); + } + + /** + * Add Node + * Adds a node to the graph + * @returns string Successful Response + * @throws ApiError + */ + public static addNode({ + sessionId, + requestBody, + }: { + /** + * The id of the session + */ + sessionId: string, + requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/sessions/{session_id}/nodes', + path: { + 'session_id': sessionId, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Invalid node or link`, + 404: `Session not found`, + 422: `Validation Error`, + }, + }); + } + + /** + * Update Node + * Updates a node in the graph and removes all linked edges + * @returns GraphExecutionState Successful Response + * @throws ApiError + */ + public static updateNode({ + sessionId, + nodePath, + requestBody, + }: { + /** + * The id of the session + */ + sessionId: string, + /** + * The path to the node in the graph + */ + nodePath: string, + requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), + }): CancelablePromise { + return __request(OpenAPI, { + method: 'PUT', + url: '/api/v1/sessions/{session_id}/nodes/{node_path}', + path: { + 'session_id': sessionId, + 'node_path': nodePath, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Invalid node or link`, + 404: `Session not found`, + 422: `Validation Error`, + }, + }); + } + + /** + * Delete Node + * Deletes a node in the graph and removes all linked edges + * @returns GraphExecutionState Successful Response + * @throws ApiError + */ + public static deleteNode({ + sessionId, + nodePath, + }: { + /** + * The id of the session + */ + sessionId: string, + /** + * The path to the node to delete + */ + nodePath: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'DELETE', + url: '/api/v1/sessions/{session_id}/nodes/{node_path}', + path: { + 'session_id': sessionId, + 'node_path': nodePath, + }, + errors: { + 400: `Invalid node or link`, + 404: `Session not found`, + 422: `Validation Error`, + }, + }); + } + + /** + * Add Edge + * Adds an edge to the graph + * @returns GraphExecutionState Successful Response + * @throws ApiError + */ + public static addEdge({ + sessionId, + requestBody, + }: { + /** + * The id of the session + */ + sessionId: string, + requestBody: Edge, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/sessions/{session_id}/edges', + path: { + 'session_id': sessionId, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Invalid node or link`, + 404: `Session not found`, + 422: `Validation Error`, + }, + }); + } + + /** + * Delete Edge + * Deletes an edge from the graph + * @returns GraphExecutionState Successful Response + * @throws ApiError + */ + public static deleteEdge({ + sessionId, + fromNodeId, + fromField, + toNodeId, + toField, + }: { + /** + * The id of the session + */ + sessionId: string, + /** + * The id of the node the edge is coming from + */ + fromNodeId: string, + /** + * The field of the node the edge is coming from + */ + fromField: string, + /** + * The id of the node the edge is going to + */ + toNodeId: string, + /** + * The field of the node the edge is going to + */ + toField: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'DELETE', + url: '/api/v1/sessions/{session_id}/edges/{from_node_id}/{from_field}/{to_node_id}/{to_field}', + path: { + 'session_id': sessionId, + 'from_node_id': fromNodeId, + 'from_field': fromField, + 'to_node_id': toNodeId, + 'to_field': toField, + }, + errors: { + 400: `Invalid node or link`, + 404: `Session not found`, + 422: `Validation Error`, + }, + }); + } + + /** + * Invoke Session + * Invokes a session + * @returns any Successful Response + * @throws ApiError + */ + public static invokeSession({ + sessionId, + all = false, + }: { + /** + * The id of the session to invoke + */ + sessionId: string, + /** + * Whether or not to invoke all remaining invocations + */ + all?: boolean, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'PUT', + url: '/api/v1/sessions/{session_id}/invoke', + path: { + 'session_id': sessionId, + }, + query: { + 'all': all, + }, + errors: { + 400: `The session has no invocations ready to invoke`, + 404: `Session not found`, + 422: `Validation Error`, + }, + }); + } + + /** + * Cancel Session Invoke + * Invokes a session + * @returns any Successful Response + * @throws ApiError + */ + public static cancelSessionInvoke({ + sessionId, + }: { + /** + * The id of the session to cancel + */ + sessionId: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'DELETE', + url: '/api/v1/sessions/{session_id}/invoke', + path: { + 'session_id': sessionId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + +} diff --git a/invokeai/frontend/web/src/services/events/actions.ts b/invokeai/frontend/web/src/services/events/actions.ts new file mode 100644 index 0000000000..79435f52bc --- /dev/null +++ b/invokeai/frontend/web/src/services/events/actions.ts @@ -0,0 +1,50 @@ +import { createAction } from '@reduxjs/toolkit'; +import { + GeneratorProgressEvent, + InvocationCompleteEvent, + InvocationErrorEvent, + InvocationStartedEvent, +} from 'services/events/types'; + +// Common socket action payload data +type BaseSocketPayload = { + timestamp: string; +}; + +// Create actions for each socket event +// Middleware and redux can then respond to them as needed + +export const socketConnected = createAction( + 'socket/socketConnected' +); + +export const socketDisconnected = createAction( + 'socket/socketDisconnected' +); + +export const socketSubscribed = createAction< + BaseSocketPayload & { sessionId: string } +>('socket/socketSubscribed'); + +export const socketUnsubscribed = createAction< + BaseSocketPayload & { sessionId: string } +>('socket/socketUnsubscribed'); + +export const invocationStarted = createAction< + BaseSocketPayload & { data: InvocationStartedEvent } +>('socket/invocationStarted'); + +export const invocationComplete = createAction< + BaseSocketPayload & { data: InvocationCompleteEvent } +>('socket/invocationComplete'); + +export const invocationError = createAction< + BaseSocketPayload & { data: InvocationErrorEvent } +>('socket/invocationError'); + +export const generatorProgress = createAction< + BaseSocketPayload & { data: GeneratorProgressEvent } +>('socket/generatorProgress'); + +// dispatch this when we need to fully reset the socket connection +export const socketReset = createAction('socket/socketReset'); diff --git a/invokeai/frontend/web/src/services/events/middleware.ts b/invokeai/frontend/web/src/services/events/middleware.ts new file mode 100644 index 0000000000..9a462a1f85 --- /dev/null +++ b/invokeai/frontend/web/src/services/events/middleware.ts @@ -0,0 +1,221 @@ +import { Middleware, MiddlewareAPI } from '@reduxjs/toolkit'; +import { io, Socket } from 'socket.io-client'; + +import { + ClientToServerEvents, + ServerToClientEvents, +} from 'services/events/types'; +import { + generatorProgress, + invocationComplete, + invocationError, + invocationStarted, + socketConnected, + socketDisconnected, + socketReset, + socketSubscribed, + socketUnsubscribed, +} from './actions'; +import { + receivedResultImagesPage, + receivedUploadImagesPage, +} from 'services/thunks/gallery'; +import { AppDispatch, RootState } from 'app/store'; +import { getTimestamp } from 'common/util/getTimestamp'; +import { + sessionInvoked, + isFulfilledSessionCreatedAction, + sessionCanceled, +} from 'services/thunks/session'; +import { OpenAPI } from 'services/api'; +import { receivedModels } from 'services/thunks/model'; +import { receivedOpenAPISchema } from 'services/thunks/schema'; + +export const socketMiddleware = () => { + let areListenersSet = false; + + let socketUrl = `ws://${window.location.host}`; + + const socketOptions: Parameters[0] = { + timeout: 60000, + path: '/ws/socket.io', + autoConnect: false, // achtung! removing this breaks the dynamic middleware + }; + + // if building in package mode, replace socket url with open api base url minus the http protocol + if (['nodes', 'package'].includes(import.meta.env.MODE)) { + if (OpenAPI.BASE) { + //eslint-disable-next-line + socketUrl = OpenAPI.BASE.replace(/^https?\:\/\//i, ''); + } + + if (OpenAPI.TOKEN) { + // TODO: handle providing jwt to socket.io + socketOptions.auth = { token: OpenAPI.TOKEN }; + } + } + + const socket: Socket = io( + socketUrl, + socketOptions + ); + + const middleware: Middleware = + (store: MiddlewareAPI) => (next) => (action) => { + const { dispatch, getState } = store; + + // Nothing dispatches `socketReset` actions yet, so this is a noop, but including anyways + if (socketReset.match(action)) { + const { sessionId } = getState().system; + + if (sessionId) { + socket.emit('unsubscribe', { session: sessionId }); + dispatch( + socketUnsubscribed({ sessionId, timestamp: getTimestamp() }) + ); + } + + if (socket.connected) { + socket.disconnect(); + dispatch(socketDisconnected({ timestamp: getTimestamp() })); + } + + socket.removeAllListeners(); + areListenersSet = false; + } + + // Set listeners for `connect` and `disconnect` events once + // Must happen in middleware to get access to `dispatch` + if (!areListenersSet) { + socket.on('connect', () => { + dispatch(socketConnected({ timestamp: getTimestamp() })); + + const { results, uploads, models, nodes } = getState(); + + // These thunks need to be dispatch in middleware; cannot handle in a reducer + if (!results.ids.length) { + dispatch(receivedResultImagesPage()); + } + + if (!uploads.ids.length) { + dispatch(receivedUploadImagesPage()); + } + + if (!models.ids.length) { + dispatch(receivedModels()); + } + + if (!nodes.schema) { + dispatch(receivedOpenAPISchema()); + } + }); + + socket.on('disconnect', () => { + dispatch(socketDisconnected({ timestamp: getTimestamp() })); + }); + + areListenersSet = true; + + // must manually connect + socket.connect(); + } + + // Everything else only happens once we have created a session + if (isFulfilledSessionCreatedAction(action)) { + const oldSessionId = getState().system.sessionId; + + // temp disable event subscription + const shouldHandleEvent = (id: string): boolean => true; + + // const subscribedNodeIds = getState().system.subscribedNodeIds; + // const shouldHandleEvent = (id: string): boolean => { + // if (subscribedNodeIds.length === 1 && subscribedNodeIds[0] === '*') { + // return true; + // } + + // return subscribedNodeIds.includes(id); + // }; + + if (oldSessionId) { + // Unsubscribe when invocations complete + socket.emit('unsubscribe', { + session: oldSessionId, + }); + + dispatch( + socketUnsubscribed({ + sessionId: oldSessionId, + timestamp: getTimestamp(), + }) + ); + + const listenersToRemove: (keyof ServerToClientEvents)[] = [ + 'invocation_started', + 'generator_progress', + 'invocation_error', + 'invocation_complete', + ]; + + // Remove listeners for these events; we need to set them up fresh whenever we subscribe + listenersToRemove.forEach((event: keyof ServerToClientEvents) => { + socket.removeAllListeners(event); + }); + } + + const sessionId = action.payload.id; + + // After a session is created, we immediately subscribe to events and then invoke the session + socket.emit('subscribe', { session: sessionId }); + + // Always dispatch the event actions for other consumers who want to know when we subscribed + dispatch( + socketSubscribed({ + sessionId, + timestamp: getTimestamp(), + }) + ); + + // Set up listeners for the present subscription + socket.on('invocation_started', (data) => { + if (shouldHandleEvent(data.node.id)) { + dispatch(invocationStarted({ data, timestamp: getTimestamp() })); + } + }); + + socket.on('generator_progress', (data) => { + if (shouldHandleEvent(data.node.id)) { + dispatch(generatorProgress({ data, timestamp: getTimestamp() })); + } + }); + + socket.on('invocation_error', (data) => { + if (shouldHandleEvent(data.node.id)) { + dispatch(invocationError({ data, timestamp: getTimestamp() })); + } + }); + + socket.on('invocation_complete', (data) => { + if (shouldHandleEvent(data.node.id)) { + const sessionId = data.graph_execution_state_id; + + const { cancelType, isCancelScheduled } = getState().system; + + // Handle scheduled cancelation + if (cancelType === 'scheduled' && isCancelScheduled) { + dispatch(sessionCanceled({ sessionId })); + } + + dispatch(invocationComplete({ data, timestamp: getTimestamp() })); + } + }); + + // Finally we actually invoke the session, starting processing + dispatch(sessionInvoked({ sessionId })); + } + + // Always pass the action on so other middleware and reducers can handle it + next(action); + }; + + return middleware; +}; diff --git a/invokeai/frontend/web/src/services/events/types.ts b/invokeai/frontend/web/src/services/events/types.ts new file mode 100644 index 0000000000..8452c46340 --- /dev/null +++ b/invokeai/frontend/web/src/services/events/types.ts @@ -0,0 +1,109 @@ +import { Graph, GraphExecutionState } from '../api'; + +/** + * A progress image, we get one for each step in the generation + */ +export type ProgressImage = { + dataURL: string; + width: number; + height: number; +}; + +export type AnyInvocationType = NonNullable< + NonNullable[string]['type'] +>; + +export type AnyInvocation = NonNullable[string]; + +// export type AnyInvocation = { +// id: string; +// type: AnyInvocationType | string; +// [key: string]: any; +// }; + +export type AnyResult = GraphExecutionState['results'][string]; + +/** + * A `generator_progress` socket.io event. + * + * @example socket.on('generator_progress', (data: GeneratorProgressEvent) => { ... } + */ +export type GeneratorProgressEvent = { + graph_execution_state_id: string; + node: AnyInvocation; + source_node_id: string; + progress_image?: ProgressImage; + step: number; + total_steps: number; +}; + +/** + * A `invocation_complete` socket.io event. + * + * `result` is a discriminated union with a `type` property as the discriminant. + * + * @example socket.on('invocation_complete', (data: InvocationCompleteEvent) => { ... } + */ +export type InvocationCompleteEvent = { + graph_execution_state_id: string; + node: AnyInvocation; + source_node_id: string; + result: AnyResult; +}; + +/** + * A `invocation_error` socket.io event. + * + * @example socket.on('invocation_error', (data: InvocationErrorEvent) => { ... } + */ +export type InvocationErrorEvent = { + graph_execution_state_id: string; + node: AnyInvocation; + source_node_id: string; + error: string; +}; + +/** + * A `invocation_started` socket.io event. + * + * @example socket.on('invocation_started', (data: InvocationStartedEvent) => { ... } + */ +export type InvocationStartedEvent = { + graph_execution_state_id: string; + node: AnyInvocation; + source_node_id: string; +}; + +/** + * A `graph_execution_state_complete` socket.io event. + * + * @example socket.on('graph_execution_state_complete', (data: GraphExecutionStateCompleteEvent) => { ... } + */ +export type GraphExecutionStateCompleteEvent = { + graph_execution_state_id: string; +}; + +export type ClientEmitSubscribe = { + session: string; +}; + +export type ClientEmitUnsubscribe = { + session: string; +}; + +export type ServerToClientEvents = { + generator_progress: (payload: GeneratorProgressEvent) => void; + invocation_complete: (payload: InvocationCompleteEvent) => void; + invocation_error: (payload: InvocationErrorEvent) => void; + invocation_started: (payload: InvocationStartedEvent) => void; + graph_execution_state_complete: ( + payload: GraphExecutionStateCompleteEvent + ) => void; +}; + +export type ClientToServerEvents = { + connect: () => void; + disconnect: () => void; + subscribe: (payload: ClientEmitSubscribe) => void; + unsubscribe: (payload: ClientEmitUnsubscribe) => void; +}; diff --git a/invokeai/frontend/web/src/services/fixtures/openapi.json b/invokeai/frontend/web/src/services/fixtures/openapi.json new file mode 100644 index 0000000000..fb2cddf3e7 --- /dev/null +++ b/invokeai/frontend/web/src/services/fixtures/openapi.json @@ -0,0 +1 @@ +{"openapi":"3.0.2","info":{"title":"Invoke AI","description":"An API for invoking AI image operations","version":"1.0.0"},"paths":{"/api/v1/sessions/":{"get":{"tags":["sessions"],"summary":"List Sessions","description":"Gets a list of sessions, optionally searching","operationId":"list_sessions","parameters":[{"description":"The page of results to get","required":false,"schema":{"title":"Page","type":"integer","description":"The page of results to get","default":0},"name":"page","in":"query"},{"description":"The number of results per page","required":false,"schema":{"title":"Per Page","type":"integer","description":"The number of results per page","default":10},"name":"per_page","in":"query"},{"description":"The query string to search for","required":false,"schema":{"title":"Query","type":"string","description":"The query string to search for","default":""},"name":"query","in":"query"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResults_GraphExecutionState_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["sessions"],"summary":"Create Session","description":"Creates a new session, optionally initializing it with an invocation graph","operationId":"create_session","requestBody":{"content":{"application/json":{"schema":{"title":"Graph","allOf":[{"$ref":"#/components/schemas/Graph"}],"description":"The graph to initialize the session with"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphExecutionState"}}}},"400":{"description":"Invalid json"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sessions/{session_id}":{"get":{"tags":["sessions"],"summary":"Get Session","description":"Gets a session","operationId":"get_session","parameters":[{"description":"The id of the session to get","required":true,"schema":{"title":"Session Id","type":"string","description":"The id of the session to get"},"name":"session_id","in":"path"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphExecutionState"}}}},"404":{"description":"Session not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sessions/{session_id}/nodes":{"post":{"tags":["sessions"],"summary":"Add Node","description":"Adds a node to the graph","operationId":"add_node","parameters":[{"description":"The id of the session","required":true,"schema":{"title":"Session Id","type":"string","description":"The id of the session"},"name":"session_id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"title":"Node","anyOf":[{"$ref":"#/components/schemas/LoadImageInvocation"},{"$ref":"#/components/schemas/ShowImageInvocation"},{"$ref":"#/components/schemas/CropImageInvocation"},{"$ref":"#/components/schemas/PasteImageInvocation"},{"$ref":"#/components/schemas/MaskFromAlphaInvocation"},{"$ref":"#/components/schemas/BlurInvocation"},{"$ref":"#/components/schemas/LerpInvocation"},{"$ref":"#/components/schemas/InverseLerpInvocation"},{"$ref":"#/components/schemas/CvInpaintInvocation"},{"$ref":"#/components/schemas/UpscaleInvocation"},{"$ref":"#/components/schemas/RestoreFaceInvocation"},{"$ref":"#/components/schemas/TextToImageInvocation"},{"$ref":"#/components/schemas/GraphInvocation"},{"$ref":"#/components/schemas/IterateInvocation"},{"$ref":"#/components/schemas/CollectInvocation"},{"$ref":"#/components/schemas/ImageToImageInvocation"},{"$ref":"#/components/schemas/InpaintInvocation"}],"description":"The node to add"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response 200 Add Node","type":"string"}}}},"400":{"description":"Invalid node or link"},"404":{"description":"Session not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sessions/{session_id}/nodes/{node_path}":{"put":{"tags":["sessions"],"summary":"Update Node","description":"Updates a node in the graph and removes all linked edges","operationId":"update_node","parameters":[{"description":"The id of the session","required":true,"schema":{"title":"Session Id","type":"string","description":"The id of the session"},"name":"session_id","in":"path"},{"description":"The path to the node in the graph","required":true,"schema":{"title":"Node Path","type":"string","description":"The path to the node in the graph"},"name":"node_path","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"title":"Node","anyOf":[{"$ref":"#/components/schemas/LoadImageInvocation"},{"$ref":"#/components/schemas/ShowImageInvocation"},{"$ref":"#/components/schemas/CropImageInvocation"},{"$ref":"#/components/schemas/PasteImageInvocation"},{"$ref":"#/components/schemas/MaskFromAlphaInvocation"},{"$ref":"#/components/schemas/BlurInvocation"},{"$ref":"#/components/schemas/LerpInvocation"},{"$ref":"#/components/schemas/InverseLerpInvocation"},{"$ref":"#/components/schemas/CvInpaintInvocation"},{"$ref":"#/components/schemas/UpscaleInvocation"},{"$ref":"#/components/schemas/RestoreFaceInvocation"},{"$ref":"#/components/schemas/TextToImageInvocation"},{"$ref":"#/components/schemas/GraphInvocation"},{"$ref":"#/components/schemas/IterateInvocation"},{"$ref":"#/components/schemas/CollectInvocation"},{"$ref":"#/components/schemas/ImageToImageInvocation"},{"$ref":"#/components/schemas/InpaintInvocation"}],"description":"The new node"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphExecutionState"}}}},"400":{"description":"Invalid node or link"},"404":{"description":"Session not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["sessions"],"summary":"Delete Node","description":"Deletes a node in the graph and removes all linked edges","operationId":"delete_node","parameters":[{"description":"The id of the session","required":true,"schema":{"title":"Session Id","type":"string","description":"The id of the session"},"name":"session_id","in":"path"},{"description":"The path to the node to delete","required":true,"schema":{"title":"Node Path","type":"string","description":"The path to the node to delete"},"name":"node_path","in":"path"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphExecutionState"}}}},"400":{"description":"Invalid node or link"},"404":{"description":"Session not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sessions/{session_id}/edges":{"post":{"tags":["sessions"],"summary":"Add Edge","description":"Adds an edge to the graph","operationId":"add_edge","parameters":[{"description":"The id of the session","required":true,"schema":{"title":"Session Id","type":"string","description":"The id of the session"},"name":"session_id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"title":"Edge","allOf":[{"$ref":"#/components/schemas/Edge"}],"description":"The edge to add"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphExecutionState"}}}},"400":{"description":"Invalid node or link"},"404":{"description":"Session not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sessions/{session_id}/edges/{from_node_id}/{from_field}/{to_node_id}/{to_field}":{"delete":{"tags":["sessions"],"summary":"Delete Edge","description":"Deletes an edge from the graph","operationId":"delete_edge","parameters":[{"description":"The id of the session","required":true,"schema":{"title":"Session Id","type":"string","description":"The id of the session"},"name":"session_id","in":"path"},{"description":"The id of the node the edge is coming from","required":true,"schema":{"title":"From Node Id","type":"string","description":"The id of the node the edge is coming from"},"name":"from_node_id","in":"path"},{"description":"The field of the node the edge is coming from","required":true,"schema":{"title":"From Field","type":"string","description":"The field of the node the edge is coming from"},"name":"from_field","in":"path"},{"description":"The id of the node the edge is going to","required":true,"schema":{"title":"To Node Id","type":"string","description":"The id of the node the edge is going to"},"name":"to_node_id","in":"path"},{"description":"The field of the node the edge is going to","required":true,"schema":{"title":"To Field","type":"string","description":"The field of the node the edge is going to"},"name":"to_field","in":"path"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphExecutionState"}}}},"400":{"description":"Invalid node or link"},"404":{"description":"Session not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sessions/{session_id}/invoke":{"put":{"tags":["sessions"],"summary":"Invoke Session","description":"Invokes a session","operationId":"invoke_session","parameters":[{"description":"The id of the session to invoke","required":true,"schema":{"title":"Session Id","type":"string","description":"The id of the session to invoke"},"name":"session_id","in":"path"},{"description":"Whether or not to invoke all remaining invocations","required":false,"schema":{"title":"All","type":"boolean","description":"Whether or not to invoke all remaining invocations","default":false},"name":"all","in":"query"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"202":{"description":"The invocation is queued"},"400":{"description":"The session has no invocations ready to invoke"},"404":{"description":"Session not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/images/{image_type}/{image_name}":{"get":{"tags":["images"],"summary":"Get Image","description":"Gets a result","operationId":"get_image","parameters":[{"description":"The type of image to get","required":true,"schema":{"allOf":[{"$ref":"#/components/schemas/ImageType"}],"description":"The type of image to get"},"name":"image_type","in":"path"},{"description":"The name of the image to get","required":true,"schema":{"title":"Image Name","type":"string","description":"The name of the image to get"},"name":"image_name","in":"path"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/images/uploads/":{"post":{"tags":["images"],"summary":"Upload Image","operationId":"upload_image","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_image"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"201":{"description":"The image was uploaded successfully"},"404":{"description":"Session not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"BlurInvocation":{"title":"BlurInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["blur"],"type":"string","default":"blur"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The image to blur"},"radius":{"title":"Radius","minimum":0.0,"type":"number","description":"The blur radius","default":8.0},"blur_type":{"title":"Blur Type","enum":["gaussian","box"],"type":"string","description":"The type of blur","default":"gaussian"}},"description":"Blurs an image","output":{"$ref":"#/components/schemas/ImageOutput"}},"Body_upload_image":{"title":"Body_upload_image","required":["file"],"type":"object","properties":{"file":{"title":"File","type":"string","format":"binary"}}},"CollectInvocation":{"title":"CollectInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["collect"],"type":"string","default":"collect"},"item":{"title":"Item","description":"The item to collect (all inputs must be of the same type)"},"collection":{"title":"Collection","type":"array","items":{},"description":"The collection, will be provided on execution"}},"description":"Collects values into a collection","output":{"$ref":"#/components/schemas/CollectInvocationOutput"}},"CollectInvocationOutput":{"title":"CollectInvocationOutput","description":"Base class for all invocation outputs","type":"object","properties":{"type":{"title":"Type","default":"collect_output","enum":["collect_output"],"type":"string"},"collection":{"title":"Collection","description":"The collection of input items","type":"array","items":{}}},"required":["collection"]},"CropImageInvocation":{"title":"CropImageInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["crop"],"type":"string","default":"crop"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The image to crop"},"x":{"title":"X","type":"integer","description":"The left x coordinate of the crop rectangle","default":0},"y":{"title":"Y","type":"integer","description":"The top y coordinate of the crop rectangle","default":0},"width":{"title":"Width","exclusiveMinimum":0.0,"type":"integer","description":"The width of the crop rectangle","default":512},"height":{"title":"Height","exclusiveMinimum":0.0,"type":"integer","description":"The height of the crop rectangle","default":512}},"description":"Crops an image to a specified box. The box can be outside of the image.","output":{"$ref":"#/components/schemas/ImageOutput"}},"CvInpaintInvocation":{"title":"CvInpaintInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["cv_inpaint"],"type":"string","default":"cv_inpaint"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The image to inpaint"},"mask":{"title":"Mask","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The mask to use when inpainting"}},"description":"Simple inpaint using opencv.","output":{"$ref":"#/components/schemas/ImageOutput"}},"Edge":{"title":"Edge","required":["source","destination"],"type":"object","properties":{"source":{"title":"Source","allOf":[{"$ref":"#/components/schemas/EdgeConnection"}],"description":"The connection for the edge's from node and field"},"destination":{"title":"Destination","allOf":[{"$ref":"#/components/schemas/EdgeConnection"}],"description":"The connection for the edge's to node and field"}}},"EdgeConnection":{"title":"EdgeConnection","required":["node_id","field"],"type":"object","properties":{"node_id":{"title":"Node Id","type":"string","description":"The id of the node for this edge connection"},"field":{"title":"Field","type":"string","description":"The field for this connection"}}},"Graph":{"title":"Graph","type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this graph"},"nodes":{"title":"Nodes","type":"object","additionalProperties":{"oneOf":[{"$ref":"#/components/schemas/LoadImageInvocation"},{"$ref":"#/components/schemas/ShowImageInvocation"},{"$ref":"#/components/schemas/CropImageInvocation"},{"$ref":"#/components/schemas/PasteImageInvocation"},{"$ref":"#/components/schemas/MaskFromAlphaInvocation"},{"$ref":"#/components/schemas/BlurInvocation"},{"$ref":"#/components/schemas/LerpInvocation"},{"$ref":"#/components/schemas/InverseLerpInvocation"},{"$ref":"#/components/schemas/CvInpaintInvocation"},{"$ref":"#/components/schemas/UpscaleInvocation"},{"$ref":"#/components/schemas/RestoreFaceInvocation"},{"$ref":"#/components/schemas/TextToImageInvocation"},{"$ref":"#/components/schemas/GraphInvocation"},{"$ref":"#/components/schemas/IterateInvocation"},{"$ref":"#/components/schemas/CollectInvocation"},{"$ref":"#/components/schemas/ImageToImageInvocation"},{"$ref":"#/components/schemas/InpaintInvocation"}],"discriminator":{"propertyName":"type","mapping":{"load_image":"#/components/schemas/LoadImageInvocation","show_image":"#/components/schemas/ShowImageInvocation","crop":"#/components/schemas/CropImageInvocation","paste":"#/components/schemas/PasteImageInvocation","tomask":"#/components/schemas/MaskFromAlphaInvocation","blur":"#/components/schemas/BlurInvocation","lerp":"#/components/schemas/LerpInvocation","ilerp":"#/components/schemas/InverseLerpInvocation","cv_inpaint":"#/components/schemas/CvInpaintInvocation","upscale":"#/components/schemas/UpscaleInvocation","restore_face":"#/components/schemas/RestoreFaceInvocation","txt2img":"#/components/schemas/TextToImageInvocation","graph":"#/components/schemas/GraphInvocation","iterate":"#/components/schemas/IterateInvocation","collect":"#/components/schemas/CollectInvocation","img2img":"#/components/schemas/ImageToImageInvocation","inpaint":"#/components/schemas/InpaintInvocation"}}},"description":"The nodes in this graph"},"edges":{"title":"Edges","type":"array","items":{"$ref":"#/components/schemas/Edge"},"description":"The connections between nodes and their fields in this graph"}}},"GraphExecutionState":{"title":"GraphExecutionState","required":["graph"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of the execution state"},"graph":{"title":"Graph","allOf":[{"$ref":"#/components/schemas/Graph"}],"description":"The graph being executed"},"execution_graph":{"title":"Execution Graph","allOf":[{"$ref":"#/components/schemas/Graph"}],"description":"The expanded graph of activated and executed nodes"},"executed":{"title":"Executed","uniqueItems":true,"type":"array","items":{"type":"string"},"description":"The set of node ids that have been executed"},"executed_history":{"title":"Executed History","type":"array","items":{"type":"string"},"description":"The list of node ids that have been executed, in order of execution"},"results":{"title":"Results","type":"object","additionalProperties":{"oneOf":[{"$ref":"#/components/schemas/ImageOutput"},{"$ref":"#/components/schemas/MaskOutput"},{"$ref":"#/components/schemas/PromptOutput"},{"$ref":"#/components/schemas/GraphInvocationOutput"},{"$ref":"#/components/schemas/IterateInvocationOutput"},{"$ref":"#/components/schemas/CollectInvocationOutput"}],"discriminator":{"propertyName":"type","mapping":{"image":"#/components/schemas/ImageOutput","mask":"#/components/schemas/MaskOutput","prompt":"#/components/schemas/PromptOutput","graph_output":"#/components/schemas/GraphInvocationOutput","iterate_output":"#/components/schemas/IterateInvocationOutput","collect_output":"#/components/schemas/CollectInvocationOutput"}}},"description":"The results of node executions"},"errors":{"title":"Errors","type":"object","additionalProperties":{"type":"string"},"description":"Errors raised when executing nodes"},"prepared_source_mapping":{"title":"Prepared Source Mapping","type":"object","additionalProperties":{"type":"string"},"description":"The map of prepared nodes to original graph nodes"},"source_prepared_mapping":{"title":"Source Prepared Mapping","type":"object","additionalProperties":{"uniqueItems":true,"type":"array","items":{"type":"string"}},"description":"The map of original graph nodes to prepared nodes"}},"description":"Tracks the state of a graph execution"},"GraphInvocation":{"title":"GraphInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["graph"],"type":"string","default":"graph"},"graph":{"title":"Graph","allOf":[{"$ref":"#/components/schemas/Graph"}],"description":"The graph to run"}},"description":"A node to process inputs and produce outputs.\nMay use dependency injection in __init__ to receive providers.","output":{"$ref":"#/components/schemas/GraphInvocationOutput"}},"GraphInvocationOutput":{"title":"GraphInvocationOutput","description":"Base class for all invocation outputs","type":"object","properties":{"type":{"title":"Type","default":"graph_output","enum":["graph_output"],"type":"string"}}},"HTTPValidationError":{"title":"HTTPValidationError","type":"object","properties":{"detail":{"title":"Detail","type":"array","items":{"$ref":"#/components/schemas/ValidationError"}}}},"ImageField":{"title":"ImageField","description":"An image field used for passing image objects between invocations","type":"object","properties":{"image_type":{"title":"Image Type","description":"The type of the image","default":"results","type":"string"},"image_name":{"title":"Image Name","description":"The name of the image","type":"string"}}},"ImageOutput":{"title":"ImageOutput","description":"Base class for invocations that output an image","type":"object","properties":{"type":{"title":"Type","default":"image","enum":["image"],"type":"string"},"image":{"title":"Image","description":"The output image","allOf":[{"$ref":"#/components/schemas/ImageField"}]}}},"ImageToImageInvocation":{"title":"ImageToImageInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["img2img"],"type":"string","default":"img2img"},"prompt":{"title":"Prompt","type":"string","description":"The prompt to generate an image from"},"seed":{"title":"Seed","maximum":4294967295.0,"minimum":-1.0,"type":"integer","description":"The seed to use (-1 for a random seed)","default":-1},"steps":{"title":"Steps","exclusiveMinimum":0.0,"type":"integer","description":"The number of steps to use to generate the image","default":10},"width":{"title":"Width","multipleOf":64.0,"exclusiveMinimum":0.0,"type":"integer","description":"The width of the resulting image","default":512},"height":{"title":"Height","multipleOf":64.0,"exclusiveMinimum":0.0,"type":"integer","description":"The height of the resulting image","default":512},"cfg_scale":{"title":"Cfg Scale","exclusiveMinimum":0.0,"type":"number","description":"The Classifier-Free Guidance, higher values may result in a result closer to the prompt","default":7.5},"sampler_name":{"title":"Sampler Name","enum":["ddim","dpmpp_2","k_dpm_2","k_dpm_2_a","k_dpmpp_2","k_euler","k_euler_a","k_heun","k_lms","plms"],"type":"string","description":"The sampler to use","default":"k_lms"},"seamless":{"title":"Seamless","type":"boolean","description":"Whether or not to generate an image that can tile without seams","default":false},"model":{"title":"Model","type":"string","description":"The model to use (currently ignored)","default":""},"progress_images":{"title":"Progress Images","type":"boolean","description":"Whether or not to produce progress images during generation","default":false},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The input image"},"strength":{"title":"Strength","maximum":1.0,"exclusiveMinimum":0.0,"type":"number","description":"The strength of the original image","default":0.75},"fit":{"title":"Fit","type":"boolean","description":"Whether or not the result should be fit to the aspect ratio of the input image","default":true}},"description":"Generates an image using img2img.","output":{"$ref":"#/components/schemas/ImageOutput"}},"ImageType":{"title":"ImageType","enum":["results","intermediates","uploads"],"type":"string","description":"An enumeration."},"InpaintInvocation":{"title":"InpaintInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["inpaint"],"type":"string","default":"inpaint"},"prompt":{"title":"Prompt","type":"string","description":"The prompt to generate an image from"},"seed":{"title":"Seed","maximum":4294967295.0,"minimum":-1.0,"type":"integer","description":"The seed to use (-1 for a random seed)","default":-1},"steps":{"title":"Steps","exclusiveMinimum":0.0,"type":"integer","description":"The number of steps to use to generate the image","default":10},"width":{"title":"Width","multipleOf":64.0,"exclusiveMinimum":0.0,"type":"integer","description":"The width of the resulting image","default":512},"height":{"title":"Height","multipleOf":64.0,"exclusiveMinimum":0.0,"type":"integer","description":"The height of the resulting image","default":512},"cfg_scale":{"title":"Cfg Scale","exclusiveMinimum":0.0,"type":"number","description":"The Classifier-Free Guidance, higher values may result in a result closer to the prompt","default":7.5},"sampler_name":{"title":"Sampler Name","enum":["ddim","dpmpp_2","k_dpm_2","k_dpm_2_a","k_dpmpp_2","k_euler","k_euler_a","k_heun","k_lms","plms"],"type":"string","description":"The sampler to use","default":"k_lms"},"seamless":{"title":"Seamless","type":"boolean","description":"Whether or not to generate an image that can tile without seams","default":false},"model":{"title":"Model","type":"string","description":"The model to use (currently ignored)","default":""},"progress_images":{"title":"Progress Images","type":"boolean","description":"Whether or not to produce progress images during generation","default":false},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The input image"},"strength":{"title":"Strength","maximum":1.0,"exclusiveMinimum":0.0,"type":"number","description":"The strength of the original image","default":0.75},"fit":{"title":"Fit","type":"boolean","description":"Whether or not the result should be fit to the aspect ratio of the input image","default":true},"mask":{"title":"Mask","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The mask"},"inpaint_replace":{"title":"Inpaint Replace","maximum":1.0,"minimum":0.0,"type":"number","description":"The amount by which to replace masked areas with latent noise","default":0.0}},"description":"Generates an image using inpaint.","output":{"$ref":"#/components/schemas/ImageOutput"}},"InverseLerpInvocation":{"title":"InverseLerpInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["ilerp"],"type":"string","default":"ilerp"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The image to lerp"},"min":{"title":"Min","maximum":255.0,"minimum":0.0,"type":"integer","description":"The minimum input value","default":0},"max":{"title":"Max","maximum":255.0,"minimum":0.0,"type":"integer","description":"The maximum input value","default":255}},"description":"Inverse linear interpolation of all pixels of an image","output":{"$ref":"#/components/schemas/ImageOutput"}},"IterateInvocation":{"title":"IterateInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["iterate"],"type":"string","default":"iterate"},"collection":{"title":"Collection","type":"array","items":{},"description":"The list of items to iterate over"},"index":{"title":"Index","type":"integer","description":"The index, will be provided on executed iterators","default":0}},"description":"A node to process inputs and produce outputs.\nMay use dependency injection in __init__ to receive providers.","output":{"$ref":"#/components/schemas/IterateInvocationOutput"}},"IterateInvocationOutput":{"title":"IterateInvocationOutput","description":"Used to connect iteration outputs. Will be expanded to a specific output.","type":"object","properties":{"type":{"title":"Type","default":"iterate_output","enum":["iterate_output"],"type":"string"},"item":{"title":"Item","description":"The item being iterated over"}}},"LerpInvocation":{"title":"LerpInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["lerp"],"type":"string","default":"lerp"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The image to lerp"},"min":{"title":"Min","maximum":255.0,"minimum":0.0,"type":"integer","description":"The minimum output value","default":0},"max":{"title":"Max","maximum":255.0,"minimum":0.0,"type":"integer","description":"The maximum output value","default":255}},"description":"Linear interpolation of all pixels of an image","output":{"$ref":"#/components/schemas/ImageOutput"}},"LoadImageInvocation":{"title":"LoadImageInvocation","required":["id","image_type","image_name"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["load_image"],"type":"string","default":"load_image"},"image_type":{"allOf":[{"$ref":"#/components/schemas/ImageType"}],"description":"The type of the image"},"image_name":{"title":"Image Name","type":"string","description":"The name of the image"}},"description":"Load an image from a filename and provide it as output.","output":{"$ref":"#/components/schemas/ImageOutput"}},"MaskFromAlphaInvocation":{"title":"MaskFromAlphaInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["tomask"],"type":"string","default":"tomask"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The image to create the mask from"},"invert":{"title":"Invert","type":"boolean","description":"Whether or not to invert the mask","default":false}},"description":"Extracts the alpha channel of an image as a mask.","output":{"$ref":"#/components/schemas/MaskOutput"}},"MaskOutput":{"title":"MaskOutput","description":"Base class for invocations that output a mask","type":"object","properties":{"type":{"title":"Type","default":"mask","enum":["mask"],"type":"string"},"mask":{"title":"Mask","description":"The output mask","allOf":[{"$ref":"#/components/schemas/ImageField"}]}}},"PaginatedResults_GraphExecutionState_":{"title":"PaginatedResults[GraphExecutionState]","required":["items","page","pages","per_page","total"],"type":"object","properties":{"items":{"title":"Items","type":"array","items":{"$ref":"#/components/schemas/GraphExecutionState"},"description":"Items"},"page":{"title":"Page","type":"integer","description":"Current Page"},"pages":{"title":"Pages","type":"integer","description":"Total number of pages"},"per_page":{"title":"Per Page","type":"integer","description":"Number of items per page"},"total":{"title":"Total","type":"integer","description":"Total number of items in result"}},"description":"Paginated results"},"PasteImageInvocation":{"title":"PasteImageInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["paste"],"type":"string","default":"paste"},"base_image":{"title":"Base Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The base image"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The image to paste"},"mask":{"title":"Mask","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The mask to use when pasting"},"x":{"title":"X","type":"integer","description":"The left x coordinate at which to paste the image","default":0},"y":{"title":"Y","type":"integer","description":"The top y coordinate at which to paste the image","default":0}},"description":"Pastes an image into another image.","output":{"$ref":"#/components/schemas/ImageOutput"}},"PromptOutput":{"title":"PromptOutput","type":"object","properties":{"type":{"title":"Type","enum":["prompt"],"type":"string","default":"prompt"},"prompt":{"title":"Prompt","type":"string","description":"The output prompt"}},"description":"Base class for invocations that output a prompt"},"RestoreFaceInvocation":{"title":"RestoreFaceInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["restore_face"],"type":"string","default":"restore_face"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The input image"},"strength":{"title":"Strength","maximum":1.0,"exclusiveMinimum":0.0,"type":"number","description":"The strength of the restoration","default":0.75}},"description":"Restores faces in an image.","output":{"$ref":"#/components/schemas/ImageOutput"}},"ShowImageInvocation":{"title":"ShowImageInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["show_image"],"type":"string","default":"show_image"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The image to show"}},"description":"Displays a provided image, and passes it forward in the pipeline.","output":{"$ref":"#/components/schemas/ImageOutput"}},"TextToImageInvocation":{"title":"TextToImageInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["txt2img"],"type":"string","default":"txt2img"},"prompt":{"title":"Prompt","type":"string","description":"The prompt to generate an image from"},"seed":{"title":"Seed","maximum":4294967295.0,"minimum":-1.0,"type":"integer","description":"The seed to use (-1 for a random seed)","default":-1},"steps":{"title":"Steps","exclusiveMinimum":0.0,"type":"integer","description":"The number of steps to use to generate the image","default":10},"width":{"title":"Width","multipleOf":64.0,"exclusiveMinimum":0.0,"type":"integer","description":"The width of the resulting image","default":512},"height":{"title":"Height","multipleOf":64.0,"exclusiveMinimum":0.0,"type":"integer","description":"The height of the resulting image","default":512},"cfg_scale":{"title":"Cfg Scale","exclusiveMinimum":0.0,"type":"number","description":"The Classifier-Free Guidance, higher values may result in a result closer to the prompt","default":7.5},"sampler_name":{"title":"Sampler Name","enum":["ddim","dpmpp_2","k_dpm_2","k_dpm_2_a","k_dpmpp_2","k_euler","k_euler_a","k_heun","k_lms","plms"],"type":"string","description":"The sampler to use","default":"k_lms"},"seamless":{"title":"Seamless","type":"boolean","description":"Whether or not to generate an image that can tile without seams","default":false},"model":{"title":"Model","type":"string","description":"The model to use (currently ignored)","default":""},"progress_images":{"title":"Progress Images","type":"boolean","description":"Whether or not to produce progress images during generation","default":false}},"description":"Generates an image using text2img.","output":{"$ref":"#/components/schemas/ImageOutput"}},"UpscaleInvocation":{"title":"UpscaleInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["upscale"],"type":"string","default":"upscale"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The input image"},"strength":{"title":"Strength","maximum":1.0,"exclusiveMinimum":0.0,"type":"number","description":"The strength","default":0.75},"level":{"title":"Level","enum":[2,4],"type":"integer","description":"The upscale level","default":2}},"description":"Upscales an image.","output":{"$ref":"#/components/schemas/ImageOutput"}},"ValidationError":{"title":"ValidationError","required":["loc","msg","type"],"type":"object","properties":{"loc":{"title":"Location","type":"array","items":{"anyOf":[{"type":"string"},{"type":"integer"}]}},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}}}}}} \ No newline at end of file diff --git a/invokeai/frontend/web/src/services/fixtures/request.ts b/invokeai/frontend/web/src/services/fixtures/request.ts new file mode 100644 index 0000000000..745f687743 --- /dev/null +++ b/invokeai/frontend/web/src/services/fixtures/request.ts @@ -0,0 +1,351 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Custom `request.ts` file for OpenAPI code generator. + * + * Patches the request logic in such a way that we can extract headers from requests. + * + * Copied from https://github.com/ferdikoomen/openapi-typescript-codegen/issues/829#issuecomment-1228224477 + * + * This file should be excluded in `tsconfig.json` and ignored by prettier/eslint! + */ + +import axios from 'axios'; +import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; +import FormData from 'form-data'; + +import { ApiError } from './ApiError'; +import type { ApiRequestOptions } from './ApiRequestOptions'; +import type { ApiResult } from './ApiResult'; +import { CancelablePromise } from './CancelablePromise'; +import type { OnCancel } from './CancelablePromise'; +import type { OpenAPIConfig } from './OpenAPI'; + +export const HEADERS = Symbol('HEADERS'); + +const isDefined = ( + value: T | null | undefined +): value is Exclude => { + return value !== undefined && value !== null; +}; + +const isString = (value: any): value is string => { + return typeof value === 'string'; +}; + +const isStringWithValue = (value: any): value is string => { + return isString(value) && value !== ''; +}; + +const isBlob = (value: any): value is Blob => { + return ( + typeof value === 'object' && + typeof value.type === 'string' && + typeof value.stream === 'function' && + typeof value.arrayBuffer === 'function' && + typeof value.constructor === 'function' && + typeof value.constructor.name === 'string' && + /^(Blob|File)$/.test(value.constructor.name) && + /^(Blob|File)$/.test(value[Symbol.toStringTag]) + ); +}; + +const isFormData = (value: any): value is FormData => { + return value instanceof FormData; +}; + +const isSuccess = (status: number): boolean => { + return status >= 200 && status < 300; +}; + +const base64 = (str: string): string => { + try { + return btoa(str); + } catch (err) { + // @ts-ignore + return Buffer.from(str).toString('base64'); + } +}; + +const getQueryString = (params: Record): string => { + const qs: string[] = []; + + const append = (key: string, value: any) => { + qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); + }; + + const process = (key: string, value: any) => { + if (isDefined(value)) { + if (Array.isArray(value)) { + value.forEach((v) => { + process(key, v); + }); + } else if (typeof value === 'object') { + Object.entries(value).forEach(([k, v]) => { + process(`${key}[${k}]`, v); + }); + } else { + append(key, value); + } + } + }; + + Object.entries(params).forEach(([key, value]) => { + process(key, value); + }); + + if (qs.length > 0) { + return `?${qs.join('&')}`; + } + + return ''; +}; + +const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => { + const encoder = config.ENCODE_PATH || encodeURI; + + const path = options.url + .replace('{api-version}', config.VERSION) + .replace(/{(.*?)}/g, (substring: string, group: string) => { + if (options.path?.hasOwnProperty(group)) { + return encoder(String(options.path[group])); + } + return substring; + }); + + const url = `${config.BASE}${path}`; + if (options.query) { + return `${url}${getQueryString(options.query)}`; + } + return url; +}; + +const getFormData = (options: ApiRequestOptions): FormData | undefined => { + if (options.formData) { + const formData = new FormData(); + + const process = (key: string, value: any) => { + if (isString(value) || isBlob(value)) { + formData.append(key, value); + } else { + formData.append(key, JSON.stringify(value)); + } + }; + + Object.entries(options.formData) + .filter(([_, value]) => isDefined(value)) + .forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach((v) => process(key, v)); + } else { + process(key, value); + } + }); + + return formData; + } + return undefined; +}; + +type Resolver = (options: ApiRequestOptions) => Promise; + +const resolve = async ( + options: ApiRequestOptions, + resolver?: T | Resolver +): Promise => { + if (typeof resolver === 'function') { + return (resolver as Resolver)(options); + } + return resolver; +}; + +const getHeaders = async ( + config: OpenAPIConfig, + options: ApiRequestOptions, + formData?: FormData +): Promise> => { + const token = await resolve(options, config.TOKEN); + const username = await resolve(options, config.USERNAME); + const password = await resolve(options, config.PASSWORD); + const additionalHeaders = await resolve(options, config.HEADERS); + const formHeaders = + (typeof formData?.getHeaders === 'function' && formData?.getHeaders()) || + {}; + + const headers = Object.entries({ + Accept: 'application/json', + ...additionalHeaders, + ...options.headers, + ...formHeaders, + }) + .filter(([_, value]) => isDefined(value)) + .reduce( + (headers, [key, value]) => ({ + ...headers, + [key]: String(value), + }), + {} as Record + ); + + if (isStringWithValue(token)) { + headers['Authorization'] = `Bearer ${token}`; + } + + if (isStringWithValue(username) && isStringWithValue(password)) { + const credentials = base64(`${username}:${password}`); + headers['Authorization'] = `Basic ${credentials}`; + } + + if (options.body) { + if (options.mediaType) { + headers['Content-Type'] = options.mediaType; + } else if (isBlob(options.body)) { + headers['Content-Type'] = options.body.type || 'application/octet-stream'; + } else if (isString(options.body)) { + headers['Content-Type'] = 'text/plain'; + } else if (!isFormData(options.body)) { + headers['Content-Type'] = 'application/json'; + } + } + + return headers; +}; + +const getRequestBody = (options: ApiRequestOptions): any => { + if (options.body) { + return options.body; + } + return undefined; +}; + +const sendRequest = async ( + config: OpenAPIConfig, + options: ApiRequestOptions, + url: string, + body: any, + formData: FormData | undefined, + headers: Record, + onCancel: OnCancel +): Promise> => { + const source = axios.CancelToken.source(); + + const requestConfig: AxiosRequestConfig = { + url, + headers, + data: body ?? formData, + method: options.method, + withCredentials: config.WITH_CREDENTIALS, + cancelToken: source.token, + }; + + onCancel(() => source.cancel('The user aborted a request.')); + + try { + return await axios.request(requestConfig); + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.response) { + return axiosError.response; + } + throw error; + } +}; + +const getResponseHeader = ( + response: AxiosResponse, + responseHeader?: string +): string | undefined => { + if (responseHeader) { + const content = response.headers[responseHeader]; + if (isString(content)) { + return content; + } + } + return undefined; +}; + +const getResponseBody = (response: AxiosResponse): any => { + if (response.status !== 204) { + return response.data; + } + return undefined; +}; + +const catchErrorCodes = ( + options: ApiRequestOptions, + result: ApiResult +): void => { + const errors: Record = { + 400: 'Bad Request', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 500: 'Internal Server Error', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + ...options.errors, + }; + + const error = errors[result.status]; + if (error) { + throw new ApiError(options, result, error); + } + + if (!result.ok) { + throw new ApiError(options, result, 'Generic Error'); + } +}; + +/** + * Request method + * @param config The OpenAPI configuration object + * @param options The request options from the service + * @returns CancelablePromise + * @throws ApiError + */ +export const request = ( + config: OpenAPIConfig, + options: ApiRequestOptions +): CancelablePromise => { + return new CancelablePromise(async (resolve, reject, onCancel) => { + try { + const url = getUrl(config, options); + const formData = getFormData(options); + const body = getRequestBody(options); + const headers = await getHeaders(config, options, formData); + + if (!onCancel.isCancelled) { + const response = await sendRequest( + config, + options, + url, + body, + formData, + headers, + onCancel + ); + const responseBody = getResponseBody(response); + const responseHeader = getResponseHeader( + response, + options.responseHeader + ); + + const result: ApiResult = { + url, + ok: isSuccess(response.status), + status: response.status, + statusText: response.statusText, + body: responseHeader ?? responseBody, + }; + + catchErrorCodes(options, result); + + resolve({ ...result.body, [HEADERS]: response.headers }); + } + } catch (error) { + reject(error); + } + }); +}; diff --git a/invokeai/frontend/web/src/services/thunks/gallery.ts b/invokeai/frontend/web/src/services/thunks/gallery.ts new file mode 100644 index 0000000000..3badee2549 --- /dev/null +++ b/invokeai/frontend/web/src/services/thunks/gallery.ts @@ -0,0 +1,30 @@ +import { createAppAsyncThunk } from 'app/storeUtils'; +import { ImagesService } from 'services/api'; + +export const IMAGES_PER_PAGE = 20; + +export const receivedResultImagesPage = createAppAsyncThunk( + 'results/receivedResultImagesPage', + async (_arg, { getState }) => { + const response = await ImagesService.listImages({ + imageType: 'results', + page: getState().results.nextPage, + perPage: IMAGES_PER_PAGE, + }); + + return response; + } +); + +export const receivedUploadImagesPage = createAppAsyncThunk( + 'uploads/receivedUploadImagesPage', + async (_arg, { getState }) => { + const response = await ImagesService.listImages({ + imageType: 'uploads', + page: getState().uploads.nextPage, + perPage: IMAGES_PER_PAGE, + }); + + return response; + } +); diff --git a/invokeai/frontend/web/src/services/thunks/image.ts b/invokeai/frontend/web/src/services/thunks/image.ts new file mode 100644 index 0000000000..7014925d87 --- /dev/null +++ b/invokeai/frontend/web/src/services/thunks/image.ts @@ -0,0 +1,36 @@ +import { isFulfilled } from '@reduxjs/toolkit'; +import { createAppAsyncThunk } from 'app/storeUtils'; +import { ImagesService } from 'services/api'; +import { getHeaders } from 'services/util/getHeaders'; + +type ImageReceivedArg = Parameters<(typeof ImagesService)['getImage']>[0]; + +/** + * `ImagesService.getImage()` thunk + */ +export const imageReceived = createAppAsyncThunk( + 'api/imageReceived', + async (arg: ImageReceivedArg, _thunkApi) => { + const response = await ImagesService.getImage(arg); + return response; + } +); + +type ImageUploadedArg = Parameters<(typeof ImagesService)['uploadImage']>[0]; + +/** + * `ImagesService.uploadImage()` thunk + */ +export const imageUploaded = createAppAsyncThunk( + 'api/imageUploaded', + async (arg: ImageUploadedArg, _thunkApi) => { + const response = await ImagesService.uploadImage(arg); + const { location } = getHeaders(response); + return { response, location }; + } +); + +/** + * Function to check if an action is a fulfilled `ImagesService.uploadImage()` thunk + */ +export const isFulfilledImageUploadedAction = isFulfilled(imageUploaded); diff --git a/invokeai/frontend/web/src/services/thunks/model.ts b/invokeai/frontend/web/src/services/thunks/model.ts new file mode 100644 index 0000000000..f5ee522593 --- /dev/null +++ b/invokeai/frontend/web/src/services/thunks/model.ts @@ -0,0 +1,24 @@ +import { createAppAsyncThunk } from 'app/storeUtils'; +import { Model } from 'features/system/store/modelSlice'; +import { reduce } from 'lodash'; +import { ModelsService } from 'services/api'; + +export const IMAGES_PER_PAGE = 20; + +export const receivedModels = createAppAsyncThunk( + 'models/receivedModels', + async (_arg) => { + const response = await ModelsService.listModels(); + const deserializedModels = reduce( + response.models, + (modelsAccumulator, model, modelName) => { + modelsAccumulator[modelName] = { ...model, name: modelName }; + + return modelsAccumulator; + }, + {} as Record + ); + + return deserializedModels; + } +); diff --git a/invokeai/frontend/web/src/services/thunks/schema.ts b/invokeai/frontend/web/src/services/thunks/schema.ts new file mode 100644 index 0000000000..edf237032f --- /dev/null +++ b/invokeai/frontend/web/src/services/thunks/schema.ts @@ -0,0 +1,14 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { OpenAPIV3 } from 'openapi-types'; + +export const receivedOpenAPISchema = createAsyncThunk( + 'nodes/receivedOpenAPISchema', + async () => { + const response = await fetch(`openapi.json`); + const jsonData = (await response.json()) as OpenAPIV3.Document; + + console.debug('OpenAPI schema: ', jsonData); + + return jsonData; + } +); diff --git a/invokeai/frontend/web/src/services/thunks/session.ts b/invokeai/frontend/web/src/services/thunks/session.ts new file mode 100644 index 0000000000..a1213ffcc2 --- /dev/null +++ b/invokeai/frontend/web/src/services/thunks/session.ts @@ -0,0 +1,132 @@ +import { createAppAsyncThunk } from 'app/storeUtils'; +import { SessionsService } from 'services/api'; +import { buildLinearGraph } from 'features/nodes/util/linearGraphBuilder/buildLinearGraph'; +import { isAnyOf, isFulfilled } from '@reduxjs/toolkit'; +import { buildNodesGraph } from 'features/nodes/util/nodesGraphBuilder/buildNodesGraph'; + +export const linearGraphBuilt = createAppAsyncThunk( + 'api/linearGraphBuilt', + async (_, { dispatch, getState }) => { + const graph = buildLinearGraph(getState()); + + dispatch(sessionCreated({ graph })); + + return graph; + } +); + +export const nodesGraphBuilt = createAppAsyncThunk( + 'api/nodesGraphBuilt', + async (_, { dispatch, getState }) => { + const graph = buildNodesGraph(getState()); + + dispatch(sessionCreated({ graph })); + + return graph; + } +); + +export const isFulfilledAnyGraphBuilt = isAnyOf( + linearGraphBuilt.fulfilled, + nodesGraphBuilt.fulfilled +); + +type SessionCreatedArg = { + graph: Parameters< + (typeof SessionsService)['createSession'] + >[0]['requestBody']; +}; + +/** + * `SessionsService.createSession()` thunk + */ +export const sessionCreated = createAppAsyncThunk( + 'api/sessionCreated', + async (arg: SessionCreatedArg, { dispatch, getState }) => { + console.log('Session created, graph: ', arg.graph); + + const response = await SessionsService.createSession({ + requestBody: arg.graph, + }); + + return response; + } +); + +/** + * Function to check if an action is a fulfilled `SessionsService.createSession()` thunk + */ +export const isFulfilledSessionCreatedAction = isFulfilled(sessionCreated); + +type NodeAddedArg = Parameters<(typeof SessionsService)['addNode']>[0]; + +/** + * `SessionsService.addNode()` thunk + */ +export const nodeAdded = createAppAsyncThunk( + 'api/nodeAdded', + async ( + arg: { node: NodeAddedArg['requestBody']; sessionId: string }, + _thunkApi + ) => { + const response = await SessionsService.addNode({ + requestBody: arg.node, + sessionId: arg.sessionId, + }); + + return response; + } +); + +/** + * `SessionsService.invokeSession()` thunk + */ +export const sessionInvoked = createAppAsyncThunk( + 'api/sessionInvoked', + async (arg: { sessionId: string }, _thunkApi) => { + const { sessionId } = arg; + + const response = await SessionsService.invokeSession({ + sessionId, + all: true, + }); + + return response; + } +); + +type SessionCanceledArg = Parameters< + (typeof SessionsService)['cancelSessionInvoke'] +>[0]; + +/** + * `SessionsService.cancelSession()` thunk + */ +export const sessionCanceled = createAppAsyncThunk( + 'api/sessionCanceled', + async (arg: SessionCanceledArg, _thunkApi) => { + const { sessionId } = arg; + + const response = await SessionsService.cancelSessionInvoke({ + sessionId, + }); + + return response; + } +); + +type SessionsListedArg = Parameters< + (typeof SessionsService)['listSessions'] +>[0]; + +/** + * `SessionsService.listSessions()` thunk + */ +export const listedSessions = createAppAsyncThunk( + 'api/listSessions', + async (arg: SessionsListedArg, _thunkApi) => { + const response = await SessionsService.listSessions(arg); + + return response; + } +); diff --git a/invokeai/frontend/web/src/services/types/guards.ts b/invokeai/frontend/web/src/services/types/guards.ts new file mode 100644 index 0000000000..5a9d891395 --- /dev/null +++ b/invokeai/frontend/web/src/services/types/guards.ts @@ -0,0 +1,33 @@ +import { + GraphExecutionState, + GraphInvocationOutput, + ImageOutput, + MaskOutput, + PromptOutput, + IterateInvocationOutput, + CollectInvocationOutput, +} from 'services/api'; + +export const isImageOutput = ( + output: GraphExecutionState['results'][string] +): output is ImageOutput => output.type === 'image'; + +export const isMaskOutput = ( + output: GraphExecutionState['results'][string] +): output is MaskOutput => output.type === 'mask'; + +export const isPromptOutput = ( + output: GraphExecutionState['results'][string] +): output is PromptOutput => output.type === 'prompt'; + +export const isGraphOutput = ( + output: GraphExecutionState['results'][string] +): output is GraphInvocationOutput => output.type === 'graph_output'; + +export const isIterateOutput = ( + output: GraphExecutionState['results'][string] +): output is IterateInvocationOutput => output.type === 'iterate_output'; + +export const isCollectOutput = ( + output: GraphExecutionState['results'][string] +): output is CollectInvocationOutput => output.type === 'collect_output'; diff --git a/invokeai/frontend/web/src/services/util/deserializeImageField.ts b/invokeai/frontend/web/src/services/util/deserializeImageField.ts new file mode 100644 index 0000000000..0d50a78e49 --- /dev/null +++ b/invokeai/frontend/web/src/services/util/deserializeImageField.ts @@ -0,0 +1,29 @@ +import { Image } from 'app/invokeai'; +import { ImageField, ImageType } from 'services/api'; +import { AnyInvocation } from 'services/events/types'; + +export const buildImageUrls = ( + imageType: ImageType, + imageName: string +): { url: string; thumbnail: string } => { + const url = `api/v1/images/${imageType}/${imageName}`; + + const thumbnail = `api/v1/images/${imageType}/thumbnails/${ + imageName.split('.')[0] + }.webp`; + + return { + url, + thumbnail, + }; +}; + +export const extractTimestampFromImageName = (imageName: string) => { + const timestamp = imageName.split('_')?.pop()?.split('.')[0]; + + if (timestamp === undefined) { + return 0; + } + + return Number(timestamp); +}; diff --git a/invokeai/frontend/web/src/services/util/deserializeImageResponse.ts b/invokeai/frontend/web/src/services/util/deserializeImageResponse.ts new file mode 100644 index 0000000000..ec90fb6793 --- /dev/null +++ b/invokeai/frontend/web/src/services/util/deserializeImageResponse.ts @@ -0,0 +1,29 @@ +import { Image } from 'app/invokeai'; +import { parseInvokeAIMetadata } from 'common/util/parseMetadata'; +import { ImageResponse } from 'services/api'; + +/** + * Process ImageReponse objects, which we get from the `list_images` endpoint. + */ +export const deserializeImageResponse = ( + imageResponse: ImageResponse +): Image => { + const { image_name, image_type, image_url, metadata, thumbnail_url } = + imageResponse; + + // TODO: parse metadata - just leaving it as-is for now + const { invokeai, ...rest } = metadata; + + const parsedMetadata = parseInvokeAIMetadata(invokeai); + + return { + name: image_name, + type: image_type, + url: image_url, + thumbnail: thumbnail_url, + metadata: { + ...rest, + ...(invokeai ? { invokeai: parsedMetadata } : {}), + }, + }; +}; diff --git a/invokeai/frontend/web/src/services/util/getHeaders.ts b/invokeai/frontend/web/src/services/util/getHeaders.ts new file mode 100644 index 0000000000..510ba35770 --- /dev/null +++ b/invokeai/frontend/web/src/services/util/getHeaders.ts @@ -0,0 +1,12 @@ +import { HEADERS } from '../api/core/request'; + +/** + * Returns the response headers of the response received by the generated API client. + */ +export const getHeaders = (response: any): Record => { + if (!(HEADERS in response)) { + throw new Error('Response does not have headers'); + } + + return response[HEADERS]; +}; diff --git a/invokeai/frontend/web/src/services/util/makeGraphOfXImages.ts b/invokeai/frontend/web/src/services/util/makeGraphOfXImages.ts new file mode 100644 index 0000000000..386ca972b1 --- /dev/null +++ b/invokeai/frontend/web/src/services/util/makeGraphOfXImages.ts @@ -0,0 +1,24 @@ +import { Graph, TextToImageInvocation } from '../api'; + +/** + * Make a graph of however many images + */ +export const makeGraphOfXImages = (numberOfImages: string) => + Array.from(Array(numberOfImages)) + .map( + (_val, i): TextToImageInvocation => ({ + id: i.toString(), + type: 'txt2img', + prompt: 'pizza', + steps: 50, + seed: 123, + sampler_name: 'ddim', + }) + ) + .reduce( + (acc, val: TextToImageInvocation) => { + acc.nodes![val.id] = val; + return acc; + }, + { nodes: {} } as Graph + ); diff --git a/invokeai/frontend/web/src/theme/components/progress.ts b/invokeai/frontend/web/src/theme/components/progress.ts index 8a66a6af7e..fa6b5b57c5 100644 --- a/invokeai/frontend/web/src/theme/components/progress.ts +++ b/invokeai/frontend/web/src/theme/components/progress.ts @@ -9,7 +9,9 @@ const { defineMultiStyleConfig, definePartsStyle } = const invokeAIFilledTrack = defineStyle((_props) => ({ bg: 'accent.600', - transition: 'width 0.2s ease-in-out', + // TODO: the animation is nice but looks weird bc it is substantially longer than each step + // so we get to 100% long before it finishes + // transition: 'width 0.2s ease-in-out', _indeterminate: { bgGradient: 'linear(to-r, transparent 0%, accent.600 50%, transparent 100%);', diff --git a/invokeai/frontend/web/src/theme/components/scrollbar.ts b/invokeai/frontend/web/src/theme/components/scrollbar.ts index 5128fb1cb8..6cca67f962 100644 --- a/invokeai/frontend/web/src/theme/components/scrollbar.ts +++ b/invokeai/frontend/web/src/theme/components/scrollbar.ts @@ -1,11 +1,13 @@ -export const no_scrollbar = { +import { ChakraProps } from '@chakra-ui/react'; + +export const no_scrollbar: ChakraProps['sx'] = { '::-webkit-scrollbar': { display: 'none', }, scrollbarWidth: 'none', }; -export const scrollbar = { +export const scrollbar: ChakraProps['sx'] = { scrollbarColor: 'accent.600 transparent', scrollbarWidth: 'thick', '::-webkit-scrollbar': { @@ -26,6 +28,6 @@ export const scrollbar = { borderColor: 'accent.500', }, '::-webkit-scrollbar-button': { - background: 'transaprent', + background: 'transparent', }, }; diff --git a/invokeai/frontend/web/src/theme/components/slider.ts b/invokeai/frontend/web/src/theme/components/slider.ts index adb874b781..ef3d84196e 100644 --- a/invokeai/frontend/web/src/theme/components/slider.ts +++ b/invokeai/frontend/web/src/theme/components/slider.ts @@ -37,6 +37,13 @@ const invokeAIMark = defineStyle((_props) => { }); const invokeAI = definePartsStyle((props) => ({ + container: { + _disabled: { + opacity: 0.6, + cursor: 'default', + pointerEvents: 'none', + }, + }, track: invokeAITrack(props), filledTrack: invokeAIFilledTrack(props), thumb: invokeAIThumb(props), diff --git a/invokeai/frontend/web/tests/metadata.ts b/invokeai/frontend/web/tests/metadata.ts new file mode 100644 index 0000000000..c694ce67c7 --- /dev/null +++ b/invokeai/frontend/web/tests/metadata.ts @@ -0,0 +1,174 @@ +export default {}; + +// python metadata parsing tests to rebuild + +// # def test_is_good_metadata_unchanged(): +// # parsed_metadata = metadata_service._parse_invokeai_metadata(valid_metadata) + +// # expected = deepcopy(valid_metadata) + +// # assert expected == parsed_metadata + +// # def test_can_parse_missing_session_id(): +// # metadata_missing_session_id = deepcopy(valid_metadata) +// # del metadata_missing_session_id["session_id"] + +// # expected = deepcopy(valid_metadata) +// # del expected["session_id"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_missing_session_id +// # ) +// # assert metadata_missing_session_id == parsed_metadata + +// # def test_can_parse_invalid_session_id(): +// # metadata_invalid_session_id = deepcopy(valid_metadata) +// # metadata_invalid_session_id["session_id"] = 123 + +// # expected = deepcopy(valid_metadata) +// # del expected["session_id"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_invalid_session_id +// # ) +// # assert expected == parsed_metadata + +// # def test_can_parse_missing_node(): +// # metadata_missing_node = deepcopy(valid_metadata) +// # del metadata_missing_node["node"] + +// # expected = deepcopy(valid_metadata) +// # del expected["node"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata(metadata_missing_node) +// # assert expected == parsed_metadata + +// # def test_can_parse_invalid_node(): +// # metadata_invalid_node = deepcopy(valid_metadata) +// # metadata_invalid_node["node"] = 123 + +// # expected = deepcopy(valid_metadata) +// # del expected["node"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata(metadata_invalid_node) +// # assert expected == parsed_metadata + +// # def test_can_parse_missing_node_id(): +// # metadata_missing_node_id = deepcopy(valid_metadata) +// # del metadata_missing_node_id["node"]["id"] + +// # expected = deepcopy(valid_metadata) +// # del expected["node"]["id"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_missing_node_id +// # ) +// # assert expected == parsed_metadata + +// # def test_can_parse_invalid_node_id(): +// # metadata_invalid_node_id = deepcopy(valid_metadata) +// # metadata_invalid_node_id["node"]["id"] = 123 + +// # expected = deepcopy(valid_metadata) +// # del expected["node"]["id"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_invalid_node_id +// # ) +// # assert expected == parsed_metadata + +// # def test_can_parse_missing_node_type(): +// # metadata_missing_node_type = deepcopy(valid_metadata) +// # del metadata_missing_node_type["node"]["type"] + +// # expected = deepcopy(valid_metadata) +// # del expected["node"]["type"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_missing_node_type +// # ) +// # assert expected == parsed_metadata + +// # def test_can_parse_invalid_node_type(): +// # metadata_invalid_node_type = deepcopy(valid_metadata) +// # metadata_invalid_node_type["node"]["type"] = 123 + +// # expected = deepcopy(valid_metadata) +// # del expected["node"]["type"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_invalid_node_type +// # ) +// # assert expected == parsed_metadata + +// # def test_can_parse_no_node_attrs(): +// # metadata_no_node_attrs = deepcopy(valid_metadata) +// # metadata_no_node_attrs["node"] = {} + +// # expected = deepcopy(valid_metadata) +// # del expected["node"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata(metadata_no_node_attrs) +// # assert expected == parsed_metadata + +// # def test_can_parse_array_attr(): +// # metadata_array_attr = deepcopy(valid_metadata) +// # metadata_array_attr["node"]["seed"] = [1, 2, 3] + +// # expected = deepcopy(valid_metadata) +// # del expected["node"]["seed"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata(metadata_array_attr) +// # assert expected == parsed_metadata + +// # def test_can_parse_invalid_dict_attr(): +// # metadata_invalid_dict_attr = deepcopy(valid_metadata) +// # metadata_invalid_dict_attr["node"]["seed"] = {"a": 1} + +// # expected = deepcopy(valid_metadata) +// # del expected["node"]["seed"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_invalid_dict_attr +// # ) +// # assert expected == parsed_metadata + +// # def test_can_parse_missing_image_field_image_type(): +// # metadata_missing_image_field_image_type = deepcopy(valid_metadata) +// # del metadata_missing_image_field_image_type["node"]["image"]["image_type"] + +// # expected = deepcopy(valid_metadata) +// # del expected["node"]["image"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_missing_image_field_image_type +// # ) +// # assert expected == parsed_metadata + +// # def test_can_parse_invalid_image_field_image_type(): +// # metadata_invalid_image_field_image_type = deepcopy(valid_metadata) +// # metadata_invalid_image_field_image_type["node"]["image"][ +// # "image_type" +// # ] = "bad image type" + +// # expected = deepcopy(valid_metadata) +// # del expected["node"]["image"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_invalid_image_field_image_type +// # ) +// # assert expected == parsed_metadata + +// # def test_can_parse_invalid_latents_field_latents_name(): +// # metadata_invalid_latents_field_latents_name = deepcopy(valid_metadata) +// # metadata_invalid_latents_field_latents_name["node"]["latents"] = { +// # "latents_name": 123 +// # } + +// # expected = deepcopy(valid_metadata) + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_invalid_latents_field_latents_name +// # ) + +// # assert expected == parsed_metadata diff --git a/invokeai/frontend/web/tsconfig.json b/invokeai/frontend/web/tsconfig.json index 2a75e16e88..9731a64d3d 100644 --- a/invokeai/frontend/web/tsconfig.json +++ b/invokeai/frontend/web/tsconfig.json @@ -18,5 +18,6 @@ "jsx": "react-jsx" }, "include": ["src", "index.d.ts"], + "exclude": ["src/services/fixtures/*"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/invokeai/frontend/web/vite.config.ts b/invokeai/frontend/web/vite.config.ts index a6071341b3..2128593f10 100644 --- a/invokeai/frontend/web/vite.config.ts +++ b/invokeai/frontend/web/vite.config.ts @@ -38,6 +38,23 @@ export default defineConfig(({ mode }) => { target: 'ws://127.0.0.1:9090', ws: true, }, + // Proxy socket.io to the nodes socketio server + '/ws/socket.io': { + target: 'ws://127.0.0.1:9090', + ws: true, + }, + // Proxy openapi schema definiton + '/openapi.json': { + target: 'http://127.0.0.1:9090/openapi.json', + rewrite: (path) => path.replace(/^\/openapi.json/, ''), + changeOrigin: true, + }, + // proxy nodes api + '/api/v1': { + target: 'http://127.0.0.1:9090/api/v1', + rewrite: (path) => path.replace(/^\/api\/v1/, ''), + changeOrigin: true, + }, }, }, build: { diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock index 1a40014efe..4c7edbffd7 100644 --- a/invokeai/frontend/web/yarn.lock +++ b/invokeai/frontend/web/yarn.lock @@ -2,6 +2,16 @@ # yarn lockfile v1 +"@apidevtools/json-schema-ref-parser@9.0.9": + version "9.0.9" + resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#d720f9256e3609621280584f2b47ae165359268b" + integrity sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w== + dependencies: + "@jsdevtools/ono" "^7.1.3" + "@types/json-schema" "^7.0.6" + call-me-maybe "^1.0.1" + js-yaml "^4.1.0" + "@babel/code-frame@^7.0.0": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" @@ -897,6 +907,11 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@dagrejs/graphlib@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@dagrejs/graphlib/-/graphlib-2.1.12.tgz#97d29eae006e4efcb68863505464e0e3f28fa5c7" + integrity sha512-yHk2G7ZNzDEHhQTlYtbtEy5PqlIoioCxZUKcrlBgubMvrLmewXqSV3v4rhc8RAt5s8lr8PcWbiovEPuORxe2KA== + "@emotion/babel-plugin@^11.10.6": version "11.10.6" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz#a68ee4b019d661d6f37dec4b8903255766925ead" @@ -1218,6 +1233,11 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@jsdevtools/ono@^7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" + integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1244,7 +1264,72 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== -"@reduxjs/toolkit@^1.9.2": +"@reactflow/background@11.2.0": + version "11.2.0" + resolved "https://registry.yarnpkg.com/@reactflow/background/-/background-11.2.0.tgz#2a6f89d4f4837d488629d32a2bd5f01708018115" + integrity sha512-Fd8Few2JsLuE/2GaIM6fkxEBaAJvfzi2Lc106HKi/ddX+dZs8NUsSwMsJy1Ajs8b4GbiX8v8axfKpbK6qFMV8w== + dependencies: + "@reactflow/core" "11.7.0" + classcat "^5.0.3" + zustand "^4.3.1" + +"@reactflow/controls@11.1.11": + version "11.1.11" + resolved "https://registry.yarnpkg.com/@reactflow/controls/-/controls-11.1.11.tgz#d58e1bd9ddc2ee83fbf96130a7c54f44ca068c09" + integrity sha512-g6WrsszhNkQjzkJ9HbVUBkGGoUy2z8dQVgH6CYQEjuoonD15cWAPGvjyg8vx8oGG7CuktUhWu5JPivL6qjECow== + dependencies: + "@reactflow/core" "11.7.0" + classcat "^5.0.3" + +"@reactflow/core@11.7.0", "@reactflow/core@^11.6.0": + version "11.7.0" + resolved "https://registry.yarnpkg.com/@reactflow/core/-/core-11.7.0.tgz#6d9bdc0b1de1c9251dd3651135450ab2d42c6562" + integrity sha512-UJcpbNRSupSSoMWh5UmRp6UUr0ug7xVKmMvadnkKKiNi9584q57nz4HMfkqwN3/ESbre7LD043yh2n678d/5FQ== + dependencies: + "@types/d3" "^7.4.0" + "@types/d3-drag" "^3.0.1" + "@types/d3-selection" "^3.0.3" + "@types/d3-zoom" "^3.0.1" + classcat "^5.0.3" + d3-drag "^3.0.0" + d3-selection "^3.0.0" + d3-zoom "^3.0.0" + zustand "^4.3.1" + +"@reactflow/minimap@11.5.0": + version "11.5.0" + resolved "https://registry.yarnpkg.com/@reactflow/minimap/-/minimap-11.5.0.tgz#ddce263a41c2e65dd2febc09c26e93764ce76bfc" + integrity sha512-n/3tlaknLpi3zaqCC+tDDPvUTOjd6jglto9V3RB1F2wlaUEbCwmuoR2GYTkiRyZMvuskKyAoQW8+0DX0+cWwsA== + dependencies: + "@reactflow/core" "11.7.0" + "@types/d3-selection" "^3.0.3" + "@types/d3-zoom" "^3.0.1" + classcat "^5.0.3" + d3-selection "^3.0.0" + d3-zoom "^3.0.0" + zustand "^4.3.1" + +"@reactflow/node-resizer@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@reactflow/node-resizer/-/node-resizer-2.1.0.tgz#7764211a7e00f873eab652937cffba8df7c02b6a" + integrity sha512-DVL8nnWsltP8/iANadAcTaDB4wsEkx2mOLlBEPNE3yc5loSm3u9l5m4enXRcBym61MiMuTtDPzZMyYYQUjuYIg== + dependencies: + "@reactflow/core" "^11.6.0" + classcat "^5.0.4" + d3-drag "^3.0.0" + d3-selection "^3.0.0" + zustand "^4.3.1" + +"@reactflow/node-toolbar@1.1.11": + version "1.1.11" + resolved "https://registry.yarnpkg.com/@reactflow/node-toolbar/-/node-toolbar-1.1.11.tgz#174b235d85de37cffba387af8f6fb315ec1d31d7" + integrity sha512-+hKtx+cvXwfCa9paGxE+G34rWRIIVEh68ZOqAtivClVmfqGzH/sEoGWtIOUyg9OEDNE1nEmZ1NrnpBGSmHHXFg== + dependencies: + "@reactflow/core" "11.7.0" + classcat "^5.0.3" + zustand "^4.3.1" + +"@reduxjs/toolkit@^1.9.3": version "1.9.3" resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.3.tgz#27e1a33072b5a312e4f7fa19247fec160bbb2df9" integrity sha512-GU2TNBQVofL09VGmuSioNPQIu6Ml0YLf4EJhgj0AvBadRlCGzUWet8372LjvO4fqKZF2vH1xU0htAa7BrK9pZg== @@ -1365,6 +1450,216 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== +"@types/d3-array@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.4.tgz#44eebe40be57476cad6a0cd6a85b0f57d54185a2" + integrity sha512-nwvEkG9vYOc0Ic7G7kwgviY4AQlTfYGIZ0fqB7CQHXGyYM6nO7kJh5EguSNA3jfh4rq7Sb7eMVq8isuvg2/miQ== + +"@types/d3-axis@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.2.tgz#96e11d51256baf5bdb2fa73a17d302993e79df07" + integrity sha512-uGC7DBh0TZrU/LY43Fd8Qr+2ja1FKmH07q2FoZFHo1eYl8aj87GhfVoY1saJVJiq24rp1+wpI6BvQJMKgQm8oA== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-brush@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.2.tgz#a610aad5a1e76c375be63e11c5eee1ed9fd2fb40" + integrity sha512-2TEm8KzUG3N7z0TrSKPmbxByBx54M+S9lHoP2J55QuLU0VSQ9mE96EJSAOVNEqd1bbynMjeTS9VHmz8/bSw8rA== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-chord@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.2.tgz#cf6f05ad2d8faaad524e9e6f454b4fd06b200930" + integrity sha512-abT/iLHD3sGZwqMTX1TYCMEulr+wBd0SzyOQnjYNLp7sngdOHYtNkMRI5v3w5thoN+BWtlHVDx2Osvq6fxhZWw== + +"@types/d3-color@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.0.tgz#6594da178ded6c7c3842f3cc0ac84b156f12f2d4" + integrity sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA== + +"@types/d3-contour@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.2.tgz#d8a0e4d12ec14f7d2bb6e59f3fbc1a527457d0b2" + integrity sha512-k6/bGDoAGJZnZWaKzeB+9glgXCYGvh6YlluxzBREiVo8f/X2vpTEdgPy9DN7Z2i42PZOZ4JDhVdlTSTSkLDPlQ== + dependencies: + "@types/d3-array" "*" + "@types/geojson" "*" + +"@types/d3-delaunay@*": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz#006b7bd838baec1511270cb900bf4fc377bbbf41" + integrity sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ== + +"@types/d3-dispatch@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.2.tgz#b2fa80bab3bcead68680766e966f59cd6cb9a69f" + integrity sha512-rxN6sHUXEZYCKV05MEh4z4WpPSqIw+aP7n9ZN6WYAAvZoEAghEK1WeVZMZcHRBwyaKflU43PCUAJNjFxCzPDjg== + +"@types/d3-drag@*", "@types/d3-drag@^3.0.1": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.2.tgz#5562da3e7b33d782c2c1f9e65c5e91bb01ee82cf" + integrity sha512-qmODKEDvyKWVHcWWCOVcuVcOwikLVsyc4q4EBJMREsoQnR2Qoc2cZQUyFUPgO9q4S3qdSqJKBsuefv+h0Qy+tw== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-dsv@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.1.tgz#c51a3505cee42653454b74a00f8713dc3548c362" + integrity sha512-76pBHCMTvPLt44wFOieouXcGXWOF0AJCceUvaFkxSZEu4VDUdv93JfpMa6VGNFs01FHfuP4a5Ou68eRG1KBfTw== + +"@types/d3-ease@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.0.tgz#c29926f8b596f9dadaeca062a32a45365681eae0" + integrity sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA== + +"@types/d3-fetch@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.2.tgz#fe1f335243e07c9bd520c9a71756fed8330c54b1" + integrity sha512-gllwYWozWfbep16N9fByNBDTkJW/SyhH6SGRlXloR7WdtAaBui4plTP+gbUgiEot7vGw/ZZop1yDZlgXXSuzjA== + dependencies: + "@types/d3-dsv" "*" + +"@types/d3-force@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.4.tgz#2d50bd2b695f709797e1745644f6bc123e6e5f5a" + integrity sha512-q7xbVLrWcXvSBBEoadowIUJ7sRpS1yvgMWnzHJggFy5cUZBq2HZL5k/pBSm0GdYWS1vs5/EDwMjSKF55PDY4Aw== + +"@types/d3-format@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.1.tgz#194f1317a499edd7e58766f96735bdc0216bb89d" + integrity sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg== + +"@types/d3-geo@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.0.3.tgz#535e5f24be13722964c52354301be09b752f5d6e" + integrity sha512-bK9uZJS3vuDCNeeXQ4z3u0E7OeJZXjUgzFdSOtNtMCJCLvDtWDwfpRVWlyt3y8EvRzI0ccOu9xlMVirawolSCw== + dependencies: + "@types/geojson" "*" + +"@types/d3-hierarchy@*": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b3a446b5437faededb30ac32b7cc0486559ab1e2" + integrity sha512-9hjRTVoZjRFR6xo8igAJyNXQyPX6Aq++Nhb5ebrUF414dv4jr2MitM2fWiOY475wa3Za7TOS2Gh9fmqEhLTt0A== + +"@types/d3-interpolate@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc" + integrity sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.0.0.tgz#939e3a784ae4f80b1fde8098b91af1776ff1312b" + integrity sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg== + +"@types/d3-polygon@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.0.tgz#5200a3fa793d7736fa104285fa19b0dbc2424b93" + integrity sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw== + +"@types/d3-quadtree@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz#433112a178eb7df123aab2ce11c67f51cafe8ff5" + integrity sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw== + +"@types/d3-random@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.1.tgz#5c8d42b36cd4c80b92e5626a252f994ca6bfc953" + integrity sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ== + +"@types/d3-scale-chromatic@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#103124777e8cdec85b20b51fd3397c682ee1e954" + integrity sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw== + +"@types/d3-scale@*": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.3.tgz#7a5780e934e52b6f63ad9c24b105e33dd58102b5" + integrity sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ== + dependencies: + "@types/d3-time" "*" + +"@types/d3-selection@*", "@types/d3-selection@^3.0.3": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.5.tgz#27cd53b7672d405025e2414d98532d7934c16ebd" + integrity sha512-xCB0z3Hi8eFIqyja3vW8iV01+OHGYR2di/+e+AiOcXIOrY82lcvWW8Ke1DYE/EUVMsBl4Db9RppSBS3X1U6J0w== + +"@types/d3-shape@*": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.1.tgz#15cc497751dac31192d7aef4e67a8d2c62354b95" + integrity sha512-6Uh86YFF7LGg4PQkuO2oG6EMBRLuW9cbavUW46zkIO5kuS2PfTqo2o9SkgtQzguBHbLgNnU90UNsITpsX1My+A== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time-format@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.0.tgz#ee7b6e798f8deb2d9640675f8811d0253aaa1946" + integrity sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw== + +"@types/d3-time@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.0.tgz#e1ac0f3e9e195135361fa1a1d62f795d87e6e819" + integrity sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg== + +"@types/d3-timer@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.0.tgz#e2505f1c21ec08bda8915238e397fb71d2fc54ce" + integrity sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g== + +"@types/d3-transition@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.3.tgz#d4ac37d08703fb039c87f92851a598ba77400402" + integrity sha512-/S90Od8Id1wgQNvIA8iFv9jRhCiZcGhPd2qX0bKF/PS+y0W5CrXKgIiELd2CvG1mlQrWK/qlYh3VxicqG1ZvgA== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-zoom@*", "@types/d3-zoom@^3.0.1": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.2.tgz#067aa6a6ecbc75a78b753cc6f7a7f9f7e4e7d117" + integrity sha512-t09DDJVBI6AkM7N8kuPsnq/3d/ehtRKBN1xSiYjjMCgbiw6HM6Ged5VhvswmhprfKyGvzeTEL/4WBaK9llWvlA== + dependencies: + "@types/d3-interpolate" "*" + "@types/d3-selection" "*" + +"@types/d3@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.0.tgz#fc5cac5b1756fc592a3cf1f3dc881bf08225f515" + integrity sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA== + dependencies: + "@types/d3-array" "*" + "@types/d3-axis" "*" + "@types/d3-brush" "*" + "@types/d3-chord" "*" + "@types/d3-color" "*" + "@types/d3-contour" "*" + "@types/d3-delaunay" "*" + "@types/d3-dispatch" "*" + "@types/d3-drag" "*" + "@types/d3-dsv" "*" + "@types/d3-ease" "*" + "@types/d3-fetch" "*" + "@types/d3-force" "*" + "@types/d3-format" "*" + "@types/d3-geo" "*" + "@types/d3-hierarchy" "*" + "@types/d3-interpolate" "*" + "@types/d3-path" "*" + "@types/d3-polygon" "*" + "@types/d3-quadtree" "*" + "@types/d3-random" "*" + "@types/d3-scale" "*" + "@types/d3-scale-chromatic" "*" + "@types/d3-selection" "*" + "@types/d3-shape" "*" + "@types/d3-time" "*" + "@types/d3-time-format" "*" + "@types/d3-timer" "*" + "@types/d3-transition" "*" + "@types/d3-zoom" "*" + "@types/dateformat@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@types/dateformat/-/dateformat-5.0.0.tgz#17ce64b0318f3f36d1c830c58a7a915445f1f93d" @@ -1383,6 +1678,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== +"@types/geojson@*": + version "7946.0.10" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249" + integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA== + "@types/hoist-non-react-statics@^3.3.1": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" @@ -1391,7 +1691,7 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" -"@types/json-schema@*", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== @@ -1413,6 +1713,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== +"@types/lodash@^4.14.194": + version "4.14.194" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.194.tgz#b71eb6f7a0ff11bff59fc987134a093029258a76" + integrity sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g== + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -1768,6 +2073,11 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + at-least-node@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" @@ -1783,6 +2093,15 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +axios@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.4.tgz#f5760cefd9cfb51fd2481acf88c05f67c4523024" + integrity sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-plugin-macros@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" @@ -1887,12 +2206,17 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-me-maybe@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa" + integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase@^6.2.0: +camelcase@^6.2.0, camelcase@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== @@ -1947,6 +2271,11 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== +classcat@^5.0.3, classcat@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/classcat/-/classcat-5.0.4.tgz#e12d1dfe6df6427f260f03b80dc63571a5107ba6" + integrity sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g== + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -2077,6 +2406,13 @@ colorette@^2.0.19: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + commander@^2.16.0, commander@^2.20.0, commander@^2.20.3, commander@^2.8.1: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -2206,6 +2542,68 @@ csstype@^3.0.11, csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== +"d3-color@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + +"d3-dispatch@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" + integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== + +"d3-drag@2 - 3", d3-drag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" + integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== + dependencies: + d3-dispatch "1 - 3" + d3-selection "3" + +"d3-ease@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +"d3-interpolate@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" + integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== + +"d3-timer@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + +"d3-transition@2 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" + integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== + dependencies: + d3-color "1 - 3" + d3-dispatch "1 - 3" + d3-ease "1 - 3" + d3-interpolate "1 - 3" + d3-timer "1 - 3" + +d3-zoom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" + integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "2 - 3" + d3-transition "2 - 3" + date-fns@^2.29.1: version "2.29.3" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" @@ -2270,6 +2668,11 @@ define-properties@^1.1.3, define-properties@^1.1.4: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + dependency-tree@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/dependency-tree/-/dependency-tree-9.0.0.tgz#9288dd6daf35f6510c1ea30d9894b75369aa50a2" @@ -2952,6 +3355,11 @@ focus-lock@^0.11.6: dependencies: tslib "^2.0.3" +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -2959,6 +3367,15 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + formik@^2.2.9: version "2.2.9" resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.9.tgz#8594ba9c5e2e5cf1f42c5704128e119fc46232d0" @@ -2988,6 +3405,15 @@ framesync@6.1.2: dependencies: tslib "2.4.0" +fs-extra@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^9.0.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" @@ -3200,6 +3626,18 @@ grapheme-splitter@^1.0.4: resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +handlebars@^4.7.7: + version "4.7.7" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.0" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -3681,6 +4119,13 @@ json-parse-even-better-errors@^2.3.0: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-schema-ref-parser@^9.0.9: + version "9.0.9" + resolved "https://registry.yarnpkg.com/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#66ea538e7450b12af342fa3d5b8458bc1e1e013f" + integrity sha512-qcP2lmGy+JUoQJ4DOQeLaZDqH9qSkeGCK3suKWxJXS82dg728Mn3j97azDMaOUmJAN4uCq91LdPx4K7E8F1a7Q== + dependencies: + "@apidevtools/json-schema-ref-parser" "9.0.9" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -3945,6 +4390,18 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -4019,6 +4476,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +neo-async@^2.6.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -4160,6 +4622,22 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +openapi-types@^12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.0.tgz#bd01acc937b73c9f6db2ac2031bf0231e21ebff0" + integrity sha512-XpeCy01X6L5EpP+6Hc3jWN7rMZJ+/k1lwki/kTmWzbVhdPie3jd5O2ZtedEx8Yp58icJ0osVldLMrTB/zslQXA== + +openapi-typescript-codegen@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/openapi-typescript-codegen/-/openapi-typescript-codegen-0.23.0.tgz#702a651eefc536b27e87e4ad54a80a31d36487f0" + integrity sha512-gOJXy5g3H3HlLpVNN+USrNK2i2KYBmDczk9Xk34u6JorwrGiDJZUj+al4S+i9TXdfUQ/ZaLxE59Xf3wqkxGfqA== + dependencies: + camelcase "^6.3.0" + commander "^9.3.0" + fs-extra "^10.1.0" + handlebars "^4.7.7" + json-schema-ref-parser "^9.0.9" + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -4449,6 +4927,11 @@ prop-types@^15.6.2, prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -4661,6 +5144,18 @@ react@^18.2.0: dependencies: loose-envify "^1.1.0" +reactflow@^11.7.0: + version "11.7.0" + resolved "https://registry.yarnpkg.com/reactflow/-/reactflow-11.7.0.tgz#821642ce9ce4a3a2fa6469053520cb032ff03ef4" + integrity sha512-bjfJV1iQZ+VwIlvsnd4TbXNs6kuJ5ONscud6fNkF8RY/oU2VUZpdjA3q1zwmgnjmJcIhxuBiBI5VLGajYx/Ozg== + dependencies: + "@reactflow/background" "11.2.0" + "@reactflow/controls" "11.1.11" + "@reactflow/core" "11.7.0" + "@reactflow/minimap" "11.5.0" + "@reactflow/node-resizer" "2.1.0" + "@reactflow/node-toolbar" "1.1.11" + readable-stream@^3.4.0: version "3.6.1" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.1.tgz#f9f9b5f536920253b3d26e7660e7da4ccff9bb62" @@ -4682,6 +5177,11 @@ redux-deep-persist@^1.0.7: resolved "https://registry.yarnpkg.com/redux-deep-persist/-/redux-deep-persist-1.0.7.tgz#fbbd00dcee6111b42624c9d8590b7e124b94476e" integrity sha512-PsD5UXbfCFvDruIPIHKAyaZ3wPhEWBMU8Rtcr/c1pXJT8aYoKbgKUS8JBkaWc3EB1ONlnLTdDDmnC/TOD39hqA== +redux-dynamic-middlewares@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/redux-dynamic-middlewares/-/redux-dynamic-middlewares-2.2.0.tgz#6835dd6d4f2fd975266376b45dcae0141320ae97" + integrity sha512-GHESQC+Y0PV98ZBoaC6br6cDOsNiM1Cu4UleGMqMWCXX03jIr3BoozYVrRkLVVAl4sC216chakMnZOu6SwNdGA== + redux-persist@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8" @@ -5027,7 +5527,7 @@ source-map@^0.5.7: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== -source-map@^0.6.0, source-map@~0.6.1: +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -5377,15 +5877,20 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typescript@4.9.5, typescript@^4.0.0, typescript@^4.5.5: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== + typescript@^3.9.10, typescript@^3.9.5, typescript@^3.9.7: version "3.9.10" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== -typescript@^4.0.0, typescript@^4.5.5: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +uglify-js@^3.1.4: + version "3.17.4" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" + integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== unbox-primitive@^1.0.2: version "1.0.2" @@ -5468,7 +5973,7 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" -use-sync-external-store@^1.0.0: +use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== @@ -5597,6 +6102,11 @@ word-wrap@^1.2.3, word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== + wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" @@ -5697,3 +6207,10 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zustand@^4.3.1: + version "4.3.7" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.7.tgz#501b1f0393a7f1d103332e45ab574be5747fedce" + integrity sha512-dY8ERwB9Nd21ellgkBZFhudER8KVlelZm8388B5nDAXhO/+FZDhYMuRnqDgu5SYyRgz/iaf8RKnbUs/cHfOGlQ== + dependencies: + use-sync-external-store "1.2.0" diff --git a/tests/nodes/test_graph_execution_state.py b/tests/nodes/test_graph_execution_state.py index f65129797e..2476786e41 100644 --- a/tests/nodes/test_graph_execution_state.py +++ b/tests/nodes/test_graph_execution_state.py @@ -27,6 +27,7 @@ def mock_services(): events = None, # type: ignore images = None, # type: ignore latents = None, # type: ignore + metadata = None, # type: ignore queue = MemoryInvocationQueue(), graph_library=SqliteItemStorage[LibraryGraph]( filename=sqlite_memory, table_name="graphs" diff --git a/tests/nodes/test_invoker.py b/tests/nodes/test_invoker.py index 46d532b9f7..d187c1b171 100644 --- a/tests/nodes/test_invoker.py +++ b/tests/nodes/test_invoker.py @@ -25,6 +25,7 @@ def mock_services() -> InvocationServices: events = TestEventService(), images = None, # type: ignore latents = None, # type: ignore + metadata = None, # type: ignore queue = MemoryInvocationQueue(), graph_library=SqliteItemStorage[LibraryGraph]( filename=sqlite_memory, table_name="graphs" diff --git a/tests/nodes/test_nodes.py b/tests/nodes/test_nodes.py index d16d67d815..e334953d7e 100644 --- a/tests/nodes/test_nodes.py +++ b/tests/nodes/test_nodes.py @@ -49,7 +49,7 @@ class ImageTestInvocation(BaseInvocation): prompt: str = Field(default = "") def invoke(self, context: InvocationContext) -> ImageTestInvocationOutput: - return ImageTestInvocationOutput(image=ImageField(image_name=self.id)) + return ImageTestInvocationOutput(image=ImageField(image_name=self.id, width=512, height=512, mode="", info={})) class PromptCollectionTestInvocationOutput(BaseInvocationOutput): type: Literal['test_prompt_collection_output'] = 'test_prompt_collection_output' diff --git a/tests/nodes/test_png_metadata_service.py b/tests/nodes/test_png_metadata_service.py new file mode 100644 index 0000000000..3075af5a4b --- /dev/null +++ b/tests/nodes/test_png_metadata_service.py @@ -0,0 +1,55 @@ +import json +import os + +from PIL import Image, PngImagePlugin + +from invokeai.app.invocations.generate import TextToImageInvocation +from invokeai.app.services.metadata import PngMetadataService + +valid_metadata = { + "session_id": "1", + "node": { + "id": "1", + "type": "txt2img", + "prompt": "dog", + "seed": 178785523, + "steps": 30, + "width": 512, + "height": 512, + "cfg_scale": 7.5, + "scheduler": "k_lms", + "seamless": False, + "model": "stable-diffusion-1.5", + "progress_images": True, + }, +} + +metadata_service = PngMetadataService() + + +def test_can_load_and_parse_invokeai_metadata(tmp_path): + raw_metadata = {"session_id": "123", "node": {"id": "456", "type": "test_type"}} + + temp_image = Image.new("RGB", (512, 512)) + temp_image_path = os.path.join(tmp_path, "test.png") + + pnginfo = PngImagePlugin.PngInfo() + pnginfo.add_text("invokeai", json.dumps(raw_metadata)) + + temp_image.save(temp_image_path, pnginfo=pnginfo) + + image = Image.open(temp_image_path) + + loaded_metadata = metadata_service.get_metadata(image) + + assert loaded_metadata is not None + assert raw_metadata == loaded_metadata + + +def test_can_build_invokeai_metadata(): + session_id = valid_metadata["session_id"] + node = TextToImageInvocation(**valid_metadata["node"]) + + metadata = metadata_service.build_metadata(session_id=session_id, node=node) + + assert valid_metadata == metadata