Compare commits

...

80 Commits

Author SHA1 Message Date
ca56238e0a hardcode the thing 2023-04-05 15:13:50 -04:00
96f48db7df add logging HELP 2023-04-05 14:57:19 -04:00
e2a5271612 add logging HELP 2023-04-05 14:52:43 -04:00
49f88046fa add logging HELP 2023-04-05 14:48:02 -04:00
7e0813c87e add logging HELP 2023-04-05 14:43:49 -04:00
4ba9b710c2 temp force nodes sockets 2023-04-05 14:37:06 -04:00
22f497388b feat(ui) working on making socket URL dynamic 2023-04-05 14:31:54 -04:00
964abde220 feat(ui): export StatusIndicator and ModelSelect for header use 2023-04-05 14:28:25 -04:00
e69a65b304 feat(ui): add optional token for auth 2023-04-05 13:08:39 -04:00
f6c6f61da6 feat(ui): wip events, comments, and general refactoring 2023-04-06 00:29:56 +10:00
500bdfa7dd lang(ui): add toast strings 2023-04-06 00:29:56 +10:00
d6104a3cea feat(nodes): add script to generate openapi.json 2023-04-06 00:29:56 +10:00
591d6bcdda docs(ui): organise and update docs 2023-04-06 00:29:56 +10:00
8f12ec659c Maryhipp/disable panels (#3116) 2023-04-05 07:22:22 -07:00
1a756e7f14 feat(ui): add support to disableTabs 2023-04-05 10:20:37 -04:00
abc10a115d Merge remote-tracking branch 'origin/main' into feat/ui/nodes-api 2023-04-05 18:05:23 +10:00
760b4b938c feat(ui): wip refactor socket events 2023-04-05 18:01:32 +10:00
4e2358cb09 fix(nodes): fix DiskImageStorage.get() bug 2023-04-05 17:59:21 +10:00
0e0ffb39ff chore(ui): regenerate api 2023-04-05 16:30:00 +10:00
a35dc090c5 feat(nodes): wip ImageResponse 2023-04-05 14:00:43 +10:00
42182b744c disable panels when app mounts 2023-04-04 15:38:43 -04:00
46aeeea29a feat(ui): invert logic to be disabled 2023-04-04 14:23:20 -04:00
9820829edb feat(ui): disable panels based on app props 2023-04-04 14:12:19 -04:00
cc3401a159 feat(ui): wip gallery migration 2023-04-04 22:58:46 +10:00
cfe86ec541 feat(ui): wip gallery migration 2023-04-04 18:34:50 +10:00
b7de3162c3 chore(ui): regenerate api 2023-04-04 18:30:52 +10:00
de0df4945d fix(nodes): fix off-by-one page count error 2023-04-04 16:38:37 +10:00
406039426a fix(nodes): fix image_type in ImageField 2023-04-04 15:46:34 +10:00
daf1bc6b67 feat(nodes): generalise list_images route for all types 2023-04-04 15:24:44 +10:00
4f3be53d55 feat(nodes): sort images returned by disk list() 2023-04-04 14:49:11 +10:00
1628262ca8 feat(ui): patch api generation for headers access 2023-04-04 14:28:07 +10:00
ed7fe23436 feat(nodes): add uuid to uploaded images filename 2023-04-04 14:24:32 +10:00
a065f7db56 feat(nodes): add datatypes module 2023-04-04 13:32:34 +10:00
77bf3c780f feat(nodes): list_uploads route wip 2023-04-04 11:05:15 +10:00
ed00afc64d fix(ui): restore removed type 2023-04-03 14:48:20 -04:00
e2114a1da5 feat(ui): POST upload working 2023-04-03 14:32:43 -04:00
3722f055fb fix(ui): separate thunk for initial gallery load so it properly gets index 0 2023-04-03 12:54:10 -04:00
69c71c83e6 Merge branch 'main' into feat/ui/nodes-api 2023-04-04 00:51:28 +10:00
dbf6b1b68a feat(ui): clean up & comment results slice 2023-04-04 00:31:17 +10:00
9baa8f7a6a fix(nodes): fix typo in list_sessions handler 2023-04-04 00:18:11 +10:00
7ca32ce9f3 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.
2023-04-03 19:05:33 +10:00
4fe7e52111 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.
2023-04-03 19:04:09 +10:00
7ff50796e5 chore(ui): regenerate api client 2023-04-03 18:56:59 +10:00
5a9157e628 feat(nodes): make ImageField properties required 2023-04-03 18:56:48 +10:00
d1058adb59 feat(nodes): save thumbnails 2023-04-03 14:34:07 +10:00
3c99abab32 fix(nodes): commit changes to db 2023-04-03 12:02:58 +10:00
7ed1772fad docs(ui): update readme 2023-04-03 11:04:24 +10:00
fd031b6c2a chore(ui): bump redux-toolkit 2023-04-02 21:29:22 +10:00
272803ba7c feat(ui): load images on socket connect
Rudimentary
2023-04-02 21:28:58 +10:00
8d8284afaa feat(ui): add type guards for outputs 2023-04-02 21:28:03 +10:00
01607c961f feat(ui): make thunk types more consistent 2023-04-02 21:27:46 +10:00
30b61ae8d4 feat(ui): fix parameters panel border color
This commit should be elsewhere but I don't want to break my flow
2023-04-02 21:26:59 +10:00
1890bffc6e feat(ui): disable NodeAPITest
This was polluting the network/socket logs.
2023-04-02 21:25:55 +10:00
662f1321f6 feat(ui): add rtk action type guard 2023-04-02 19:48:29 +10:00
4ac447cf74 fix(ui): fix middleware types 2023-04-02 19:48:03 +10:00
f9c243d29f feat(ui): handle random seeds 2023-04-02 18:41:54 +10:00
eda334bc34 feat(ui): add nodes mode script 2023-04-02 17:48:12 +10:00
f2a2f326ce chore(ui): add support for package mode 2023-04-01 14:50:56 +11:00
7b10762ea1 feat(ui): get intermediate images working but types are stubbed out 2023-04-01 14:50:56 +11:00
2db2c986d1 feat(ui): img2img implementation 2023-04-01 14:50:56 +11:00
40b2d2b05b feat(ui): write separate nodes socket layer, txt2img generating and rendering w single node 2023-04-01 14:50:56 +11:00
4fe49718e0 feat(ui): start hooking up dynamic txt2img node generation, create middleware for session invocation 2023-04-01 14:50:56 +11:00
3ebd289a59 add optional apiUrl prop 2023-04-01 14:50:56 +11:00
1bd246e9a9 use reference to sampler_name 2023-04-01 14:50:56 +11:00
e75141150f use reference to sampler_name 2023-04-01 14:50:56 +11:00
660e665cf3 start building out node translations from frontend state and add notes about missing features 2023-04-01 14:50:56 +11:00
ca41a52174 feat(ui): wip nodes
- extract api client method arg types instead of manually declaring them
- update example to display images
- general tidy up
2023-04-01 14:50:56 +11:00
999c3a443b feat(ui): add socketio types 2023-04-01 14:50:56 +11:00
21fb41ef56 fix(ui): fix scrollbar styles typing and prop
just noticed the typo, and made the types stronger.
2023-04-01 14:50:56 +11:00
4f99b005b1 fix(ui): disable OG web server socket connection 2023-04-01 14:50:56 +11:00
d86588ec76 chore(ui): regenerate api client 2023-04-01 14:50:56 +11:00
6c1f666242 feat(ui): nodes cancel 2023-04-01 14:50:56 +11:00
07428769df feat(ui): more nodes api prototyping 2023-04-01 14:50:56 +11:00
b49338b464 feat(ui): generate object args for api client 2023-04-01 14:50:56 +11:00
92996898f2 feat(backend): fixes for nodes/generator 2023-04-01 14:50:56 +11:00
d66d844dd2 chore(ui): update openapi.json 2023-04-01 14:50:56 +11:00
d46d52ca63 chore(ui): update .eslintignore, .prettierignore 2023-04-01 14:50:56 +11:00
427104f936 chore(ui): organize generated files 2023-04-01 14:50:56 +11:00
16442e8f15 fix(ui): update client & nodes test code w/ new Edge type 2023-04-01 14:50:56 +11:00
1c7d92dc48 feat(ui): add axios client generator and simple example 2023-04-01 14:50:56 +11:00
169 changed files with 5804 additions and 328 deletions

View File

@ -1,18 +1,20 @@
# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654)
import io
from datetime import datetime, timezone
import uuid
from fastapi import Path, Request, UploadFile
from fastapi import Path, Query, Request, UploadFile
from fastapi.responses import FileResponse, Response
from fastapi.routing import APIRouter
from PIL import Image
from invokeai.app.datatypes.image import ImageResponse
from invokeai.app.services.item_storage import PaginatedResults
from ...services.image_storage import ImageType
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"),
@ -48,19 +50,35 @@ async def upload_image(file: UploadFile, request: Request):
contents = await file.read()
try:
im = Image.open(contents)
im = Image.open(io.BytesIO(contents))
except:
# Error opening the image
return Response(status_code=415)
filename = f"{str(int(datetime.now(timezone.utc).timestamp()))}.png"
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, image_name=filename
"get_image", image_type=ImageType.UPLOAD.value, image_name=filename
)
},
)
@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"),
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
)
return result

View File

@ -5,7 +5,8 @@ import argparse
from typing import Any, Callable, Iterable, Literal, get_args, get_origin, get_type_hints
from pydantic import BaseModel, Field
from ..invocations.image import ImageField
from invokeai.app.datatypes.image import ImageField
from ..services.graph import GraphExecutionState
from ..services.invoker import Invoker

View File

View File

@ -0,0 +1,3 @@
class CanceledException(Exception):
"""Execution canceled by user."""
pass

View File

@ -0,0 +1,38 @@
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field
from invokeai.app.datatypes.metadata import ImageMetadata
class ImageType(str, Enum):
RESULT = "results"
INTERMEDIATE = "intermediates"
UPLOAD = "uploads"
class ImageField(BaseModel):
"""An image field used for passing image objects between invocations"""
image_type: ImageType = Field(
default=ImageType.RESULT, description="The type of the image"
)
image_name: Optional[str] = Field(default=None, description="The name of the image")
class Config:
schema_extra = {
"required": [
"image_type",
"image_name",
]
}
class ImageResponse(BaseModel):
"""The response type for images"""
image_type: ImageType = Field(description="The type of the image")
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")

View File

@ -0,0 +1,11 @@
from typing import Optional
from pydantic import BaseModel, Field
class ImageMetadata(BaseModel):
"""An image's metadata"""
timestamp: 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 width of the image in pixels")
# TODO: figure out metadata
sd_metadata: Optional[dict] = Field(default={}, description="The image's SD-specific metadata")

View File

@ -7,9 +7,9 @@ import numpy
from PIL import Image, ImageOps
from pydantic import Field
from ..services.image_storage import ImageType
from invokeai.app.datatypes.image import ImageField, ImageType
from .baseinvocation import BaseInvocation, InvocationContext
from .image import ImageField, ImageOutput
from .image import ImageOutput
class CvInpaintInvocation(BaseInvocation):

View File

@ -8,12 +8,13 @@ from torch import Tensor
from pydantic import Field
from ..services.image_storage import ImageType
from invokeai.app.datatypes.image import ImageField, ImageType
from .baseinvocation import BaseInvocation, InvocationContext
from .image import ImageField, ImageOutput
from .image import ImageOutput
from ...backend.generator import Txt2Img, Img2Img, Inpaint, InvokeAIGenerator
from ...backend.stable_diffusion import PipelineIntermediateState
from ..util.util import diffusers_step_callback_adapter, CanceledException
from ..datatypes.exceptions import CanceledException
from ..util.step_callback import diffusers_step_callback_adapter
SAMPLER_NAME_VALUES = Literal[
tuple(InvokeAIGenerator.schedulers())

View File

@ -7,20 +7,10 @@ import numpy
from PIL import Image, ImageFilter, ImageOps
from pydantic import BaseModel, Field
from ..services.image_storage import ImageType
from invokeai.app.datatypes.image import ImageField, ImageType
from ..services.invocation_services import InvocationServices
from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationContext
class ImageField(BaseModel):
"""An image field used for passing image objects between invocations"""
image_type: str = Field(
default=ImageType.RESULT, description="The type of the image"
)
image_name: Optional[str] = Field(default=None, description="The name of the image")
class ImageOutput(BaseInvocationOutput):
"""Base class for invocations that output an image"""
#fmt: off

View File

@ -3,10 +3,10 @@ from typing import Literal, Union
from pydantic import Field
from ..services.image_storage import ImageType
from invokeai.app.datatypes.image import ImageField, ImageType
from ..services.invocation_services import InvocationServices
from .baseinvocation import BaseInvocation, InvocationContext
from .image import ImageField, ImageOutput
from .image import ImageOutput
class RestoreFaceInvocation(BaseInvocation):
"""Restores faces in an image."""

View File

@ -5,10 +5,10 @@ from typing import Literal, Union
from pydantic import Field
from ..services.image_storage import ImageType
from invokeai.app.datatypes.image import ImageField, ImageType
from ..services.invocation_services import InvocationServices
from .baseinvocation import BaseInvocation, InvocationContext
from .image import ImageField, ImageOutput
from .image import ImageOutput
class UpscaleInvocation(BaseInvocation):

View File

@ -2,24 +2,24 @@
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 Dict
from typing import Callable, Dict, List
from PIL.Image import Image
import PIL.Image as PILImage
from pydantic import BaseModel
from invokeai.app.datatypes.image import ImageField, ImageResponse, ImageType
from invokeai.app.datatypes.metadata import ImageMetadata
from invokeai.app.services.item_storage import PaginatedResults
from invokeai.app.util.save_thumbnail import save_thumbnail
from invokeai.backend.image_util import PngWriter
class ImageType(str, Enum):
RESULT = "results"
INTERMEDIATE = "intermediates"
UPLOAD = "uploads"
class ImageStorageBase(ABC):
"""Responsible for storing and retrieving images."""
@ -27,9 +27,17 @@ class ImageStorageBase(ABC):
def get(self, image_type: ImageType, image_name: str) -> Image:
pass
@abstractmethod
def list(
self, image_type: ImageType, page: int = 0, per_page: int = 10
) -> PaginatedResults[ImageResponse]:
pass
# TODO: make this a bit more flexible for e.g. cloud storage
@abstractmethod
def get_path(self, image_type: ImageType, image_name: str) -> str:
def get_path(
self, image_type: ImageType, image_name: str, is_thumbnail: bool = False
) -> str:
pass
@abstractmethod
@ -71,19 +79,75 @@ class DiskImageStorage(ImageStorageBase):
parents=True, exist_ok=True
)
def list(
self, image_type: ImageType, page: int = 0, per_page: int = 10
) -> PaginatedResults[ImageResponse]:
dir_path = os.path.join(self.__output_folder, image_type)
image_paths = glob(f"{dir_path}/*.png")
count = len(image_paths)
# TODO: do all platforms support `getmtime`? seem to recall some do not...
sorted_image_paths = sorted(
glob(f"{dir_path}/*.png"), key=os.path.getmtime, reverse=True
)
page_of_image_paths = sorted_image_paths[
page * per_page : (page + 1) * per_page
]
page_of_images: List[ImageResponse] = []
for path in page_of_image_paths:
filename = os.path.basename(path)
img = PILImage.open(path)
page_of_images.append(
ImageResponse(
image_type=image_type.value,
image_name=os.path.basename(path),
# 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=int(os.path.splitext(filename)[0].split("_")[-1]),
width=img.width,
height=img.height,
),
)
)
page_count_trunc = int(count / per_page)
page_count_mod = count % per_page
page_count = page_count_trunc if page_count_mod == 0 else page_count_trunc + 1
return PaginatedResults[ImageResponse](
items=page_of_images,
page=page,
pages=page_count,
per_page=per_page,
total=count,
)
def get(self, image_type: ImageType, image_name: str) -> Image:
image_path = self.get_path(image_type, image_name)
cache_item = self.__get_cache(image_path)
if cache_item:
return cache_item
image = Image.open(image_path)
image = PILImage.open(image_path)
self.__set_cache(image_path, image)
return image
# TODO: make this a bit more flexible for e.g. cloud storage
def get_path(self, image_type: ImageType, image_name: str) -> str:
path = os.path.join(self.__output_folder, image_type, image_name)
def get_path(
self, image_type: ImageType, image_name: str, is_thumbnail: bool = False
) -> str:
if is_thumbnail:
path = os.path.join(
self.__output_folder, image_type, "thumbnails", image_name
)
else:
path = os.path.join(self.__output_folder, image_type, image_name)
return path
def save(self, image_type: ImageType, image_name: str, image: Image) -> None:
@ -101,12 +165,19 @@ class DiskImageStorage(ImageStorageBase):
def delete(self, image_type: ImageType, image_name: str) -> None:
image_path = self.get_path(image_type, image_name)
thumbnail_path = self.get_path(image_type, image_name, True)
if os.path.exists(image_path):
os.remove(image_path)
if image_path in self.__cache:
del self.__cache[image_path]
if os.path.exists(thumbnail_path):
os.remove(thumbnail_path)
if thumbnail_path in self.__cache:
del self.__cache[thumbnail_path]
def __get_cache(self, image_name: str) -> Image:
return None if image_name not in self.__cache else self.__cache[image_name]

View File

@ -4,7 +4,7 @@ from threading import Event, Thread
from ..invocations.baseinvocation import InvocationContext
from .invocation_queue import InvocationQueueItem
from .invoker import InvocationProcessorABC, Invoker
from ..util.util import CanceledException
from ..datatypes.exceptions import CanceledException
class DefaultInvocationProcessor(InvocationProcessorABC):
__invoker_thread: Thread

View File

@ -106,10 +106,12 @@ class SqliteItemStorage(ItemStorageABC, Generic[T]):
finally:
self._lock.release()
pageCount = int(count / per_page) + 1
page_count_trunc = int(count / per_page)
page_count_mod = count % per_page
page_count = page_count_trunc if page_count_mod == 0 else page_count_trunc + 1
return PaginatedResults[T](
items=items, page=page, pages=pageCount, per_page=per_page, total=count
items=items, page=page, pages=page_count, per_page=per_page, total=count
)
def search(

View File

View File

@ -0,0 +1,15 @@
# Generate the OpenAPI schema json
import json
from invokeai.app.api_app import app
from fastapi.openapi.utils import get_openapi
openapi_doc = get_openapi(
title=app.title,
version=app.version,
openapi_version=app.openapi_version,
routes=app.routes,
)
with open("./openapi.json", "w") as f:
json.dump(openapi_doc, f)

View File

@ -1,14 +1,16 @@
import torch
from PIL import Image
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
class CanceledException(Exception):
pass
def fast_latents_step_callback(sample: torch.Tensor, step: int, steps: int, id: str, context: InvocationContext, ):
def fast_latents_step_callback(
sample: torch.Tensor,
step: int,
steps: int,
id: str,
context: InvocationContext,
):
# TODO: only output a preview image when requested
image = Generator.sample_to_lowres_estimated_image(sample)
@ -21,15 +23,12 @@ def fast_latents_step_callback(sample: torch.Tensor, step: int, steps: int, id:
context.services.events.emit_generator_progress(
context.graph_execution_state_id,
id,
{
"width": width,
"height": height,
"dataURL": dataURL
},
{"width": width, "height": height, "dataURL": dataURL},
step,
steps,
)
def diffusers_step_callback_adapter(*cb_args, **kwargs):
"""
txt2img gives us a Tensor in the step_callbak, while img2img gives us a PipelineIntermediateState.
@ -37,6 +36,8 @@ def diffusers_step_callback_adapter(*cb_args, **kwargs):
"""
if isinstance(cb_args[0], PipelineIntermediateState):
progress_state: PipelineIntermediateState = cb_args[0]
return fast_latents_step_callback(progress_state.latents, progress_state.step, **kwargs)
return fast_latents_step_callback(
progress_state.latents, progress_state.step, **kwargs
)
else:
return fast_latents_step_callback(*cb_args, **kwargs)

View File

@ -6,3 +6,5 @@ stats.html
index.html
.yarn/
*.scss
src/services/api/
src/services/fixtures/*

View File

@ -3,4 +3,8 @@ dist/
node_modules/
patches/
stats.html
index.html
.yarn/
*.scss
src/services/api/
src/services/fixtures/*

View File

@ -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`.

View File

@ -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.

View File

@ -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: <https://github.com/chakra-ui/chakra-ui/issues/7394>
### 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.

View File

@ -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. <http://localhost:5173/>
### Production builds

View File

@ -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,24 @@ declare module '@invoke-ai/invoke-ai-ui' {
declare class SettingsModal extends React.Component<SettingsModalProps> {
public constructor(props: SettingsModalProps);
}
declare class StatusIndicator extends React.Component<StatusIndicatorProps> {
public constructor(props: StatusIndicatorProps);
}
declare class ModelSelect extends React.Component<ModelSelectProps> {
public constructor(props: ModelSelectProps);
}
}
declare function Invoke(props: PropsWithChildren): JSX.Element;
interface InvokeProps extends PropsWithChildren {
apiUrl?: string;
disabledPanels?: string[];
disabledTabs?: InvokeTabName[];
token?: string;
}
declare function Invoke(props: InvokeProps): JSX.Element;
export {
ThemeChanger,
@ -74,5 +90,7 @@ export {
IAIPopover,
IAIIconButton,
SettingsModal,
StatusIndicator,
ModelSelect,
};
export = Invoke;

View File

@ -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 .",
@ -43,7 +46,7 @@
"@chakra-ui/theme-tools": "^2.0.16",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@reduxjs/toolkit": "^1.9.2",
"@reduxjs/toolkit": "^1.9.3",
"chakra-ui-contextmenu": "^1.0.5",
"dateformat": "^5.0.3",
"formik": "^2.2.9",
@ -83,6 +86,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 +94,16 @@
"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-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",

View File

@ -522,6 +522,9 @@
"resetComplete": "Web UI has been reset. Refresh the page to reload."
},
"toast": {
"serverError": "Server Error",
"disconnected": "Disconnected from Server",
"connected": "Connected to Server",
"tempFoldersEmptied": "Temp Folder Emptied",
"uploadFailed": "Upload failed",
"uploadFailedMultipleImagesDesc": "Multiple images pasted, may only upload one image at a time",

View File

@ -13,16 +13,34 @@ 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';
keepGUIAlive();
const App = (props: PropsWithChildren) => {
interface Props extends PropsWithChildren {
options: {
disabledPanels: string[];
disabledTabs: InvokeTabName[];
};
}
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(() => {
setColorMode(['light'].includes(currentTheme) ? 'light' : 'dark');

View File

@ -14,6 +14,7 @@
import { InvokeTabName } from 'features/ui/store/tabMap';
import { IRect } from 'konva/lib/types';
import { ImageMetadata, ImageType } from 'services/api';
/**
* TODO:
@ -113,7 +114,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 +125,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<Image>;
images: Array<_Image>;
};
/**
@ -275,7 +288,7 @@ export declare type SystemStatusResponse = SystemStatus;
export declare type SystemConfigResponse = SystemConfig;
export declare type ImageResultResponse = Omit<Image, 'uuid'> & {
export declare type ImageResultResponse = Omit<_Image, 'uuid'> & {
boundingBox?: IRect;
generationMode: InvokeTabName;
};
@ -296,7 +309,7 @@ export declare type ErrorResponse = {
};
export declare type GalleryImagesResponse = {
images: Array<Omit<Image, 'uuid'>>;
images: Array<Omit<_Image, 'uuid'>>;
areMoreImagesAvailable: boolean;
category: GalleryCategory;
};

View File

@ -13,9 +13,13 @@ import { InvokeTabName } from 'features/ui/store/tabMap';
export const generateImage = createAction<InvokeTabName>(
'socketio/generateImage'
);
export const runESRGAN = createAction<InvokeAI.Image>('socketio/runESRGAN');
export const runFacetool = createAction<InvokeAI.Image>('socketio/runFacetool');
export const deleteImage = createAction<InvokeAI.Image>('socketio/deleteImage');
export const runESRGAN = createAction<InvokeAI._Image>('socketio/runESRGAN');
export const runFacetool = createAction<InvokeAI._Image>(
'socketio/runFacetool'
);
export const deleteImage = createAction<InvokeAI._Image>(
'socketio/deleteImage'
);
export const requestImages = createAction<GalleryCategory>(
'socketio/requestImages'
);

View File

@ -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);

View File

@ -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';
@ -146,7 +147,8 @@ const makeSocketIOListeners = (
const activeTabName = tabMap[activeTab];
switch (activeTabName) {
case 'img2img': {
dispatch(setInitialImage(newImage));
dispatch(initialImageSelected(newImage.uuid));
// dispatch(setInitialImage(newImage));
break;
}
}
@ -262,7 +264,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 +336,7 @@ const makeSocketIOListeners = (
if (
initialImage === url ||
(initialImage as InvokeAI.Image)?.url === url
(initialImage as InvokeAI._Image)?.url === url
) {
dispatch(clearInitialImage());
}

View File

@ -29,6 +29,8 @@ export const socketioMiddleware = () => {
path: `${window.location.pathname}socket.io`,
});
socketio.disconnect();
let areListenersSet = false;
const middleware: Middleware = (store) => (next) => (action) => {

View File

@ -7,6 +7,8 @@ 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';
@ -14,6 +16,7 @@ import systemReducer from 'features/system/store/systemSlice';
import uiReducer from 'features/ui/store/uiSlice';
import { socketioMiddleware } from './socketio/middleware';
import { socketMiddleware } from 'services/events/middleware';
/**
* redux-persist provides an easy and reliable way to persist state across reloads.
@ -64,6 +67,10 @@ const lightboxBlacklist = ['isLightboxOpen'].map(
(blacklistItem) => `lightbox.${blacklistItem}`
);
const apiBlacklist = ['sessionId', 'status', 'progress', 'progressImage'].map(
(blacklistItem) => `api.${blacklistItem}`
);
const rootReducer = combineReducers({
generation: generationReducer,
postprocessing: postprocessingReducer,
@ -72,6 +79,8 @@ const rootReducer = combineReducers({
canvas: canvasReducer,
ui: uiReducer,
lightbox: lightboxReducer,
results: resultsReducer,
uploads: uploadsReducer,
});
const rootPersistConfig = getPersistConfig({
@ -83,12 +92,24 @@ const rootPersistConfig = getPersistConfig({
...systemBlacklist,
...galleryBlacklist,
...lightboxBlacklist,
...apiBlacklist,
// for now, never persist the results/uploads slices
'results',
'uploads',
],
debounce: 300,
});
const persistedReducer = persistReducer(rootPersistConfig, rootReducer);
// function buildMiddleware() {
// if (import.meta.env.MODE === 'nodes' || import.meta.env.MODE === 'package') {
// return [socketMiddleware()];
// } else {
// return [socketioMiddleware()];
// }
// }
// Continue with store setup
export const store = configureStore({
reducer: persistedReducer,
@ -96,7 +117,7 @@ export const store = configureStore({
getDefaultMiddleware({
immutableCheck: false,
serializableCheck: false,
}).concat(socketioMiddleware()),
}).concat(socketMiddleware()),
devTools: {
// Uncommenting these very rapidly called actions makes the redux dev tools output much more readable
actionsDenylist: [

View File

@ -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;
}>();

View File

@ -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 { uploadImage } 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(uploadImage({ formData: { file } }));
},
[dispatch]
);
@ -124,7 +124,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
return;
}
dispatch(uploadImage({ imageFile: file }));
dispatch(uploadImage({ formData: { file } }));
};
document.addEventListener('paste', pasteImageListener);
return () => {

View File

@ -14,6 +14,7 @@ const WorkInProgress = (props: WorkInProgressProps) => {
width: '100%',
height: '100%',
bg: 'base.850',
borderRadius: 'base',
}}
>
{children}

View File

@ -0,0 +1,34 @@
import { v4 as uuidv4 } from 'uuid';
import { RootState } from 'app/store';
import { InvokeTabName, tabMap } from 'features/ui/store/tabMap';
import { Graph } from 'services/api';
import { buildImg2ImgNode, buildTxt2ImgNode } from './buildNodes';
function mapTabToFunction(activeTabName: InvokeTabName) {
switch (activeTabName) {
case 'txt2img':
return buildTxt2ImgNode;
case 'img2img':
return buildImg2ImgNode;
default:
return buildTxt2ImgNode;
}
}
export const buildGraph = (state: RootState): Graph => {
const { activeTab } = state.ui;
const activeTabName = tabMap[activeTab];
const nodeId = uuidv4();
return {
nodes: {
[nodeId]: {
id: nodeId,
...mapTabToFunction(activeTabName)(state),
},
},
};
};

View File

@ -0,0 +1,145 @@
import { RootState } from 'app/store';
import {
ImageToImageInvocation,
RestoreFaceInvocation,
TextToImageInvocation,
UpscaleInvocation,
} from 'services/api';
import { _Image } from 'app/invokeai';
import { initialImageSelector } from 'features/parameters/store/generationSelectors';
// fe todo fix model type (frontend uses null, backend uses undefined)
// fe todo update front end to store to have whole image field (vs just name)
// be todo add symmetry fields
// be todo variations....
export function buildTxt2ImgNode(
state: RootState
): Omit<TextToImageInvocation, 'id'> {
const { generation, system } = state;
const { shouldDisplayInProgressType, model } = system;
const {
prompt,
seed,
steps,
width,
height,
cfgScale: cfg_scale,
sampler,
seamless,
shouldRandomizeSeed,
} = generation;
// missing fields in TextToImageInvocation: strength, hires_fix
return {
type: 'txt2img',
prompt,
seed: shouldRandomizeSeed ? -1 : seed,
steps,
width,
height,
cfg_scale,
sampler_name: sampler as TextToImageInvocation['sampler_name'],
seamless,
model,
progress_images: shouldDisplayInProgressType === 'full-res',
};
}
export function buildImg2ImgNode(
state: RootState
): Omit<ImageToImageInvocation, 'id'> {
const { generation, system } = state;
const { shouldDisplayInProgressType, model } = system;
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';
}
return {
type: 'img2img',
prompt,
seed: shouldRandomizeSeed ? -1 : seed,
steps,
width,
height,
cfg_scale: cfgScale,
sampler_name: sampler as ImageToImageInvocation['sampler_name'],
seamless,
model,
progress_images: shouldDisplayInProgressType === 'full-res',
image: {
image_name: initialImage.name,
image_type: 'results',
},
strength,
fit,
};
}
export function buildFacetoolNode(
state: RootState
): Omit<RestoreFaceInvocation, 'id'> {
const { generation, postprocessing } = state;
const { initialImage } = generation;
const { facetoolStrength: strength } = postprocessing;
// missing fields in RestoreFaceInvocation: type, codeformer_fidelity
return {
type: 'restore_face',
image: {
image_name:
(typeof initialImage === 'string' ? initialImage : initialImage?.url) ||
'',
image_type: 'results',
},
strength,
};
}
// is this ESRGAN??
export function buildUpscaleNode(
state: RootState
): Omit<UpscaleInvocation, 'id'> {
const { generation, postprocessing } = state;
const { initialImage } = generation;
const { upscalingLevel: level, upscalingStrength: strength } = postprocessing;
// missing fields in UpscaleInvocation: denoise_str
return {
type: 'upscale',
image: {
image_name:
(typeof initialImage === 'string' ? initialImage : initialImage?.url) ||
'',
image_type: 'results',
},
strength,
level,
};
}

View File

@ -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');

View File

@ -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 { 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';
@ -21,18 +23,45 @@ import './i18n';
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;
}
export default function Component({
apiUrl,
disabledPanels = [],
disabledTabs = [],
token,
children,
}: Props) {
const [ready, setReady] = useState(false);
useEffect(() => {
console.log('setting OPENAPI.BASE to', apiUrl);
if (apiUrl) OpenAPI.BASE = apiUrl;
setReady(true);
}, [apiUrl]);
useEffect(() => {
if (token) OpenAPI.TOKEN = token;
}, [token]);
return (
<React.StrictMode>
<Provider store={store}>
<PersistGate loading={<Loading />} persistor={persistor}>
<React.Suspense fallback={<Loading showText />}>
<ThemeLocaleProvider>
<App>{props.children}</App>
</ThemeLocaleProvider>
</React.Suspense>
</PersistGate>
</Provider>
{ready && (
<Provider store={store}>
<PersistGate loading={<Loading />} persistor={persistor}>
<React.Suspense fallback={<Loading showText />}>
<ThemeLocaleProvider>
<App options={{ disabledPanels, disabledTabs }}>{children}</App>
</ThemeLocaleProvider>
</React.Suspense>
</PersistGate>
</Provider>
)}
</React.StrictMode>
);
}

View File

@ -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,
};

View File

@ -156,7 +156,7 @@ export const canvasSlice = createSlice({
setCursorPosition: (state, action: PayloadAction<Vector2d | null>) => {
state.cursorPosition = action.payload;
},
setInitialCanvasImage: (state, action: PayloadAction<InvokeAI.Image>) => {
setInitialCanvasImage: (state, action: PayloadAction<InvokeAI._Image>) => {
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;

View File

@ -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;

View File

@ -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,

View File

@ -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';
@ -129,8 +130,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
const handleClickUseAsInitialImage = () => {
if (!currentImage) return;
if (isLightboxOpen) dispatch(setIsLightboxOpen(false));
dispatch(setInitialImage(currentImage));
dispatch(setActiveTab('img2img'));
dispatch(initialImageSelected(currentImage.uuid));
// dispatch(setInitialImage(currentImage));
// dispatch(setActiveTab('img2img'));
};
const handleCopyImage = async () => {

View File

@ -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,
};
},
{

View File

@ -1,26 +1,44 @@
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 { systemSelector } from 'features/system/store/systemSelectors';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash';
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';
export const imagesSelector = createSelector(
[gallerySelector, uiSelector],
(gallery: GalleryState, ui) => {
const { currentImage, intermediateImage } = gallery;
[uiSelector, selectedImageSelector, systemSelector],
(ui, selectedImage, system) => {
const { shouldShowImageDetails } = 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,
imageToDisplay,
};
},
{
@ -31,9 +49,9 @@ export const imagesSelector = createSelector(
);
export default function CurrentImagePreview() {
const { shouldShowImageDetails, imageToDisplay, isIntermediate } =
const { shouldShowImageDetails, imageToDisplay } =
useAppSelector(imagesSelector);
console.log(imageToDisplay);
return (
<Flex
sx={{
@ -49,34 +67,42 @@ export default function CurrentImagePreview() {
src={imageToDisplay.url}
width={imageToDisplay.width}
height={imageToDisplay.height}
fallback={!isIntermediate ? <CurrentImageFallback /> : undefined}
fallback={
!imageToDisplay.isProgressImage ? (
<CurrentImageFallback />
) : undefined
}
sx={{
objectFit: 'contain',
maxWidth: '100%',
maxHeight: '100%',
height: 'auto',
position: 'absolute',
imageRendering: isIntermediate ? 'pixelated' : 'initial',
imageRendering: imageToDisplay.isProgressImage
? 'pixelated'
: 'initial',
borderRadius: 'base',
}}
/>
)}
{!shouldShowImageDetails && <NextPrevImageButtons />}
{shouldShowImageDetails && imageToDisplay && (
<Box
sx={{
position: 'absolute',
top: '0',
width: '100%',
height: '100%',
borderRadius: 'base',
overflow: 'scroll',
maxHeight: APP_METADATA_HEIGHT,
}}
>
<ImageMetadataViewer image={imageToDisplay} />
</Box>
)}
{shouldShowImageDetails &&
imageToDisplay &&
'metadata' in imageToDisplay.image && (
<Box
sx={{
position: 'absolute',
top: '0',
width: '100%',
height: '100%',
borderRadius: 'base',
overflow: 'scroll',
maxHeight: APP_METADATA_HEIGHT,
}}
>
<ImageMetadataViewer image={imageToDisplay.image} />
</Box>
)}
</Flex>
);
}

View File

@ -52,7 +52,7 @@ interface DeleteImageModalProps {
/**
* The image to delete.
*/
image?: InvokeAI.Image;
image?: InvokeAI._Image;
}
/**

View File

@ -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';
@ -40,7 +43,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 +58,7 @@ 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 [isHovered, setIsHovered] = useState<boolean>(false);
@ -69,10 +72,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 +84,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 +95,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 +116,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 +126,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 +151,18 @@ const HoverableImage = memo((props: HoverableImageProps) => {
});
};
const handleSelectImage = () => dispatch(setCurrentImage(image));
const handleSelectImage = () => {
dispatch(imageSelected(image.name));
};
const handleDragStart = (e: DragEvent<HTMLDivElement>) => {
e.dataTransfer.setData('invokeai/imageUuid', uuid);
e.dataTransfer.effectAllowed = 'move';
// e.dataTransfer.setData('invokeai/imageUuid', uuid);
// e.dataTransfer.effectAllowed = 'move';
};
const handleLightBox = () => {
dispatch(setCurrentImage(image));
dispatch(setIsLightboxOpen(true));
// dispatch(setCurrentImage(image));
// dispatch(setIsLightboxOpen(true));
};
return (
@ -177,28 +175,30 @@ const HoverableImage = memo((props: HoverableImageProps) => {
</MenuItem>
<MenuItem
onClickCapture={handleUsePrompt}
isDisabled={image?.metadata?.image?.prompt === undefined}
isDisabled={image?.metadata?.sd_metadata?.prompt === undefined}
>
{t('parameters.usePrompt')}
</MenuItem>
<MenuItem
onClickCapture={handleUseSeed}
isDisabled={image?.metadata?.image?.seed === undefined}
isDisabled={image?.metadata?.sd_metadata?.seed === undefined}
>
{t('parameters.useSeed')}
</MenuItem>
<MenuItem
onClickCapture={handleUseAllParameters}
isDisabled={
!['txt2img', 'img2img'].includes(image?.metadata?.image?.type)
!['txt2img', 'img2img'].includes(
image?.metadata?.sd_metadata?.type
)
}
>
{t('parameters.useAll')}
</MenuItem>
<MenuItem
onClickCapture={handleUseInitialImage}
isDisabled={image?.metadata?.image?.type !== 'img2img'}
isDisabled={image?.metadata?.sd_metadata?.type !== 'img2img'}
>
{t('parameters.useInitImg')}
</MenuItem>
@ -209,9 +209,9 @@ const HoverableImage = memo((props: HoverableImageProps) => {
{t('parameters.sendToUnifiedCanvas')}
</MenuItem>
<MenuItem data-warning>
<DeleteImageModal image={image}>
{/* <DeleteImageModal image={image}>
<p>{t('parameters.deleteImage')}</p>
</DeleteImageModal>
</DeleteImageModal> */}
</MenuItem>
</MenuList>
)}
@ -219,7 +219,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
{(ref) => (
<Box
position="relative"
key={uuid}
key={name}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
userSelect="none"
@ -290,7 +290,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
insetInlineEnd: 1,
}}
>
<DeleteImageModal image={image}>
{/* <DeleteImageModal image={image}>
<IAIIconButton
aria-label={t('parameters.deleteImage')}
icon={<FaTrashAlt />}
@ -298,7 +298,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
fontSize={14}
isDisabled={!mayDeleteImage}
/>
</DeleteImageModal>
</DeleteImageModal> */}
</Box>
)}
</Box>

View File

@ -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 (
<HoverableImage
key={uuid}
key={name}
image={image}
isSelected={isSelected}
/>
@ -217,6 +264,7 @@ const ImageGalleryContent = () => {
<IAIButton
onClick={handleClickLoadMore}
isDisabled={!areMoreImagesAvailable}
isLoading={isLoading}
flexShrink={0}
>
{areMoreImagesAvailable

View File

@ -18,7 +18,7 @@ import {
setCfgScale,
setHeight,
setImg2imgStrength,
setInitialImage,
// setInitialImage,
setMaskPath,
setPerlin,
setSampler,
@ -120,7 +120,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,8 +137,8 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
dispatch(setShouldShowImageDetails(false));
});
const metadata = image?.metadata?.image || {};
const dreamPrompt = image?.dreamPrompt;
const metadata = image?.metadata.sd_metadata || {};
const dreamPrompt = image?.metadata.sd_metadata?.dreamPrompt;
const {
cfg_scale,
@ -160,6 +160,7 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
type,
variations,
width,
model_weights,
} = metadata;
const { t } = useTranslation();
@ -193,8 +194,8 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
{Object.keys(metadata).length > 0 ? (
<>
{type && <MetadataItem label="Generation type" value={type} />}
{image.metadata?.model_weights && (
<MetadataItem label="Model" value={image.metadata.model_weights} />
{model_weights && (
<MetadataItem label="Model" value={model_weights} />
)}
{['esrgan', 'gfpgan'].includes(type) && (
<MetadataItem label="Original image" value={orig_path} />
@ -288,14 +289,14 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
onClick={() => dispatch(setHeight(height))}
/>
)}
{init_image_path && (
{/* {init_image_path && (
<MetadataItem
label="Initial image"
value={init_image_path}
isLink
onClick={() => dispatch(setInitialImage(init_image_path))}
/>
)}
)} */}
{mask_image_path && (
<MetadataItem
label="Mask image"

View File

@ -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];
}
}
);

View File

@ -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 { uploadImage } from 'services/thunks/image';
export type GalleryCategory = 'user' | 'result';
export type AddImagesPayload = {
images: Array<InvokeAI.Image>;
images: Array<InvokeAI._Image>;
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<InvokeAI.Image>) => {
imageSelected: (state, action: PayloadAction<string>) => {
state.selectedImageName = action.payload;
},
setCurrentImage: (state, action: PayloadAction<InvokeAI._Image>) => {
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(uploadImage.fulfilled, (state, action) => {
const location = action.payload;
const imageName = location.split('/').pop() || '';
state.selectedImageName = imageName;
});
},
});
export const {
imageSelected,
addImage,
clearIntermediateImage,
removeImage,

View File

@ -0,0 +1,110 @@
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 { deserializeImageField } from 'services/util/deserializeImageField';
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
// import { deserializeImageField } from 'services/util/deserializeImageField';
// 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<Image>({
// 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.timestamp - a.metadata.timestamp,
});
// 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
};
const resultsSlice = createSlice({
name: 'results',
initialState: resultsAdapter.getInitialState<AdditionalResultsState>({
// provide the additional initial state
page: 0,
pages: 0,
isLoading: false,
nextPage: 0,
}),
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.addOne,
},
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;
if (isImageOutput(data.result)) {
const resultImage = deserializeImageField(data.result.image);
resultsAdapter.addOne(state, resultImage);
}
});
},
});
// 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<RootState>((state) => state.results);
export const { resultAdded } = resultsSlice.actions;
export default resultsSlice.reducer;

View File

@ -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<void, RootState, unknown, AnyAction> =>
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));
}
};

View File

@ -0,0 +1,86 @@
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 { uploadImage } from 'services/thunks/image';
import { deserializeImageField } from 'services/util/deserializeImageField';
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
export const uploadsAdapter = createEntityAdapter<Image>({
selectId: (image) => image.name,
sortComparer: (a, b) => b.metadata.timestamp - a.metadata.timestamp,
});
type AdditionalUploadsState = {
page: number;
pages: number;
isLoading: boolean;
nextPage: number;
};
const uploadsSlice = createSlice({
name: 'uploads',
initialState: uploadsAdapter.getInitialState<AdditionalUploadsState>({
page: 0,
pages: 0,
nextPage: 0,
isLoading: false,
}),
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(uploadImage.fulfilled, (state, action) => {
const location = action.payload;
const uploadedImage = deserializeImageField({
image_name: location.split('/').pop() || '',
image_type: 'uploads',
});
uploadsAdapter.addOne(state, uploadedImage);
});
},
});
export const {
selectAll: selectUploadsAll,
selectById: selectUploadsById,
selectEntities: selectUploadsEntities,
selectIds: selectUploadsIds,
selectTotal: selectUploadsTotal,
} = uploadsAdapter.getSelectors<RootState>((state) => state.uploads);
export const { uploadAdded } = uploadsSlice.actions;
export default uploadsSlice.reducer;

View File

@ -3,7 +3,7 @@ import { TransformComponent, useTransformContext } from 'react-zoom-pan-pinch';
import * as InvokeAI from 'app/invokeai';
type ReactPanZoomProps = {
image: InvokeAI.Image;
image: InvokeAI._Image;
styleClass?: string;
alt?: string;
ref?: React.Ref<HTMLImageElement>;

View File

@ -21,9 +21,10 @@ type ParametersAccordionsType = {
const ParametersAccordion = (props: ParametersAccordionsType) => {
const { accordionInfo } = props;
const openAccordions = useAppSelector(
(state: RootState) => state.system.openAccordions
);
const { system, ui } = useAppSelector((state: RootState) => state);
const { openAccordions } = system;
const { disabledParameterPanels } = ui;
const dispatch = useAppDispatch();
@ -39,15 +40,19 @@ const ParametersAccordion = (props: ParametersAccordionsType) => {
Object.keys(accordionInfo).forEach((key) => {
const { header, feature, content, additionalHeaderComponents } =
accordionInfo[key];
accordionsToRender.push(
<InvokeAccordionItem
key={key}
header={header}
feature={feature}
content={content}
additionalHeaderComponents={additionalHeaderComponents}
/>
);
// do not render if panel is disabled in global state
if (disabledParameterPanels.indexOf(key) === -1) {
accordionsToRender.push(
<InvokeAccordionItem
key={key}
header={header}
feature={feature}
content={content}
additionalHeaderComponents={additionalHeaderComponents}
/>
);
}
});
}
return accordionsToRender;

View File

@ -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 { createSession } from 'services/thunks/session';
interface InvokeButton
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
@ -24,7 +25,8 @@ export default function InvokeButton(props: InvokeButton) {
const activeTabName = useAppSelector(activeTabNameSelector);
const handleClickGenerate = () => {
dispatch(generateImage(activeTabName));
// dispatch(generateImage(activeTabName));
dispatch(createSession());
};
const { t } = useTranslation();

View File

@ -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)
);
}
);

View File

@ -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;
@ -317,12 +317,12 @@ export const generationSlice = createSlice({
setShouldRandomizeSeed: (state, action: PayloadAction<boolean>) => {
state.shouldRandomizeSeed = action.payload;
},
setInitialImage: (
state,
action: PayloadAction<InvokeAI.Image | string>
) => {
state.initialImage = action.payload;
},
// setInitialImage: (
// state,
// action: PayloadAction<InvokeAI._Image | string>
// ) => {
// state.initialImage = action.payload;
// },
clearInitialImage: (state) => {
state.initialImage = undefined;
},
@ -353,6 +353,9 @@ export const generationSlice = createSlice({
setVerticalSymmetrySteps: (state, action: PayloadAction<number>) => {
state.verticalSymmetrySteps = action.payload;
},
initialImageSelected: (state, action: PayloadAction<string>) => {
state.initialImage = action.payload;
},
},
});
@ -368,7 +371,7 @@ export const {
setHeight,
setImg2imgStrength,
setInfillMethod,
setInitialImage,
// setInitialImage,
setIterations,
setMaskPath,
setParameter,
@ -394,6 +397,7 @@ export const {
setShouldUseSymmetry,
setHorizontalSymmetrySteps,
setVerticalSymmetrySteps,
initialImageSelected,
} = generationSlice.actions;
export default generationSlice.reducer;

View File

@ -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);

View File

@ -2,7 +2,20 @@ 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,
} 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';
export type LogLevel = 'info' | 'warning' | 'error';
@ -56,6 +69,10 @@ export interface SystemState
cancelType: CancelType;
cancelAfter: number | null;
};
/**
* The current progress image
*/
progressImage: ProgressImage | null;
}
const initialSystemState: SystemState = {
@ -98,6 +115,7 @@ const initialSystemState: SystemState = {
cancelType: 'immediate',
cancelAfter: null,
},
progressImage: null,
};
export const systemSlice = createSlice({
@ -272,6 +290,111 @@ export const systemSlice = createSlice({
state.cancelOptions.cancelAfter = action.payload;
},
},
extraReducers(builder) {
/**
* 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.currentStatusHasSteps = false;
});
/**
* Generator Progress
*/
builder.addCase(generatorProgress, (state, action) => {
const { step, total_steps, progress_image } = 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;
// 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' })
);
});
/**
* Initial Image Selected
*/
builder.addCase(initialImageSelected, (state) => {
state.toastQueue.push(makeToast(i18n.t('toast.sentToImageToImage')));
});
},
});
export const {

View File

@ -45,38 +45,41 @@ const tabIconStyles: ChakraProps['sx'] = {
boxSize: 6,
};
const tabInfo: InvokeTabInfo[] = [
{
id: 'txt2img',
icon: <Icon as={MdTextFields} sx={tabIconStyles} />,
workarea: <TextToImageWorkarea />,
},
{
id: 'img2img',
icon: <Icon as={MdPhotoLibrary} sx={tabIconStyles} />,
workarea: <ImageToImageWorkarea />,
},
{
id: 'unifiedCanvas',
icon: <Icon as={MdGridOn} sx={tabIconStyles} />,
workarea: <UnifiedCanvasWorkarea />,
},
{
id: 'nodes',
icon: <Icon as={MdDeviceHub} sx={tabIconStyles} />,
workarea: <NodesWIP />,
},
{
id: 'postprocessing',
icon: <Icon as={MdPhotoFilter} sx={tabIconStyles} />,
workarea: <PostProcessingWIP />,
},
{
id: 'training',
icon: <Icon as={MdFlashOn} sx={tabIconStyles} />,
workarea: <TrainingWIP />,
},
];
const buildTabs = (disabledTabs: InvokeTabName[]): InvokeTabInfo[] => {
const tabs: InvokeTabInfo[] = [
{
id: 'txt2img',
icon: <Icon as={MdTextFields} sx={tabIconStyles} />,
workarea: <TextToImageWorkarea />,
},
{
id: 'img2img',
icon: <Icon as={MdPhotoLibrary} sx={tabIconStyles} />,
workarea: <ImageToImageWorkarea />,
},
{
id: 'unifiedCanvas',
icon: <Icon as={MdGridOn} sx={tabIconStyles} />,
workarea: <UnifiedCanvasWorkarea />,
},
{
id: 'nodes',
icon: <Icon as={MdDeviceHub} sx={tabIconStyles} />,
workarea: <NodesWIP />,
},
{
id: 'postprocessing',
icon: <Icon as={MdPhotoFilter} sx={tabIconStyles} />,
workarea: <PostProcessingWIP />,
},
{
id: 'training',
icon: <Icon as={MdFlashOn} sx={tabIconStyles} />,
workarea: <TrainingWIP />,
},
];
return tabs.filter((tab) => !disabledTabs.includes(tab.id));
};
export default function InvokeTabs() {
const activeTab = useAppSelector(activeTabIndexSelector);
@ -85,13 +88,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();
@ -142,7 +142,7 @@ export default function InvokeTabs() {
const tabs = useMemo(
() =>
tabInfo.map((tab) => (
activeTabs.map((tab) => (
<Tooltip
key={tab.id}
hasArrow
@ -157,13 +157,13 @@ export default function InvokeTabs() {
</Tab>
</Tooltip>
)),
[t]
[t, activeTabs]
);
const tabPanels = useMemo(
() =>
tabInfo.map((tab) => <TabPanel key={tab.id}>{tab.workarea}</TabPanel>),
[]
activeTabs.map((tab) => <TabPanel key={tab.id}>{tab.workarea}</TabPanel>),
[activeTabs]
);
return (

View File

@ -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,
@ -47,7 +47,7 @@ const InvokeWorkarea = (props: InvokeWorkareaProps) => {
const image = getImageByUuid(uuid);
if (!image) return;
if (activeTabName === 'img2img') {
dispatch(setInitialImage(image));
dispatch(initialImageSelected(image.uuid));
} else if (activeTabName === 'unifiedCanvas') {
dispatch(setInitialCanvasImage(image));
}

View File

@ -96,7 +96,6 @@ const ParametersPanel = ({ children }: ParametersPanelProps) => {
onClose={closeParametersPanel}
isPinned={shouldPinParametersPanel || isLightboxOpen}
sx={{
borderColor: 'base.700',
p: shouldPinParametersPanel ? 0 : 4,
bg: 'base.900',
}}

View File

@ -1,14 +1,12 @@
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 { initialImageSelector } from 'features/parameters/store/generationSelectors';
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 initialImage = useAppSelector(initialImageSelector);
const { t } = useTranslation();

View File

@ -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);
}
};

View File

@ -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';
@ -16,6 +18,8 @@ const initialtabsState: UIState = {
addNewModelUIOption: null,
shouldPinGallery: true,
shouldShowGallery: true,
disabledParameterPanels: [],
disabledTabs: [],
};
const initialState: UIState = initialtabsState;
@ -25,11 +29,7 @@ export const uiSlice = createSlice({
initialState,
reducers: {
setActiveTab: (state, action: PayloadAction<number | InvokeTabName>) => {
if (typeof action.payload === 'number') {
state.activeTab = action.payload;
} else {
state.activeTab = tabMap.indexOf(action.payload);
}
setActiveTabReducer(state, action.payload);
},
setCurrentTheme: (state, action: PayloadAction<string>) => {
state.currentTheme = action.payload;
@ -92,6 +92,19 @@ export const uiSlice = createSlice({
state.shouldShowParametersPanel = true;
}
},
setDisabledPanels: (state, action: PayloadAction<string[]>) => {
state.disabledParameterPanels = action.payload;
},
setDisabledTabs: (state, action: PayloadAction<InvokeTabName[]>) => {
state.disabledTabs = action.payload;
},
},
extraReducers(builder) {
builder.addCase(initialImageSelected, (state) => {
if (tabMap[state.activeTab] !== 'img2img') {
setActiveTabReducer(state, 'img2img');
}
});
},
});
@ -113,6 +126,8 @@ export const {
togglePinParametersPanel,
toggleParametersPanel,
toggleGalleryPanel,
setDisabledPanels,
setDisabledTabs,
} = uiSlice.actions;
export default uiSlice.reducer;

View File

@ -1,3 +1,5 @@
import { InvokeTabName } from './tabMap';
export type AddNewModelType = 'ckpt' | 'diffusers' | null;
export interface UIState {
@ -13,4 +15,6 @@ export interface UIState {
addNewModelUIOption: AddNewModelType;
shouldPinGallery: boolean;
shouldShowGallery: boolean;
disabledParameterPanels: string[];
disabledTabs: InvokeTabName[];
}

View File

@ -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;
}
}

View File

@ -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<string, any>;
readonly cookies?: Record<string, any>;
readonly headers?: Record<string, any>;
readonly query?: Record<string, any>;
readonly formData?: Record<string, any>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly errors?: Record<number, string>;
};

View File

@ -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;
};

View File

@ -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<T> implements Promise<T> {
readonly [Symbol.toStringTag]!: string;
private _isResolved: boolean;
private _isRejected: boolean;
private _isCancelled: boolean;
private readonly _cancelHandlers: (() => void)[];
private readonly _promise: Promise<T>;
private _resolve?: (value: T | PromiseLike<T>) => void;
private _reject?: (reason?: any) => void;
constructor(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void,
onCancel: OnCancel
) => void
) {
this._isResolved = false;
this._isRejected = false;
this._isCancelled = false;
this._cancelHandlers = [];
this._promise = new Promise<T>((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
const onResolve = (value: T | PromiseLike<T>): 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<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2> {
return this._promise.then(onFulfilled, onRejected);
}
public catch<TResult = never>(
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<T | TResult> {
return this._promise.catch(onRejected);
}
public finally(onFinally?: (() => void) | null): Promise<T> {
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;
}
}

View File

@ -0,0 +1,31 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from './ApiRequestOptions';
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
type Headers = Record<string, string>;
export type OpenAPIConfig = {
BASE: string;
VERSION: string;
WITH_CREDENTIALS: boolean;
CREDENTIALS: 'include' | 'omit' | 'same-origin';
TOKEN?: string | Resolver<string>;
USERNAME?: string | Resolver<string>;
PASSWORD?: string | Resolver<string>;
HEADERS?: Headers | Resolver<Headers>;
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,
};

View File

@ -0,0 +1,349 @@
/* 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
*/
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 = <T>(
value: T | null | undefined
): value is Exclude<T, null | undefined> => {
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, any>): 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<T> = (options: ApiRequestOptions) => Promise<T>;
const resolve = async <T>(
options: ApiRequestOptions,
resolver?: T | Resolver<T>
): Promise<T | undefined> => {
if (typeof resolver === 'function') {
return (resolver as Resolver<T>)(options);
}
return resolver;
};
const getHeaders = async (
config: OpenAPIConfig,
options: ApiRequestOptions,
formData?: FormData
): Promise<Record<string, string>> => {
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<string, string>
);
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 <T>(
config: OpenAPIConfig,
options: ApiRequestOptions,
url: string,
body: any,
formData: FormData | undefined,
headers: Record<string, string>,
onCancel: OnCancel
): Promise<AxiosResponse<T>> => {
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<T>;
if (axiosError.response) {
return axiosError.response;
}
throw error;
}
};
const getResponseHeader = (
response: AxiosResponse<any>,
responseHeader?: string
): string | undefined => {
if (responseHeader) {
const content = response.headers[responseHeader];
if (isString(content)) {
return content;
}
}
return undefined;
};
const getResponseBody = (response: AxiosResponse<any>): any => {
if (response.status !== 204) {
return response.data;
}
return undefined;
};
const catchErrorCodes = (
options: ApiRequestOptions,
result: ApiResult
): void => {
const errors: Record<number, string> = {
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<T>
* @throws ApiError
*/
export const request = <T>(
config: OpenAPIConfig,
options: ApiRequestOptions
): CancelablePromise<T> => {
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<T>(
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);
}
});
};

View File

@ -0,0 +1,84 @@
/* 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 { BlurInvocation } from './models/BlurInvocation';
export type { Body_upload_image } from './models/Body_upload_image';
export type { CollectInvocation } from './models/CollectInvocation';
export type { CollectInvocationOutput } from './models/CollectInvocationOutput';
export type { CropImageInvocation } from './models/CropImageInvocation';
export type { CvInpaintInvocation } from './models/CvInpaintInvocation';
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 { ImageMetadata } from './models/ImageMetadata';
export type { ImageOutput } from './models/ImageOutput';
export type { ImageResponse } from './models/ImageResponse';
export type { ImageToImageInvocation } from './models/ImageToImageInvocation';
export type { ImageType } from './models/ImageType';
export type { InpaintInvocation } from './models/InpaintInvocation';
export type { InverseLerpInvocation } from './models/InverseLerpInvocation';
export type { IterateInvocation } from './models/IterateInvocation';
export type { IterateInvocationOutput } from './models/IterateInvocationOutput';
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 { PaginatedResults_GraphExecutionState_ } from './models/PaginatedResults_GraphExecutionState_';
export type { PaginatedResults_ImageResponse_ } from './models/PaginatedResults_ImageResponse_';
export type { PasteImageInvocation } from './models/PasteImageInvocation';
export type { PromptOutput } from './models/PromptOutput';
export type { RestoreFaceInvocation } from './models/RestoreFaceInvocation';
export type { ShowImageInvocation } from './models/ShowImageInvocation';
export type { TextToImageInvocation } from './models/TextToImageInvocation';
export type { UpscaleInvocation } from './models/UpscaleInvocation';
export type { ValidationError } from './models/ValidationError';
export { $BlurInvocation } from './schemas/$BlurInvocation';
export { $Body_upload_image } from './schemas/$Body_upload_image';
export { $CollectInvocation } from './schemas/$CollectInvocation';
export { $CollectInvocationOutput } from './schemas/$CollectInvocationOutput';
export { $CropImageInvocation } from './schemas/$CropImageInvocation';
export { $CvInpaintInvocation } from './schemas/$CvInpaintInvocation';
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 { $ImageMetadata } from './schemas/$ImageMetadata';
export { $ImageOutput } from './schemas/$ImageOutput';
export { $ImageResponse } from './schemas/$ImageResponse';
export { $ImageToImageInvocation } from './schemas/$ImageToImageInvocation';
export { $ImageType } from './schemas/$ImageType';
export { $InpaintInvocation } from './schemas/$InpaintInvocation';
export { $InverseLerpInvocation } from './schemas/$InverseLerpInvocation';
export { $IterateInvocation } from './schemas/$IterateInvocation';
export { $IterateInvocationOutput } from './schemas/$IterateInvocationOutput';
export { $LerpInvocation } from './schemas/$LerpInvocation';
export { $LoadImageInvocation } from './schemas/$LoadImageInvocation';
export { $MaskFromAlphaInvocation } from './schemas/$MaskFromAlphaInvocation';
export { $MaskOutput } from './schemas/$MaskOutput';
export { $PaginatedResults_GraphExecutionState_ } from './schemas/$PaginatedResults_GraphExecutionState_';
export { $PaginatedResults_ImageResponse_ } from './schemas/$PaginatedResults_ImageResponse_';
export { $PasteImageInvocation } from './schemas/$PasteImageInvocation';
export { $PromptOutput } from './schemas/$PromptOutput';
export { $RestoreFaceInvocation } from './schemas/$RestoreFaceInvocation';
export { $ShowImageInvocation } from './schemas/$ShowImageInvocation';
export { $TextToImageInvocation } from './schemas/$TextToImageInvocation';
export { $UpscaleInvocation } from './schemas/$UpscaleInvocation';
export { $ValidationError } from './schemas/$ValidationError';
export { ImagesService } from './services/ImagesService';
export { SessionsService } from './services/SessionsService';

View File

@ -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';
};

View File

@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Body_upload_image = {
file: Blob;
};

View File

@ -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<any>;
};

View File

@ -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<any>;
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -0,0 +1,38 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BlurInvocation } from './BlurInvocation';
import type { CollectInvocation } from './CollectInvocation';
import type { CropImageInvocation } from './CropImageInvocation';
import type { CvInpaintInvocation } from './CvInpaintInvocation';
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 { LerpInvocation } from './LerpInvocation';
import type { LoadImageInvocation } from './LoadImageInvocation';
import type { MaskFromAlphaInvocation } from './MaskFromAlphaInvocation';
import type { PasteImageInvocation } from './PasteImageInvocation';
import type { RestoreFaceInvocation } from './RestoreFaceInvocation';
import type { ShowImageInvocation } from './ShowImageInvocation';
import type { TextToImageInvocation } from './TextToImageInvocation';
import type { UpscaleInvocation } from './UpscaleInvocation';
export type Graph = {
/**
* The id of this graph
*/
id?: string;
/**
* The nodes in this graph
*/
nodes?: Record<string, (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | CvInpaintInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | GraphInvocation | IterateInvocation | CollectInvocation | ImageToImageInvocation | InpaintInvocation)>;
/**
* The connections between nodes and their fields in this graph
*/
edges?: Array<Edge>;
};

View File

@ -0,0 +1,54 @@
/* 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 { IterateInvocationOutput } from './IterateInvocationOutput';
import type { MaskOutput } from './MaskOutput';
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<string>;
/**
* The list of node ids that have been executed, in order of execution
*/
executed_history: Array<string>;
/**
* The results of node executions
*/
results: Record<string, (ImageOutput | MaskOutput | PromptOutput | GraphInvocationOutput | IterateInvocationOutput | CollectInvocationOutput)>;
/**
* Errors raised when executing nodes
*/
errors: Record<string, string>;
/**
* The map of prepared nodes to original graph nodes
*/
prepared_source_mapping: Record<string, string>;
/**
* The map of original graph nodes to prepared nodes
*/
source_prepared_mapping: Record<string, Array<string>>;
};

View File

@ -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;
};

View File

@ -0,0 +1,11 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* Base class for all invocation outputs
*/
export type GraphInvocationOutput = {
type: 'graph_output';
};

View File

@ -0,0 +1,10 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ValidationError } from './ValidationError';
export type HTTPValidationError = {
detail?: Array<ValidationError>;
};

View File

@ -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;
};

View File

@ -0,0 +1,26 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* An image's metadata
*/
export type ImageMetadata = {
/**
* The creation timestamp of the image
*/
timestamp: number;
/**
* The width of the image in pixels
*/
width: number;
/**
* The width of the image in pixels
*/
height: number;
/**
* The image's SD-specific metadata
*/
sd_metadata?: any;
};

View File

@ -0,0 +1,17 @@
/* 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;
};

View File

@ -0,0 +1,33 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ImageMetadata } from './ImageMetadata';
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: ImageMetadata;
};

View File

@ -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 sampler to use
*/
sampler_name?: '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;
};

View File

@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* An enumeration.
*/
export type ImageType = 'results' | 'intermediates' | 'uploads';

View File

@ -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 sampler to use
*/
sampler_name?: '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;
};

View File

@ -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;
};

View File

@ -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<any>;
/**
* The index, will be provided on executed iterators
*/
index?: number;
};

View File

@ -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;
};

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