mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Compare commits
80 Commits
release/ad
...
maryhipp/f
Author | SHA1 | Date | |
---|---|---|---|
ca56238e0a | |||
96f48db7df | |||
e2a5271612 | |||
49f88046fa | |||
7e0813c87e | |||
4ba9b710c2 | |||
22f497388b | |||
964abde220 | |||
e69a65b304 | |||
f6c6f61da6 | |||
500bdfa7dd | |||
d6104a3cea | |||
591d6bcdda | |||
8f12ec659c | |||
1a756e7f14 | |||
abc10a115d | |||
760b4b938c | |||
4e2358cb09 | |||
0e0ffb39ff | |||
a35dc090c5 | |||
42182b744c | |||
46aeeea29a | |||
9820829edb | |||
cc3401a159 | |||
cfe86ec541 | |||
b7de3162c3 | |||
de0df4945d | |||
406039426a | |||
daf1bc6b67 | |||
4f3be53d55 | |||
1628262ca8 | |||
ed7fe23436 | |||
a065f7db56 | |||
77bf3c780f | |||
ed00afc64d | |||
e2114a1da5 | |||
3722f055fb | |||
69c71c83e6 | |||
dbf6b1b68a | |||
9baa8f7a6a | |||
7ca32ce9f3 | |||
4fe7e52111 | |||
7ff50796e5 | |||
5a9157e628 | |||
d1058adb59 | |||
3c99abab32 | |||
7ed1772fad | |||
fd031b6c2a | |||
272803ba7c | |||
8d8284afaa | |||
01607c961f | |||
30b61ae8d4 | |||
1890bffc6e | |||
662f1321f6 | |||
4ac447cf74 | |||
f9c243d29f | |||
eda334bc34 | |||
f2a2f326ce | |||
7b10762ea1 | |||
2db2c986d1 | |||
40b2d2b05b | |||
4fe49718e0 | |||
3ebd289a59 | |||
1bd246e9a9 | |||
e75141150f | |||
660e665cf3 | |||
ca41a52174 | |||
999c3a443b | |||
21fb41ef56 | |||
4f99b005b1 | |||
d86588ec76 | |||
6c1f666242 | |||
07428769df | |||
b49338b464 | |||
92996898f2 | |||
d66d844dd2 | |||
d46d52ca63 | |||
427104f936 | |||
16442e8f15 | |||
1c7d92dc48 |
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
0
invokeai/app/datatypes/__init__.py
Normal file
0
invokeai/app/datatypes/__init__.py
Normal file
3
invokeai/app/datatypes/exceptions.py
Normal file
3
invokeai/app/datatypes/exceptions.py
Normal file
@ -0,0 +1,3 @@
|
||||
class CanceledException(Exception):
|
||||
"""Execution canceled by user."""
|
||||
pass
|
38
invokeai/app/datatypes/image.py
Normal file
38
invokeai/app/datatypes/image.py
Normal 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")
|
11
invokeai/app/datatypes/metadata.py
Normal file
11
invokeai/app/datatypes/metadata.py
Normal 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")
|
@ -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):
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
|
@ -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."""
|
||||
|
@ -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):
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
0
invokeai/app/util/__init__.py
Normal file
0
invokeai/app/util/__init__.py
Normal file
15
invokeai/app/util/generate_openapi_json.py
Normal file
15
invokeai/app/util/generate_openapi_json.py
Normal 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)
|
@ -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)
|
@ -6,3 +6,5 @@ stats.html
|
||||
index.html
|
||||
.yarn/
|
||||
*.scss
|
||||
src/services/api/
|
||||
src/services/fixtures/*
|
||||
|
@ -3,4 +3,8 @@ dist/
|
||||
node_modules/
|
||||
patches/
|
||||
stats.html
|
||||
index.html
|
||||
.yarn/
|
||||
*.scss
|
||||
src/services/api/
|
||||
src/services/fixtures/*
|
||||
|
87
invokeai/frontend/web/docs/API_CLIENT.md
Normal file
87
invokeai/frontend/web/docs/API_CLIENT.md
Normal 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`.
|
21
invokeai/frontend/web/docs/EVENTS.md
Normal file
21
invokeai/frontend/web/docs/EVENTS.md
Normal 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.
|
29
invokeai/frontend/web/docs/PACKAGE_SCRIPTS.md
Normal file
29
invokeai/frontend/web/docs/PACKAGE_SCRIPTS.md
Normal 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.
|
@ -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
|
||||
|
20
invokeai/frontend/web/index.d.ts
vendored
20
invokeai/frontend/web/index.d.ts
vendored
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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');
|
||||
|
21
invokeai/frontend/web/src/app/invokeai.d.ts
vendored
21
invokeai/frontend/web/src/app/invokeai.d.ts
vendored
@ -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;
|
||||
};
|
||||
|
@ -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'
|
||||
);
|
||||
|
@ -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);
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -29,6 +29,8 @@ export const socketioMiddleware = () => {
|
||||
path: `${window.location.pathname}socket.io`,
|
||||
});
|
||||
|
||||
socketio.disconnect();
|
||||
|
||||
let areListenersSet = false;
|
||||
|
||||
const middleware: Middleware = (store) => (next) => (action) => {
|
||||
|
@ -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: [
|
||||
|
8
invokeai/frontend/web/src/app/storeUtils.ts
Normal file
8
invokeai/frontend/web/src/app/storeUtils.ts
Normal 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;
|
||||
}>();
|
@ -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 () => {
|
||||
|
@ -14,6 +14,7 @@ const WorkInProgress = (props: WorkInProgressProps) => {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
bg: 'base.850',
|
||||
borderRadius: 'base',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
34
invokeai/frontend/web/src/common/util/buildGraph.ts
Normal file
34
invokeai/frontend/web/src/common/util/buildGraph.ts
Normal 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),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
145
invokeai/frontend/web/src/common/util/buildNodes.ts
Normal file
145
invokeai/frontend/web/src/common/util/buildNodes.ts
Normal 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,
|
||||
};
|
||||
}
|
6
invokeai/frontend/web/src/common/util/getTimestamp.ts
Normal file
6
invokeai/frontend/web/src/common/util/getTimestamp.ts
Normal 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');
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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 () => {
|
||||
|
@ -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,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ interface DeleteImageModalProps {
|
||||
/**
|
||||
* The image to delete.
|
||||
*/
|
||||
image?: InvokeAI.Image;
|
||||
image?: InvokeAI._Image;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -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,
|
||||
|
110
invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts
Normal file
110
invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts
Normal 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;
|
@ -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));
|
||||
}
|
||||
};
|
@ -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;
|
@ -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>;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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 (
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -96,7 +96,6 @@ const ParametersPanel = ({ children }: ParametersPanelProps) => {
|
||||
onClose={closeParametersPanel}
|
||||
isPinned={shouldPinParametersPanel || isLightboxOpen}
|
||||
sx={{
|
||||
borderColor: 'base.700',
|
||||
p: shouldPinParametersPanel ? 0 : 4,
|
||||
bg: 'base.900',
|
||||
}}
|
||||
|
@ -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();
|
||||
|
||||
|
13
invokeai/frontend/web/src/features/ui/store/extraReducers.ts
Normal file
13
invokeai/frontend/web/src/features/ui/store/extraReducers.ts
Normal 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);
|
||||
}
|
||||
};
|
@ -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;
|
||||
|
@ -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[];
|
||||
}
|
||||
|
24
invokeai/frontend/web/src/services/api/core/ApiError.ts
Normal file
24
invokeai/frontend/web/src/services/api/core/ApiError.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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>;
|
||||
};
|
10
invokeai/frontend/web/src/services/api/core/ApiResult.ts
Normal file
10
invokeai/frontend/web/src/services/api/core/ApiResult.ts
Normal 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;
|
||||
};
|
128
invokeai/frontend/web/src/services/api/core/CancelablePromise.ts
Normal file
128
invokeai/frontend/web/src/services/api/core/CancelablePromise.ts
Normal 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;
|
||||
}
|
||||
}
|
31
invokeai/frontend/web/src/services/api/core/OpenAPI.ts
Normal file
31
invokeai/frontend/web/src/services/api/core/OpenAPI.ts
Normal 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,
|
||||
};
|
349
invokeai/frontend/web/src/services/api/core/request.ts
Normal file
349
invokeai/frontend/web/src/services/api/core/request.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
};
|
84
invokeai/frontend/web/src/services/api/index.ts
Normal file
84
invokeai/frontend/web/src/services/api/index.ts
Normal 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';
|
@ -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';
|
||||
};
|
||||
|
@ -0,0 +1,8 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
export type Body_upload_image = {
|
||||
file: Blob;
|
||||
};
|
||||
|
@ -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>;
|
||||
};
|
||||
|
@ -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>;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
17
invokeai/frontend/web/src/services/api/models/Edge.ts
Normal file
17
invokeai/frontend/web/src/services/api/models/Edge.ts
Normal 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;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
38
invokeai/frontend/web/src/services/api/models/Graph.ts
Normal file
38
invokeai/frontend/web/src/services/api/models/Graph.ts
Normal 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>;
|
||||
};
|
||||
|
@ -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>>;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -0,0 +1,11 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* Base class for all invocation outputs
|
||||
*/
|
||||
export type GraphInvocationOutput = {
|
||||
type: 'graph_output';
|
||||
};
|
||||
|
@ -0,0 +1,10 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
import type { ValidationError } from './ValidationError';
|
||||
|
||||
export type HTTPValidationError = {
|
||||
detail?: Array<ValidationError>;
|
||||
};
|
||||
|
20
invokeai/frontend/web/src/services/api/models/ImageField.ts
Normal file
20
invokeai/frontend/web/src/services/api/models/ImageField.ts
Normal 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;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
17
invokeai/frontend/web/src/services/api/models/ImageOutput.ts
Normal file
17
invokeai/frontend/web/src/services/api/models/ImageOutput.ts
Normal 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;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -0,0 +1,8 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* An enumeration.
|
||||
*/
|
||||
export type ImageType = 'results' | 'intermediates' | 'uploads';
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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
Reference in New Issue
Block a user