mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Compare commits
80 Commits
fix/diffus
...
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)
|
# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654)
|
||||||
|
import io
|
||||||
from datetime import datetime, timezone
|
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.responses import FileResponse, Response
|
||||||
from fastapi.routing import APIRouter
|
from fastapi.routing import APIRouter
|
||||||
from PIL import Image
|
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 ...services.image_storage import ImageType
|
||||||
from ..dependencies import ApiDependencies
|
from ..dependencies import ApiDependencies
|
||||||
|
|
||||||
images_router = APIRouter(prefix="/v1/images", tags=["images"])
|
images_router = APIRouter(prefix="/v1/images", tags=["images"])
|
||||||
|
|
||||||
|
|
||||||
@images_router.get("/{image_type}/{image_name}", operation_id="get_image")
|
@images_router.get("/{image_type}/{image_name}", operation_id="get_image")
|
||||||
async def get_image(
|
async def get_image(
|
||||||
image_type: ImageType = Path(description="The type of image to get"),
|
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()
|
contents = await file.read()
|
||||||
try:
|
try:
|
||||||
im = Image.open(contents)
|
im = Image.open(io.BytesIO(contents))
|
||||||
except:
|
except:
|
||||||
# Error opening the image
|
# Error opening the image
|
||||||
return Response(status_code=415)
|
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)
|
ApiDependencies.invoker.services.images.save(ImageType.UPLOAD, filename, im)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
status_code=201,
|
status_code=201,
|
||||||
headers={
|
headers={
|
||||||
"Location": request.url_for(
|
"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 typing import Any, Callable, Iterable, Literal, get_args, get_origin, get_type_hints
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from ..invocations.image import ImageField
|
from invokeai.app.datatypes.image import ImageField
|
||||||
|
|
||||||
from ..services.graph import GraphExecutionState
|
from ..services.graph import GraphExecutionState
|
||||||
from ..services.invoker import Invoker
|
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 PIL import Image, ImageOps
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
from ..services.image_storage import ImageType
|
from invokeai.app.datatypes.image import ImageField, ImageType
|
||||||
from .baseinvocation import BaseInvocation, InvocationContext
|
from .baseinvocation import BaseInvocation, InvocationContext
|
||||||
from .image import ImageField, ImageOutput
|
from .image import ImageOutput
|
||||||
|
|
||||||
|
|
||||||
class CvInpaintInvocation(BaseInvocation):
|
class CvInpaintInvocation(BaseInvocation):
|
||||||
|
@ -8,12 +8,13 @@ from torch import Tensor
|
|||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
from ..services.image_storage import ImageType
|
from invokeai.app.datatypes.image import ImageField, ImageType
|
||||||
from .baseinvocation import BaseInvocation, InvocationContext
|
from .baseinvocation import BaseInvocation, InvocationContext
|
||||||
from .image import ImageField, ImageOutput
|
from .image import ImageOutput
|
||||||
from ...backend.generator import Txt2Img, Img2Img, Inpaint, InvokeAIGenerator
|
from ...backend.generator import Txt2Img, Img2Img, Inpaint, InvokeAIGenerator
|
||||||
from ...backend.stable_diffusion import PipelineIntermediateState
|
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[
|
SAMPLER_NAME_VALUES = Literal[
|
||||||
tuple(InvokeAIGenerator.schedulers())
|
tuple(InvokeAIGenerator.schedulers())
|
||||||
|
@ -7,20 +7,10 @@ import numpy
|
|||||||
from PIL import Image, ImageFilter, ImageOps
|
from PIL import Image, ImageFilter, ImageOps
|
||||||
from pydantic import BaseModel, Field
|
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 ..services.invocation_services import InvocationServices
|
||||||
from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationContext
|
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):
|
class ImageOutput(BaseInvocationOutput):
|
||||||
"""Base class for invocations that output an image"""
|
"""Base class for invocations that output an image"""
|
||||||
#fmt: off
|
#fmt: off
|
||||||
|
@ -3,10 +3,10 @@ from typing import Literal, Union
|
|||||||
|
|
||||||
from pydantic import Field
|
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 ..services.invocation_services import InvocationServices
|
||||||
from .baseinvocation import BaseInvocation, InvocationContext
|
from .baseinvocation import BaseInvocation, InvocationContext
|
||||||
from .image import ImageField, ImageOutput
|
from .image import ImageOutput
|
||||||
|
|
||||||
class RestoreFaceInvocation(BaseInvocation):
|
class RestoreFaceInvocation(BaseInvocation):
|
||||||
"""Restores faces in an image."""
|
"""Restores faces in an image."""
|
||||||
|
@ -5,10 +5,10 @@ from typing import Literal, Union
|
|||||||
|
|
||||||
from pydantic import Field
|
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 ..services.invocation_services import InvocationServices
|
||||||
from .baseinvocation import BaseInvocation, InvocationContext
|
from .baseinvocation import BaseInvocation, InvocationContext
|
||||||
from .image import ImageField, ImageOutput
|
from .image import ImageOutput
|
||||||
|
|
||||||
|
|
||||||
class UpscaleInvocation(BaseInvocation):
|
class UpscaleInvocation(BaseInvocation):
|
||||||
|
@ -2,24 +2,24 @@
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
|
from glob import glob
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from typing import Dict
|
from typing import Callable, Dict, List
|
||||||
|
|
||||||
from PIL.Image import Image
|
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.app.util.save_thumbnail import save_thumbnail
|
||||||
|
|
||||||
from invokeai.backend.image_util import PngWriter
|
from invokeai.backend.image_util import PngWriter
|
||||||
|
|
||||||
|
|
||||||
class ImageType(str, Enum):
|
|
||||||
RESULT = "results"
|
|
||||||
INTERMEDIATE = "intermediates"
|
|
||||||
UPLOAD = "uploads"
|
|
||||||
|
|
||||||
|
|
||||||
class ImageStorageBase(ABC):
|
class ImageStorageBase(ABC):
|
||||||
"""Responsible for storing and retrieving images."""
|
"""Responsible for storing and retrieving images."""
|
||||||
|
|
||||||
@ -27,9 +27,17 @@ class ImageStorageBase(ABC):
|
|||||||
def get(self, image_type: ImageType, image_name: str) -> Image:
|
def get(self, image_type: ImageType, image_name: str) -> Image:
|
||||||
pass
|
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
|
# TODO: make this a bit more flexible for e.g. cloud storage
|
||||||
@abstractmethod
|
@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
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@ -71,18 +79,74 @@ class DiskImageStorage(ImageStorageBase):
|
|||||||
parents=True, exist_ok=True
|
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:
|
def get(self, image_type: ImageType, image_name: str) -> Image:
|
||||||
image_path = self.get_path(image_type, image_name)
|
image_path = self.get_path(image_type, image_name)
|
||||||
cache_item = self.__get_cache(image_path)
|
cache_item = self.__get_cache(image_path)
|
||||||
if cache_item:
|
if cache_item:
|
||||||
return cache_item
|
return cache_item
|
||||||
|
|
||||||
image = Image.open(image_path)
|
image = PILImage.open(image_path)
|
||||||
self.__set_cache(image_path, image)
|
self.__set_cache(image_path, image)
|
||||||
return image
|
return image
|
||||||
|
|
||||||
# TODO: make this a bit more flexible for e.g. cloud storage
|
# TODO: make this a bit more flexible for e.g. cloud storage
|
||||||
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:
|
||||||
|
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)
|
path = os.path.join(self.__output_folder, image_type, image_name)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
@ -101,12 +165,19 @@ class DiskImageStorage(ImageStorageBase):
|
|||||||
|
|
||||||
def delete(self, image_type: ImageType, image_name: str) -> None:
|
def delete(self, image_type: ImageType, image_name: str) -> None:
|
||||||
image_path = self.get_path(image_type, image_name)
|
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):
|
if os.path.exists(image_path):
|
||||||
os.remove(image_path)
|
os.remove(image_path)
|
||||||
|
|
||||||
if image_path in self.__cache:
|
if image_path in self.__cache:
|
||||||
del self.__cache[image_path]
|
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:
|
def __get_cache(self, image_name: str) -> Image:
|
||||||
return None if image_name not in self.__cache else self.__cache[image_name]
|
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 ..invocations.baseinvocation import InvocationContext
|
||||||
from .invocation_queue import InvocationQueueItem
|
from .invocation_queue import InvocationQueueItem
|
||||||
from .invoker import InvocationProcessorABC, Invoker
|
from .invoker import InvocationProcessorABC, Invoker
|
||||||
from ..util.util import CanceledException
|
from ..datatypes.exceptions import CanceledException
|
||||||
|
|
||||||
class DefaultInvocationProcessor(InvocationProcessorABC):
|
class DefaultInvocationProcessor(InvocationProcessorABC):
|
||||||
__invoker_thread: Thread
|
__invoker_thread: Thread
|
||||||
|
@ -106,10 +106,12 @@ class SqliteItemStorage(ItemStorageABC, Generic[T]):
|
|||||||
finally:
|
finally:
|
||||||
self._lock.release()
|
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](
|
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(
|
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
|
import torch
|
||||||
from PIL import Image
|
|
||||||
from ..invocations.baseinvocation import InvocationContext
|
from ..invocations.baseinvocation import InvocationContext
|
||||||
from ...backend.util.util import image_to_dataURL
|
from ...backend.util.util import image_to_dataURL
|
||||||
from ...backend.generator.base import Generator
|
from ...backend.generator.base import Generator
|
||||||
from ...backend.stable_diffusion import PipelineIntermediateState
|
from ...backend.stable_diffusion import PipelineIntermediateState
|
||||||
|
|
||||||
class CanceledException(Exception):
|
def fast_latents_step_callback(
|
||||||
pass
|
sample: torch.Tensor,
|
||||||
|
step: int,
|
||||||
def fast_latents_step_callback(sample: torch.Tensor, step: int, steps: int, id: str, context: InvocationContext, ):
|
steps: int,
|
||||||
|
id: str,
|
||||||
|
context: InvocationContext,
|
||||||
|
):
|
||||||
# TODO: only output a preview image when requested
|
# TODO: only output a preview image when requested
|
||||||
image = Generator.sample_to_lowres_estimated_image(sample)
|
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.services.events.emit_generator_progress(
|
||||||
context.graph_execution_state_id,
|
context.graph_execution_state_id,
|
||||||
id,
|
id,
|
||||||
{
|
{"width": width, "height": height, "dataURL": dataURL},
|
||||||
"width": width,
|
|
||||||
"height": height,
|
|
||||||
"dataURL": dataURL
|
|
||||||
},
|
|
||||||
step,
|
step,
|
||||||
steps,
|
steps,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def diffusers_step_callback_adapter(*cb_args, **kwargs):
|
def diffusers_step_callback_adapter(*cb_args, **kwargs):
|
||||||
"""
|
"""
|
||||||
txt2img gives us a Tensor in the step_callbak, while img2img gives us a PipelineIntermediateState.
|
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):
|
if isinstance(cb_args[0], PipelineIntermediateState):
|
||||||
progress_state: PipelineIntermediateState = cb_args[0]
|
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:
|
else:
|
||||||
return fast_latents_step_callback(*cb_args, **kwargs)
|
return fast_latents_step_callback(*cb_args, **kwargs)
|
@ -6,3 +6,5 @@ stats.html
|
|||||||
index.html
|
index.html
|
||||||
.yarn/
|
.yarn/
|
||||||
*.scss
|
*.scss
|
||||||
|
src/services/api/
|
||||||
|
src/services/fixtures/*
|
||||||
|
@ -3,4 +3,8 @@ dist/
|
|||||||
node_modules/
|
node_modules/
|
||||||
patches/
|
patches/
|
||||||
stats.html
|
stats.html
|
||||||
|
index.html
|
||||||
.yarn/
|
.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](#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.
|
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.
|
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).
|
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`
|
1. Start the dev server: `yarn dev`
|
||||||
2. Start the InvokeAI UI per usual: `invokeai --web`
|
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
|
### 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 React, { PropsWithChildren } from 'react';
|
||||||
import { IAIPopoverProps } from '../web/src/common/components/IAIPopover';
|
import { IAIPopoverProps } from '../web/src/common/components/IAIPopover';
|
||||||
import { IAIIconButtonProps } from '../web/src/common/components/IAIIconButton';
|
import { IAIIconButtonProps } from '../web/src/common/components/IAIIconButton';
|
||||||
|
import { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|
||||||
@ -64,9 +65,24 @@ declare module '@invoke-ai/invoke-ai-ui' {
|
|||||||
declare class SettingsModal extends React.Component<SettingsModalProps> {
|
declare class SettingsModal extends React.Component<SettingsModalProps> {
|
||||||
public constructor(props: SettingsModalProps);
|
public constructor(props: SettingsModalProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare class StatusIndicator extends React.Component<StatusIndicatorProps> {
|
||||||
|
public constructor(props: StatusIndicatorProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
declare function Invoke(props: PropsWithChildren): JSX.Element;
|
declare class ModelSelect extends React.Component<ModelSelectProps> {
|
||||||
|
public constructor(props: ModelSelectProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InvokeProps extends PropsWithChildren {
|
||||||
|
apiUrl?: string;
|
||||||
|
disabledPanels?: string[];
|
||||||
|
disabledTabs?: InvokeTabName[];
|
||||||
|
token?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare function Invoke(props: InvokeProps): JSX.Element;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ThemeChanger,
|
ThemeChanger,
|
||||||
@ -74,5 +90,7 @@ export {
|
|||||||
IAIPopover,
|
IAIPopover,
|
||||||
IAIIconButton,
|
IAIIconButton,
|
||||||
SettingsModal,
|
SettingsModal,
|
||||||
|
StatusIndicator,
|
||||||
|
ModelSelect,
|
||||||
};
|
};
|
||||||
export = Invoke;
|
export = Invoke;
|
||||||
|
@ -5,7 +5,10 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "cd ../../../ && husky install invokeai/frontend/web/.husky",
|
"prepare": "cd ../../../ && husky install invokeai/frontend/web/.husky",
|
||||||
"dev": "concurrently \"vite dev\" \"yarn run theme:watch\"",
|
"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",
|
"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",
|
"preview": "vite preview",
|
||||||
"lint:madge": "madge --circular src/main.tsx",
|
"lint:madge": "madge --circular src/main.tsx",
|
||||||
"lint:eslint": "eslint --max-warnings=0 .",
|
"lint:eslint": "eslint --max-warnings=0 .",
|
||||||
@ -43,7 +46,7 @@
|
|||||||
"@chakra-ui/theme-tools": "^2.0.16",
|
"@chakra-ui/theme-tools": "^2.0.16",
|
||||||
"@emotion/react": "^11.10.6",
|
"@emotion/react": "^11.10.6",
|
||||||
"@emotion/styled": "^11.10.6",
|
"@emotion/styled": "^11.10.6",
|
||||||
"@reduxjs/toolkit": "^1.9.2",
|
"@reduxjs/toolkit": "^1.9.3",
|
||||||
"chakra-ui-contextmenu": "^1.0.5",
|
"chakra-ui-contextmenu": "^1.0.5",
|
||||||
"dateformat": "^5.0.3",
|
"dateformat": "^5.0.3",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
@ -83,6 +86,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^5.52.0",
|
"@typescript-eslint/eslint-plugin": "^5.52.0",
|
||||||
"@typescript-eslint/parser": "^5.52.0",
|
"@typescript-eslint/parser": "^5.52.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.2.0",
|
"@vitejs/plugin-react-swc": "^3.2.0",
|
||||||
|
"axios": "^1.3.4",
|
||||||
"babel-plugin-transform-imports": "^2.0.0",
|
"babel-plugin-transform-imports": "^2.0.0",
|
||||||
"concurrently": "^7.6.0",
|
"concurrently": "^7.6.0",
|
||||||
"eslint": "^8.34.0",
|
"eslint": "^8.34.0",
|
||||||
@ -90,13 +94,16 @@
|
|||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-react": "^7.32.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"lint-staged": "^13.1.2",
|
"lint-staged": "^13.1.2",
|
||||||
"madge": "^6.0.0",
|
"madge": "^6.0.0",
|
||||||
|
"openapi-typescript-codegen": "^0.23.0",
|
||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"prettier": "^2.8.4",
|
"prettier": "^2.8.4",
|
||||||
"rollup-plugin-visualizer": "^5.9.0",
|
"rollup-plugin-visualizer": "^5.9.0",
|
||||||
"terser": "^5.16.4",
|
"terser": "^5.16.4",
|
||||||
|
"typescript": "4.9.5",
|
||||||
"vite": "^4.1.2",
|
"vite": "^4.1.2",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-tsconfig-paths": "^4.0.5",
|
"vite-tsconfig-paths": "^4.0.5",
|
||||||
|
@ -522,6 +522,9 @@
|
|||||||
"resetComplete": "Web UI has been reset. Refresh the page to reload."
|
"resetComplete": "Web UI has been reset. Refresh the page to reload."
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
|
"serverError": "Server Error",
|
||||||
|
"disconnected": "Disconnected from Server",
|
||||||
|
"connected": "Connected to Server",
|
||||||
"tempFoldersEmptied": "Temp Folder Emptied",
|
"tempFoldersEmptied": "Temp Folder Emptied",
|
||||||
"uploadFailed": "Upload failed",
|
"uploadFailed": "Upload failed",
|
||||||
"uploadFailedMultipleImagesDesc": "Multiple images pasted, may only upload one image at a time",
|
"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 { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants';
|
||||||
import ImageGalleryPanel from 'features/gallery/components/ImageGalleryPanel';
|
import ImageGalleryPanel from 'features/gallery/components/ImageGalleryPanel';
|
||||||
import Lightbox from 'features/lightbox/components/Lightbox';
|
import Lightbox from 'features/lightbox/components/Lightbox';
|
||||||
import { useAppSelector } from './storeHooks';
|
import { useAppDispatch, useAppSelector } from './storeHooks';
|
||||||
import { PropsWithChildren, useEffect } from 'react';
|
import { PropsWithChildren, useEffect } from 'react';
|
||||||
|
import { setDisabledPanels, setDisabledTabs } from 'features/ui/store/uiSlice';
|
||||||
|
import { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
|
|
||||||
keepGUIAlive();
|
keepGUIAlive();
|
||||||
|
|
||||||
const App = (props: PropsWithChildren) => {
|
interface Props extends PropsWithChildren {
|
||||||
|
options: {
|
||||||
|
disabledPanels: string[];
|
||||||
|
disabledTabs: InvokeTabName[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const App = (props: Props) => {
|
||||||
useToastWatcher();
|
useToastWatcher();
|
||||||
|
|
||||||
const currentTheme = useAppSelector((state) => state.ui.currentTheme);
|
const currentTheme = useAppSelector((state) => state.ui.currentTheme);
|
||||||
const { setColorMode } = useColorMode();
|
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(() => {
|
useEffect(() => {
|
||||||
setColorMode(['light'].includes(currentTheme) ? 'light' : 'dark');
|
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 { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
import { IRect } from 'konva/lib/types';
|
import { IRect } from 'konva/lib/types';
|
||||||
|
import { ImageMetadata, ImageType } from 'services/api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO:
|
* TODO:
|
||||||
@ -113,7 +114,7 @@ export declare type Metadata = SystemGenerationMetadata & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// An Image has a UUID, url, modified timestamp, width, height and maybe metadata
|
// An Image has a UUID, url, modified timestamp, width, height and maybe metadata
|
||||||
export declare type Image = {
|
export declare type _Image = {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
url: string;
|
url: string;
|
||||||
thumbnail: string;
|
thumbnail: string;
|
||||||
@ -124,11 +125,23 @@ export declare type Image = {
|
|||||||
category: GalleryCategory;
|
category: GalleryCategory;
|
||||||
isBase64?: boolean;
|
isBase64?: boolean;
|
||||||
dreamPrompt?: 'string';
|
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.
|
// GalleryImages is an array of Image.
|
||||||
export declare type GalleryImages = {
|
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 SystemConfigResponse = SystemConfig;
|
||||||
|
|
||||||
export declare type ImageResultResponse = Omit<Image, 'uuid'> & {
|
export declare type ImageResultResponse = Omit<_Image, 'uuid'> & {
|
||||||
boundingBox?: IRect;
|
boundingBox?: IRect;
|
||||||
generationMode: InvokeTabName;
|
generationMode: InvokeTabName;
|
||||||
};
|
};
|
||||||
@ -296,7 +309,7 @@ export declare type ErrorResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export declare type GalleryImagesResponse = {
|
export declare type GalleryImagesResponse = {
|
||||||
images: Array<Omit<Image, 'uuid'>>;
|
images: Array<Omit<_Image, 'uuid'>>;
|
||||||
areMoreImagesAvailable: boolean;
|
areMoreImagesAvailable: boolean;
|
||||||
category: GalleryCategory;
|
category: GalleryCategory;
|
||||||
};
|
};
|
||||||
|
@ -13,9 +13,13 @@ import { InvokeTabName } from 'features/ui/store/tabMap';
|
|||||||
export const generateImage = createAction<InvokeTabName>(
|
export const generateImage = createAction<InvokeTabName>(
|
||||||
'socketio/generateImage'
|
'socketio/generateImage'
|
||||||
);
|
);
|
||||||
export const runESRGAN = createAction<InvokeAI.Image>('socketio/runESRGAN');
|
export const runESRGAN = createAction<InvokeAI._Image>('socketio/runESRGAN');
|
||||||
export const runFacetool = createAction<InvokeAI.Image>('socketio/runFacetool');
|
export const runFacetool = createAction<InvokeAI._Image>(
|
||||||
export const deleteImage = createAction<InvokeAI.Image>('socketio/deleteImage');
|
'socketio/runFacetool'
|
||||||
|
);
|
||||||
|
export const deleteImage = createAction<InvokeAI._Image>(
|
||||||
|
'socketio/deleteImage'
|
||||||
|
);
|
||||||
export const requestImages = createAction<GalleryCategory>(
|
export const requestImages = createAction<GalleryCategory>(
|
||||||
'socketio/requestImages'
|
'socketio/requestImages'
|
||||||
);
|
);
|
||||||
|
@ -91,7 +91,7 @@ const makeSocketIOEmitters = (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
emitRunESRGAN: (imageToProcess: InvokeAI.Image) => {
|
emitRunESRGAN: (imageToProcess: InvokeAI._Image) => {
|
||||||
dispatch(setIsProcessing(true));
|
dispatch(setIsProcessing(true));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -119,7 +119,7 @@ const makeSocketIOEmitters = (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
emitRunFacetool: (imageToProcess: InvokeAI.Image) => {
|
emitRunFacetool: (imageToProcess: InvokeAI._Image) => {
|
||||||
dispatch(setIsProcessing(true));
|
dispatch(setIsProcessing(true));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -150,7 +150,7 @@ const makeSocketIOEmitters = (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
emitDeleteImage: (imageToDelete: InvokeAI.Image) => {
|
emitDeleteImage: (imageToDelete: InvokeAI._Image) => {
|
||||||
const { url, uuid, category, thumbnail } = imageToDelete;
|
const { url, uuid, category, thumbnail } = imageToDelete;
|
||||||
dispatch(removeImage(imageToDelete));
|
dispatch(removeImage(imageToDelete));
|
||||||
socketio.emit('deleteImage', url, thumbnail, uuid, category);
|
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 { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
|
||||||
import {
|
import {
|
||||||
clearInitialImage,
|
clearInitialImage,
|
||||||
|
initialImageSelected,
|
||||||
setInfillMethod,
|
setInfillMethod,
|
||||||
setInitialImage,
|
// setInitialImage,
|
||||||
setMaskPath,
|
setMaskPath,
|
||||||
} from 'features/parameters/store/generationSlice';
|
} from 'features/parameters/store/generationSlice';
|
||||||
import { tabMap } from 'features/ui/store/tabMap';
|
import { tabMap } from 'features/ui/store/tabMap';
|
||||||
@ -146,7 +147,8 @@ const makeSocketIOListeners = (
|
|||||||
const activeTabName = tabMap[activeTab];
|
const activeTabName = tabMap[activeTab];
|
||||||
switch (activeTabName) {
|
switch (activeTabName) {
|
||||||
case 'img2img': {
|
case 'img2img': {
|
||||||
dispatch(setInitialImage(newImage));
|
dispatch(initialImageSelected(newImage.uuid));
|
||||||
|
// dispatch(setInitialImage(newImage));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -262,7 +264,7 @@ const makeSocketIOListeners = (
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Generate a UUID for each image
|
// Generate a UUID for each image
|
||||||
const preparedImages = images.map((image): InvokeAI.Image => {
|
const preparedImages = images.map((image): InvokeAI._Image => {
|
||||||
return {
|
return {
|
||||||
uuid: uuidv4(),
|
uuid: uuidv4(),
|
||||||
...image,
|
...image,
|
||||||
@ -334,7 +336,7 @@ const makeSocketIOListeners = (
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
initialImage === url ||
|
initialImage === url ||
|
||||||
(initialImage as InvokeAI.Image)?.url === url
|
(initialImage as InvokeAI._Image)?.url === url
|
||||||
) {
|
) {
|
||||||
dispatch(clearInitialImage());
|
dispatch(clearInitialImage());
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,8 @@ export const socketioMiddleware = () => {
|
|||||||
path: `${window.location.pathname}socket.io`,
|
path: `${window.location.pathname}socket.io`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socketio.disconnect();
|
||||||
|
|
||||||
let areListenersSet = false;
|
let areListenersSet = false;
|
||||||
|
|
||||||
const middleware: Middleware = (store) => (next) => (action) => {
|
const middleware: Middleware = (store) => (next) => (action) => {
|
||||||
|
@ -7,6 +7,8 @@ import { getPersistConfig } from 'redux-deep-persist';
|
|||||||
|
|
||||||
import canvasReducer from 'features/canvas/store/canvasSlice';
|
import canvasReducer from 'features/canvas/store/canvasSlice';
|
||||||
import galleryReducer from 'features/gallery/store/gallerySlice';
|
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 lightboxReducer from 'features/lightbox/store/lightboxSlice';
|
||||||
import generationReducer from 'features/parameters/store/generationSlice';
|
import generationReducer from 'features/parameters/store/generationSlice';
|
||||||
import postprocessingReducer from 'features/parameters/store/postprocessingSlice';
|
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 uiReducer from 'features/ui/store/uiSlice';
|
||||||
|
|
||||||
import { socketioMiddleware } from './socketio/middleware';
|
import { socketioMiddleware } from './socketio/middleware';
|
||||||
|
import { socketMiddleware } from 'services/events/middleware';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* redux-persist provides an easy and reliable way to persist state across reloads.
|
* redux-persist provides an easy and reliable way to persist state across reloads.
|
||||||
@ -64,6 +67,10 @@ const lightboxBlacklist = ['isLightboxOpen'].map(
|
|||||||
(blacklistItem) => `lightbox.${blacklistItem}`
|
(blacklistItem) => `lightbox.${blacklistItem}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const apiBlacklist = ['sessionId', 'status', 'progress', 'progressImage'].map(
|
||||||
|
(blacklistItem) => `api.${blacklistItem}`
|
||||||
|
);
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
generation: generationReducer,
|
generation: generationReducer,
|
||||||
postprocessing: postprocessingReducer,
|
postprocessing: postprocessingReducer,
|
||||||
@ -72,6 +79,8 @@ const rootReducer = combineReducers({
|
|||||||
canvas: canvasReducer,
|
canvas: canvasReducer,
|
||||||
ui: uiReducer,
|
ui: uiReducer,
|
||||||
lightbox: lightboxReducer,
|
lightbox: lightboxReducer,
|
||||||
|
results: resultsReducer,
|
||||||
|
uploads: uploadsReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rootPersistConfig = getPersistConfig({
|
const rootPersistConfig = getPersistConfig({
|
||||||
@ -83,12 +92,24 @@ const rootPersistConfig = getPersistConfig({
|
|||||||
...systemBlacklist,
|
...systemBlacklist,
|
||||||
...galleryBlacklist,
|
...galleryBlacklist,
|
||||||
...lightboxBlacklist,
|
...lightboxBlacklist,
|
||||||
|
...apiBlacklist,
|
||||||
|
// for now, never persist the results/uploads slices
|
||||||
|
'results',
|
||||||
|
'uploads',
|
||||||
],
|
],
|
||||||
debounce: 300,
|
debounce: 300,
|
||||||
});
|
});
|
||||||
|
|
||||||
const persistedReducer = persistReducer(rootPersistConfig, rootReducer);
|
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
|
// Continue with store setup
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: persistedReducer,
|
reducer: persistedReducer,
|
||||||
@ -96,7 +117,7 @@ export const store = configureStore({
|
|||||||
getDefaultMiddleware({
|
getDefaultMiddleware({
|
||||||
immutableCheck: false,
|
immutableCheck: false,
|
||||||
serializableCheck: false,
|
serializableCheck: false,
|
||||||
}).concat(socketioMiddleware()),
|
}).concat(socketMiddleware()),
|
||||||
devTools: {
|
devTools: {
|
||||||
// Uncommenting these very rapidly called actions makes the redux dev tools output much more readable
|
// Uncommenting these very rapidly called actions makes the redux dev tools output much more readable
|
||||||
actionsDenylist: [
|
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 { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||||
import useImageUploader from 'common/hooks/useImageUploader';
|
import useImageUploader from 'common/hooks/useImageUploader';
|
||||||
import { uploadImage } from 'features/gallery/store/thunks/uploadImage';
|
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import { ResourceKey } from 'i18next';
|
import { ResourceKey } from 'i18next';
|
||||||
import {
|
import {
|
||||||
@ -15,6 +14,7 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { uploadImage } from 'services/thunks/image';
|
||||||
import ImageUploadOverlay from './ImageUploadOverlay';
|
import ImageUploadOverlay from './ImageUploadOverlay';
|
||||||
|
|
||||||
type ImageUploaderProps = {
|
type ImageUploaderProps = {
|
||||||
@ -49,7 +49,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
|
|
||||||
const fileAcceptedCallback = useCallback(
|
const fileAcceptedCallback = useCallback(
|
||||||
async (file: File) => {
|
async (file: File) => {
|
||||||
dispatch(uploadImage({ imageFile: file }));
|
dispatch(uploadImage({ formData: { file } }));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
@ -124,7 +124,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(uploadImage({ imageFile: file }));
|
dispatch(uploadImage({ formData: { file } }));
|
||||||
};
|
};
|
||||||
document.addEventListener('paste', pasteImageListener);
|
document.addEventListener('paste', pasteImageListener);
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -14,6 +14,7 @@ const WorkInProgress = (props: WorkInProgressProps) => {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
bg: 'base.850',
|
bg: 'base.850',
|
||||||
|
borderRadius: 'base',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{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 { Provider } from 'react-redux';
|
||||||
import { PersistGate } from 'redux-persist/integration/react';
|
import { PersistGate } from 'redux-persist/integration/react';
|
||||||
import { store } from './app/store';
|
import { store } from './app/store';
|
||||||
import { persistor } from './persistor';
|
import { persistor } from './persistor';
|
||||||
|
import { OpenAPI } from 'services/api';
|
||||||
|
import { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
import '@fontsource/inter/100.css';
|
import '@fontsource/inter/100.css';
|
||||||
import '@fontsource/inter/200.css';
|
import '@fontsource/inter/200.css';
|
||||||
import '@fontsource/inter/300.css';
|
import '@fontsource/inter/300.css';
|
||||||
@ -21,18 +23,45 @@ import './i18n';
|
|||||||
const App = lazy(() => import('./app/App'));
|
const App = lazy(() => import('./app/App'));
|
||||||
const ThemeLocaleProvider = lazy(() => import('./app/ThemeLocaleProvider'));
|
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 (
|
return (
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
{ready && (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<PersistGate loading={<Loading />} persistor={persistor}>
|
<PersistGate loading={<Loading />} persistor={persistor}>
|
||||||
<React.Suspense fallback={<Loading showText />}>
|
<React.Suspense fallback={<Loading showText />}>
|
||||||
<ThemeLocaleProvider>
|
<ThemeLocaleProvider>
|
||||||
<App>{props.children}</App>
|
<App options={{ disabledPanels, disabledTabs }}>{children}</App>
|
||||||
</ThemeLocaleProvider>
|
</ThemeLocaleProvider>
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</PersistGate>
|
</PersistGate>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
)}
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ import ThemeChanger from './features/system/components/ThemeChanger';
|
|||||||
import IAIPopover from './common/components/IAIPopover';
|
import IAIPopover from './common/components/IAIPopover';
|
||||||
import IAIIconButton from './common/components/IAIIconButton';
|
import IAIIconButton from './common/components/IAIIconButton';
|
||||||
import SettingsModal from './features/system/components/SettingsModal/SettingsModal';
|
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 default Component;
|
||||||
export {
|
export {
|
||||||
@ -13,4 +15,6 @@ export {
|
|||||||
IAIPopover,
|
IAIPopover,
|
||||||
IAIIconButton,
|
IAIIconButton,
|
||||||
SettingsModal,
|
SettingsModal,
|
||||||
|
StatusIndicator,
|
||||||
|
ModelSelect,
|
||||||
};
|
};
|
||||||
|
@ -156,7 +156,7 @@ export const canvasSlice = createSlice({
|
|||||||
setCursorPosition: (state, action: PayloadAction<Vector2d | null>) => {
|
setCursorPosition: (state, action: PayloadAction<Vector2d | null>) => {
|
||||||
state.cursorPosition = action.payload;
|
state.cursorPosition = action.payload;
|
||||||
},
|
},
|
||||||
setInitialCanvasImage: (state, action: PayloadAction<InvokeAI.Image>) => {
|
setInitialCanvasImage: (state, action: PayloadAction<InvokeAI._Image>) => {
|
||||||
const image = action.payload;
|
const image = action.payload;
|
||||||
const { stageDimensions } = state;
|
const { stageDimensions } = state;
|
||||||
|
|
||||||
@ -291,7 +291,7 @@ export const canvasSlice = createSlice({
|
|||||||
state,
|
state,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<{
|
||||||
boundingBox: IRect;
|
boundingBox: IRect;
|
||||||
image: InvokeAI.Image;
|
image: InvokeAI._Image;
|
||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
const { boundingBox, image } = action.payload;
|
const { boundingBox, image } = action.payload;
|
||||||
|
@ -37,7 +37,7 @@ export type CanvasImage = {
|
|||||||
y: number;
|
y: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
image: InvokeAI.Image;
|
image: InvokeAI._Image;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CanvasMaskLine = {
|
export type CanvasMaskLine = {
|
||||||
@ -125,7 +125,7 @@ export interface CanvasState {
|
|||||||
cursorPosition: Vector2d | null;
|
cursorPosition: Vector2d | null;
|
||||||
doesCanvasNeedScaling: boolean;
|
doesCanvasNeedScaling: boolean;
|
||||||
futureLayerStates: CanvasLayerState[];
|
futureLayerStates: CanvasLayerState[];
|
||||||
intermediateImage?: InvokeAI.Image;
|
intermediateImage?: InvokeAI._Image;
|
||||||
isCanvasInitialized: boolean;
|
isCanvasInitialized: boolean;
|
||||||
isDrawing: boolean;
|
isDrawing: boolean;
|
||||||
isMaskEnabled: boolean;
|
isMaskEnabled: boolean;
|
||||||
|
@ -105,7 +105,7 @@ export const mergeAndUploadCanvas =
|
|||||||
|
|
||||||
const { url, width, height } = image;
|
const { url, width, height } = image;
|
||||||
|
|
||||||
const newImage: InvokeAI.Image = {
|
const newImage: InvokeAI._Image = {
|
||||||
uuid: uuidv4(),
|
uuid: uuidv4(),
|
||||||
category: shouldSaveToGallery ? 'result' : 'user',
|
category: shouldSaveToGallery ? 'result' : 'user',
|
||||||
...image,
|
...image,
|
||||||
|
@ -14,8 +14,9 @@ import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
|
|||||||
import FaceRestoreSettings from 'features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreSettings';
|
import FaceRestoreSettings from 'features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreSettings';
|
||||||
import UpscaleSettings from 'features/parameters/components/AdvancedParameters/Upscale/UpscaleSettings';
|
import UpscaleSettings from 'features/parameters/components/AdvancedParameters/Upscale/UpscaleSettings';
|
||||||
import {
|
import {
|
||||||
|
initialImageSelected,
|
||||||
setAllParameters,
|
setAllParameters,
|
||||||
setInitialImage,
|
// setInitialImage,
|
||||||
setSeed,
|
setSeed,
|
||||||
} from 'features/parameters/store/generationSlice';
|
} from 'features/parameters/store/generationSlice';
|
||||||
import { postprocessingSelector } from 'features/parameters/store/postprocessingSelectors';
|
import { postprocessingSelector } from 'features/parameters/store/postprocessingSelectors';
|
||||||
@ -129,8 +130,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
const handleClickUseAsInitialImage = () => {
|
const handleClickUseAsInitialImage = () => {
|
||||||
if (!currentImage) return;
|
if (!currentImage) return;
|
||||||
if (isLightboxOpen) dispatch(setIsLightboxOpen(false));
|
if (isLightboxOpen) dispatch(setIsLightboxOpen(false));
|
||||||
dispatch(setInitialImage(currentImage));
|
dispatch(initialImageSelected(currentImage.uuid));
|
||||||
dispatch(setActiveTab('img2img'));
|
// dispatch(setInitialImage(currentImage));
|
||||||
|
|
||||||
|
// dispatch(setActiveTab('img2img'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyImage = async () => {
|
const handleCopyImage = async () => {
|
||||||
|
@ -4,17 +4,20 @@ import { useAppSelector } from 'app/storeHooks';
|
|||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
|
|
||||||
import { MdPhoto } from 'react-icons/md';
|
import { MdPhoto } from 'react-icons/md';
|
||||||
import { gallerySelector } from '../store/gallerySelectors';
|
import {
|
||||||
|
gallerySelector,
|
||||||
|
selectedImageSelector,
|
||||||
|
} from '../store/gallerySelectors';
|
||||||
import CurrentImageButtons from './CurrentImageButtons';
|
import CurrentImageButtons from './CurrentImageButtons';
|
||||||
import CurrentImagePreview from './CurrentImagePreview';
|
import CurrentImagePreview from './CurrentImagePreview';
|
||||||
|
|
||||||
export const currentImageDisplaySelector = createSelector(
|
export const currentImageDisplaySelector = createSelector(
|
||||||
[gallerySelector],
|
[gallerySelector, selectedImageSelector],
|
||||||
(gallery) => {
|
(gallery, selectedImage) => {
|
||||||
const { currentImage, intermediateImage } = gallery;
|
const { currentImage, intermediateImage } = gallery;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasAnImageToDisplay: currentImage || intermediateImage,
|
hasAnImageToDisplay: selectedImage || intermediateImage,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,26 +1,44 @@
|
|||||||
import { Box, Flex, Image } from '@chakra-ui/react';
|
import { Box, Flex, Image } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppSelector } from 'app/storeHooks';
|
import { useAppSelector } from 'app/storeHooks';
|
||||||
import { GalleryState } from 'features/gallery/store/gallerySlice';
|
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||||
import { uiSelector } from 'features/ui/store/uiSelectors';
|
import { uiSelector } from 'features/ui/store/uiSelectors';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { APP_METADATA_HEIGHT } from 'theme/util/constants';
|
import { APP_METADATA_HEIGHT } from 'theme/util/constants';
|
||||||
|
|
||||||
import { gallerySelector } from '../store/gallerySelectors';
|
import { selectedImageSelector } from '../store/gallerySelectors';
|
||||||
import CurrentImageFallback from './CurrentImageFallback';
|
import CurrentImageFallback from './CurrentImageFallback';
|
||||||
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
|
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
|
||||||
import NextPrevImageButtons from './NextPrevImageButtons';
|
import NextPrevImageButtons from './NextPrevImageButtons';
|
||||||
|
|
||||||
export const imagesSelector = createSelector(
|
export const imagesSelector = createSelector(
|
||||||
[gallerySelector, uiSelector],
|
[uiSelector, selectedImageSelector, systemSelector],
|
||||||
(gallery: GalleryState, ui) => {
|
(ui, selectedImage, system) => {
|
||||||
const { currentImage, intermediateImage } = gallery;
|
|
||||||
const { shouldShowImageDetails } = ui;
|
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 {
|
return {
|
||||||
imageToDisplay: intermediateImage ? intermediateImage : currentImage,
|
|
||||||
isIntermediate: Boolean(intermediateImage),
|
|
||||||
shouldShowImageDetails,
|
shouldShowImageDetails,
|
||||||
|
imageToDisplay,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -31,9 +49,9 @@ export const imagesSelector = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export default function CurrentImagePreview() {
|
export default function CurrentImagePreview() {
|
||||||
const { shouldShowImageDetails, imageToDisplay, isIntermediate } =
|
const { shouldShowImageDetails, imageToDisplay } =
|
||||||
useAppSelector(imagesSelector);
|
useAppSelector(imagesSelector);
|
||||||
|
console.log(imageToDisplay);
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
@ -49,20 +67,28 @@ export default function CurrentImagePreview() {
|
|||||||
src={imageToDisplay.url}
|
src={imageToDisplay.url}
|
||||||
width={imageToDisplay.width}
|
width={imageToDisplay.width}
|
||||||
height={imageToDisplay.height}
|
height={imageToDisplay.height}
|
||||||
fallback={!isIntermediate ? <CurrentImageFallback /> : undefined}
|
fallback={
|
||||||
|
!imageToDisplay.isProgressImage ? (
|
||||||
|
<CurrentImageFallback />
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
sx={{
|
sx={{
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
maxHeight: '100%',
|
maxHeight: '100%',
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
imageRendering: isIntermediate ? 'pixelated' : 'initial',
|
imageRendering: imageToDisplay.isProgressImage
|
||||||
|
? 'pixelated'
|
||||||
|
: 'initial',
|
||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!shouldShowImageDetails && <NextPrevImageButtons />}
|
{!shouldShowImageDetails && <NextPrevImageButtons />}
|
||||||
{shouldShowImageDetails && imageToDisplay && (
|
{shouldShowImageDetails &&
|
||||||
|
imageToDisplay &&
|
||||||
|
'metadata' in imageToDisplay.image && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@ -74,7 +100,7 @@ export default function CurrentImagePreview() {
|
|||||||
maxHeight: APP_METADATA_HEIGHT,
|
maxHeight: APP_METADATA_HEIGHT,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ImageMetadataViewer image={imageToDisplay} />
|
<ImageMetadataViewer image={imageToDisplay.image} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -52,7 +52,7 @@ interface DeleteImageModalProps {
|
|||||||
/**
|
/**
|
||||||
* The image to delete.
|
* The image to delete.
|
||||||
*/
|
*/
|
||||||
image?: InvokeAI.Image;
|
image?: InvokeAI._Image;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -9,11 +9,14 @@ import {
|
|||||||
useToast,
|
useToast,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||||
import { setCurrentImage } from 'features/gallery/store/gallerySlice';
|
|
||||||
import {
|
import {
|
||||||
|
imageSelected,
|
||||||
|
setCurrentImage,
|
||||||
|
} from 'features/gallery/store/gallerySlice';
|
||||||
|
import {
|
||||||
|
initialImageSelected,
|
||||||
setAllImageToImageParameters,
|
setAllImageToImageParameters,
|
||||||
setAllParameters,
|
setAllParameters,
|
||||||
setInitialImage,
|
|
||||||
setSeed,
|
setSeed,
|
||||||
} from 'features/parameters/store/generationSlice';
|
} from 'features/parameters/store/generationSlice';
|
||||||
import { DragEvent, memo, useState } from 'react';
|
import { DragEvent, memo, useState } from 'react';
|
||||||
@ -40,7 +43,7 @@ interface HoverableImageProps {
|
|||||||
const memoEqualityCheck = (
|
const memoEqualityCheck = (
|
||||||
prev: HoverableImageProps,
|
prev: HoverableImageProps,
|
||||||
next: 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.
|
* Gallery image component with delete/use all/use seed buttons on hover.
|
||||||
@ -55,7 +58,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
shouldUseSingleGalleryColumn,
|
shouldUseSingleGalleryColumn,
|
||||||
} = useAppSelector(hoverableImageSelector);
|
} = useAppSelector(hoverableImageSelector);
|
||||||
const { image, isSelected } = props;
|
const { image, isSelected } = props;
|
||||||
const { url, thumbnail, uuid, metadata } = image;
|
const { url, thumbnail, name, metadata } = image;
|
||||||
|
|
||||||
const [isHovered, setIsHovered] = useState<boolean>(false);
|
const [isHovered, setIsHovered] = useState<boolean>(false);
|
||||||
|
|
||||||
@ -69,10 +72,9 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
const handleMouseOut = () => setIsHovered(false);
|
const handleMouseOut = () => setIsHovered(false);
|
||||||
|
|
||||||
const handleUsePrompt = () => {
|
const handleUsePrompt = () => {
|
||||||
if (image.metadata?.image?.prompt) {
|
if (image.metadata?.sd_metadata?.prompt) {
|
||||||
setBothPrompts(image.metadata?.image?.prompt);
|
setBothPrompts(image.metadata?.sd_metadata?.prompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t('toast.promptSet'),
|
title: t('toast.promptSet'),
|
||||||
status: 'success',
|
status: 'success',
|
||||||
@ -82,7 +84,8 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUseSeed = () => {
|
const handleUseSeed = () => {
|
||||||
image.metadata && dispatch(setSeed(image.metadata.image.seed));
|
image.metadata.sd_metadata &&
|
||||||
|
dispatch(setSeed(image.metadata.sd_metadata.image.seed));
|
||||||
toast({
|
toast({
|
||||||
title: t('toast.seedSet'),
|
title: t('toast.seedSet'),
|
||||||
status: 'success',
|
status: 'success',
|
||||||
@ -92,20 +95,11 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSendToImageToImage = () => {
|
const handleSendToImageToImage = () => {
|
||||||
dispatch(setInitialImage(image));
|
dispatch(initialImageSelected(image.name));
|
||||||
if (activeTabName !== 'img2img') {
|
|
||||||
dispatch(setActiveTab('img2img'));
|
|
||||||
}
|
|
||||||
toast({
|
|
||||||
title: t('toast.sentToImageToImage'),
|
|
||||||
status: 'success',
|
|
||||||
duration: 2500,
|
|
||||||
isClosable: true,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendToCanvas = () => {
|
const handleSendToCanvas = () => {
|
||||||
dispatch(setInitialCanvasImage(image));
|
// dispatch(setInitialCanvasImage(image));
|
||||||
|
|
||||||
dispatch(resizeAndScaleCanvas());
|
dispatch(resizeAndScaleCanvas());
|
||||||
|
|
||||||
@ -122,7 +116,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUseAllParameters = () => {
|
const handleUseAllParameters = () => {
|
||||||
metadata && dispatch(setAllParameters(metadata));
|
metadata.sd_metadata && dispatch(setAllParameters(metadata.sd_metadata));
|
||||||
toast({
|
toast({
|
||||||
title: t('toast.parametersSet'),
|
title: t('toast.parametersSet'),
|
||||||
status: 'success',
|
status: 'success',
|
||||||
@ -132,11 +126,13 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUseInitialImage = async () => {
|
const handleUseInitialImage = async () => {
|
||||||
if (metadata?.image?.init_image_path) {
|
if (metadata.sd_metadata?.image?.init_image_path) {
|
||||||
const response = await fetch(metadata.image.init_image_path);
|
const response = await fetch(
|
||||||
|
metadata.sd_metadata?.image?.init_image_path
|
||||||
|
);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
dispatch(setActiveTab('img2img'));
|
dispatch(setActiveTab('img2img'));
|
||||||
dispatch(setAllImageToImageParameters(metadata));
|
dispatch(setAllImageToImageParameters(metadata?.sd_metadata));
|
||||||
toast({
|
toast({
|
||||||
title: t('toast.initialImageSet'),
|
title: t('toast.initialImageSet'),
|
||||||
status: 'success',
|
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>) => {
|
const handleDragStart = (e: DragEvent<HTMLDivElement>) => {
|
||||||
e.dataTransfer.setData('invokeai/imageUuid', uuid);
|
// e.dataTransfer.setData('invokeai/imageUuid', uuid);
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
// e.dataTransfer.effectAllowed = 'move';
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLightBox = () => {
|
const handleLightBox = () => {
|
||||||
dispatch(setCurrentImage(image));
|
// dispatch(setCurrentImage(image));
|
||||||
dispatch(setIsLightboxOpen(true));
|
// dispatch(setIsLightboxOpen(true));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -177,28 +175,30 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClickCapture={handleUsePrompt}
|
onClickCapture={handleUsePrompt}
|
||||||
isDisabled={image?.metadata?.image?.prompt === undefined}
|
isDisabled={image?.metadata?.sd_metadata?.prompt === undefined}
|
||||||
>
|
>
|
||||||
{t('parameters.usePrompt')}
|
{t('parameters.usePrompt')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClickCapture={handleUseSeed}
|
onClickCapture={handleUseSeed}
|
||||||
isDisabled={image?.metadata?.image?.seed === undefined}
|
isDisabled={image?.metadata?.sd_metadata?.seed === undefined}
|
||||||
>
|
>
|
||||||
{t('parameters.useSeed')}
|
{t('parameters.useSeed')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClickCapture={handleUseAllParameters}
|
onClickCapture={handleUseAllParameters}
|
||||||
isDisabled={
|
isDisabled={
|
||||||
!['txt2img', 'img2img'].includes(image?.metadata?.image?.type)
|
!['txt2img', 'img2img'].includes(
|
||||||
|
image?.metadata?.sd_metadata?.type
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('parameters.useAll')}
|
{t('parameters.useAll')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClickCapture={handleUseInitialImage}
|
onClickCapture={handleUseInitialImage}
|
||||||
isDisabled={image?.metadata?.image?.type !== 'img2img'}
|
isDisabled={image?.metadata?.sd_metadata?.type !== 'img2img'}
|
||||||
>
|
>
|
||||||
{t('parameters.useInitImg')}
|
{t('parameters.useInitImg')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@ -209,9 +209,9 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
{t('parameters.sendToUnifiedCanvas')}
|
{t('parameters.sendToUnifiedCanvas')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem data-warning>
|
<MenuItem data-warning>
|
||||||
<DeleteImageModal image={image}>
|
{/* <DeleteImageModal image={image}>
|
||||||
<p>{t('parameters.deleteImage')}</p>
|
<p>{t('parameters.deleteImage')}</p>
|
||||||
</DeleteImageModal>
|
</DeleteImageModal> */}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
)}
|
)}
|
||||||
@ -219,7 +219,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
{(ref) => (
|
{(ref) => (
|
||||||
<Box
|
<Box
|
||||||
position="relative"
|
position="relative"
|
||||||
key={uuid}
|
key={name}
|
||||||
onMouseOver={handleMouseOver}
|
onMouseOver={handleMouseOver}
|
||||||
onMouseOut={handleMouseOut}
|
onMouseOut={handleMouseOut}
|
||||||
userSelect="none"
|
userSelect="none"
|
||||||
@ -290,7 +290,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
insetInlineEnd: 1,
|
insetInlineEnd: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DeleteImageModal image={image}>
|
{/* <DeleteImageModal image={image}>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label={t('parameters.deleteImage')}
|
aria-label={t('parameters.deleteImage')}
|
||||||
icon={<FaTrashAlt />}
|
icon={<FaTrashAlt />}
|
||||||
@ -298,7 +298,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
fontSize={14}
|
fontSize={14}
|
||||||
isDisabled={!mayDeleteImage}
|
isDisabled={!mayDeleteImage}
|
||||||
/>
|
/>
|
||||||
</DeleteImageModal>
|
</DeleteImageModal> */}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</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 { requestImages } from 'app/socketio/actions';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||||
import IAIButton from 'common/components/IAIButton';
|
import IAIButton from 'common/components/IAIButton';
|
||||||
@ -25,9 +25,44 @@ import HoverableImage from './HoverableImage';
|
|||||||
|
|
||||||
import Scrollable from 'features/ui/components/common/Scrollable';
|
import Scrollable from 'features/ui/components/common/Scrollable';
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
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 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 ImageGalleryContent = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -35,7 +70,7 @@ const ImageGalleryContent = () => {
|
|||||||
const [shouldShouldIconButtons, setShouldShouldIconButtons] = useState(true);
|
const [shouldShouldIconButtons, setShouldShouldIconButtons] = useState(true);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
images,
|
// images,
|
||||||
currentCategory,
|
currentCategory,
|
||||||
currentImageUuid,
|
currentImageUuid,
|
||||||
shouldPinGallery,
|
shouldPinGallery,
|
||||||
@ -43,12 +78,24 @@ const ImageGalleryContent = () => {
|
|||||||
galleryGridTemplateColumns,
|
galleryGridTemplateColumns,
|
||||||
galleryImageObjectFit,
|
galleryImageObjectFit,
|
||||||
shouldAutoSwitchToNewImages,
|
shouldAutoSwitchToNewImages,
|
||||||
areMoreImagesAvailable,
|
// areMoreImagesAvailable,
|
||||||
shouldUseSingleGalleryColumn,
|
shouldUseSingleGalleryColumn,
|
||||||
} = useAppSelector(imageGallerySelector);
|
} = useAppSelector(imageGallerySelector);
|
||||||
|
|
||||||
|
const { images, areMoreImagesAvailable, isLoading } =
|
||||||
|
useAppSelector(gallerySelector);
|
||||||
|
|
||||||
|
// const handleClickLoadMore = () => {
|
||||||
|
// dispatch(requestImages(currentCategory));
|
||||||
|
// };
|
||||||
const handleClickLoadMore = () => {
|
const handleClickLoadMore = () => {
|
||||||
dispatch(requestImages(currentCategory));
|
if (currentCategory === 'result') {
|
||||||
|
dispatch(receivedResultImagesPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentCategory === 'user') {
|
||||||
|
dispatch(receivedUploadImagesPage());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeGalleryImageMinimumWidth = (v: number) => {
|
const handleChangeGalleryImageMinimumWidth = (v: number) => {
|
||||||
@ -203,11 +250,11 @@ const ImageGalleryContent = () => {
|
|||||||
style={{ gridTemplateColumns: galleryGridTemplateColumns }}
|
style={{ gridTemplateColumns: galleryGridTemplateColumns }}
|
||||||
>
|
>
|
||||||
{images.map((image) => {
|
{images.map((image) => {
|
||||||
const { uuid } = image;
|
const { name } = image;
|
||||||
const isSelected = currentImageUuid === uuid;
|
const isSelected = currentImageUuid === name;
|
||||||
return (
|
return (
|
||||||
<HoverableImage
|
<HoverableImage
|
||||||
key={uuid}
|
key={name}
|
||||||
image={image}
|
image={image}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
/>
|
/>
|
||||||
@ -217,6 +264,7 @@ const ImageGalleryContent = () => {
|
|||||||
<IAIButton
|
<IAIButton
|
||||||
onClick={handleClickLoadMore}
|
onClick={handleClickLoadMore}
|
||||||
isDisabled={!areMoreImagesAvailable}
|
isDisabled={!areMoreImagesAvailable}
|
||||||
|
isLoading={isLoading}
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
{areMoreImagesAvailable
|
{areMoreImagesAvailable
|
||||||
|
@ -18,7 +18,7 @@ import {
|
|||||||
setCfgScale,
|
setCfgScale,
|
||||||
setHeight,
|
setHeight,
|
||||||
setImg2imgStrength,
|
setImg2imgStrength,
|
||||||
setInitialImage,
|
// setInitialImage,
|
||||||
setMaskPath,
|
setMaskPath,
|
||||||
setPerlin,
|
setPerlin,
|
||||||
setSampler,
|
setSampler,
|
||||||
@ -120,7 +120,7 @@ type ImageMetadataViewerProps = {
|
|||||||
const memoEqualityCheck = (
|
const memoEqualityCheck = (
|
||||||
prev: ImageMetadataViewerProps,
|
prev: ImageMetadataViewerProps,
|
||||||
next: ImageMetadataViewerProps
|
next: ImageMetadataViewerProps
|
||||||
) => prev.image.uuid === next.image.uuid;
|
) => prev.image.name === next.image.name;
|
||||||
|
|
||||||
// TODO: Show more interesting information in this component.
|
// TODO: Show more interesting information in this component.
|
||||||
|
|
||||||
@ -137,8 +137,8 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
|||||||
dispatch(setShouldShowImageDetails(false));
|
dispatch(setShouldShowImageDetails(false));
|
||||||
});
|
});
|
||||||
|
|
||||||
const metadata = image?.metadata?.image || {};
|
const metadata = image?.metadata.sd_metadata || {};
|
||||||
const dreamPrompt = image?.dreamPrompt;
|
const dreamPrompt = image?.metadata.sd_metadata?.dreamPrompt;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
cfg_scale,
|
cfg_scale,
|
||||||
@ -160,6 +160,7 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
|||||||
type,
|
type,
|
||||||
variations,
|
variations,
|
||||||
width,
|
width,
|
||||||
|
model_weights,
|
||||||
} = metadata;
|
} = metadata;
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -193,8 +194,8 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
|||||||
{Object.keys(metadata).length > 0 ? (
|
{Object.keys(metadata).length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{type && <MetadataItem label="Generation type" value={type} />}
|
{type && <MetadataItem label="Generation type" value={type} />}
|
||||||
{image.metadata?.model_weights && (
|
{model_weights && (
|
||||||
<MetadataItem label="Model" value={image.metadata.model_weights} />
|
<MetadataItem label="Model" value={model_weights} />
|
||||||
)}
|
)}
|
||||||
{['esrgan', 'gfpgan'].includes(type) && (
|
{['esrgan', 'gfpgan'].includes(type) && (
|
||||||
<MetadataItem label="Original image" value={orig_path} />
|
<MetadataItem label="Original image" value={orig_path} />
|
||||||
@ -288,14 +289,14 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
|||||||
onClick={() => dispatch(setHeight(height))}
|
onClick={() => dispatch(setHeight(height))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{init_image_path && (
|
{/* {init_image_path && (
|
||||||
<MetadataItem
|
<MetadataItem
|
||||||
label="Initial image"
|
label="Initial image"
|
||||||
value={init_image_path}
|
value={init_image_path}
|
||||||
isLink
|
isLink
|
||||||
onClick={() => dispatch(setInitialImage(init_image_path))}
|
onClick={() => dispatch(setInitialImage(init_image_path))}
|
||||||
/>
|
/>
|
||||||
)}
|
)} */}
|
||||||
{mask_image_path && (
|
{mask_image_path && (
|
||||||
<MetadataItem
|
<MetadataItem
|
||||||
label="Mask image"
|
label="Mask image"
|
||||||
|
@ -7,6 +7,16 @@ import {
|
|||||||
uiSelector,
|
uiSelector,
|
||||||
} from 'features/ui/store/uiSelectors';
|
} from 'features/ui/store/uiSelectors';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
|
import {
|
||||||
|
selectResultsAll,
|
||||||
|
selectResultsById,
|
||||||
|
selectResultsEntities,
|
||||||
|
} from './resultsSlice';
|
||||||
|
import {
|
||||||
|
selectUploadsAll,
|
||||||
|
selectUploadsById,
|
||||||
|
selectUploadsEntities,
|
||||||
|
} from './uploadsSlice';
|
||||||
|
|
||||||
export const gallerySelector = (state: RootState) => state.gallery;
|
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 type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import * as InvokeAI from 'app/invokeai';
|
import * as InvokeAI from 'app/invokeai';
|
||||||
|
import { invocationComplete } from 'services/events/actions';
|
||||||
import { InvokeTabName } from 'features/ui/store/tabMap';
|
import { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
import { IRect } from 'konva/lib/types';
|
import { IRect } from 'konva/lib/types';
|
||||||
import { clamp } from 'lodash';
|
import { clamp } from 'lodash';
|
||||||
|
import { isImageOutput } from 'services/types/guards';
|
||||||
|
import { uploadImage } from 'services/thunks/image';
|
||||||
|
|
||||||
export type GalleryCategory = 'user' | 'result';
|
export type GalleryCategory = 'user' | 'result';
|
||||||
|
|
||||||
export type AddImagesPayload = {
|
export type AddImagesPayload = {
|
||||||
images: Array<InvokeAI.Image>;
|
images: Array<InvokeAI._Image>;
|
||||||
areMoreImagesAvailable: boolean;
|
areMoreImagesAvailable: boolean;
|
||||||
category: GalleryCategory;
|
category: GalleryCategory;
|
||||||
};
|
};
|
||||||
@ -16,16 +19,33 @@ export type AddImagesPayload = {
|
|||||||
type GalleryImageObjectFitType = 'contain' | 'cover';
|
type GalleryImageObjectFitType = 'contain' | 'cover';
|
||||||
|
|
||||||
export type Gallery = {
|
export type Gallery = {
|
||||||
images: InvokeAI.Image[];
|
images: InvokeAI._Image[];
|
||||||
latest_mtime?: number;
|
latest_mtime?: number;
|
||||||
earliest_mtime?: number;
|
earliest_mtime?: number;
|
||||||
areMoreImagesAvailable: boolean;
|
areMoreImagesAvailable: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface GalleryState {
|
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;
|
currentImageUuid: string;
|
||||||
intermediateImage?: InvokeAI.Image & {
|
/**
|
||||||
|
* The current progress image
|
||||||
|
* @deprecated See `state.system.progressImage`
|
||||||
|
*/
|
||||||
|
intermediateImage?: InvokeAI._Image & {
|
||||||
boundingBox?: IRect;
|
boundingBox?: IRect;
|
||||||
generationMode?: InvokeTabName;
|
generationMode?: InvokeTabName;
|
||||||
};
|
};
|
||||||
@ -42,6 +62,7 @@ export interface GalleryState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initialState: GalleryState = {
|
const initialState: GalleryState = {
|
||||||
|
selectedImageName: '',
|
||||||
currentImageUuid: '',
|
currentImageUuid: '',
|
||||||
galleryImageMinimumWidth: 64,
|
galleryImageMinimumWidth: 64,
|
||||||
galleryImageObjectFit: 'cover',
|
galleryImageObjectFit: 'cover',
|
||||||
@ -69,7 +90,10 @@ export const gallerySlice = createSlice({
|
|||||||
name: 'gallery',
|
name: 'gallery',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
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.currentImage = action.payload;
|
||||||
state.currentImageUuid = action.payload.uuid;
|
state.currentImageUuid = action.payload.uuid;
|
||||||
},
|
},
|
||||||
@ -124,7 +148,7 @@ export const gallerySlice = createSlice({
|
|||||||
addImage: (
|
addImage: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<{
|
||||||
image: InvokeAI.Image;
|
image: InvokeAI._Image;
|
||||||
category: GalleryCategory;
|
category: GalleryCategory;
|
||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
@ -150,7 +174,10 @@ export const gallerySlice = createSlice({
|
|||||||
setIntermediateImage: (
|
setIntermediateImage: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<
|
action: PayloadAction<
|
||||||
InvokeAI.Image & { boundingBox?: IRect; generationMode?: InvokeTabName }
|
InvokeAI._Image & {
|
||||||
|
boundingBox?: IRect;
|
||||||
|
generationMode?: InvokeTabName;
|
||||||
|
}
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
state.intermediateImage = action.payload;
|
state.intermediateImage = action.payload;
|
||||||
@ -252,9 +279,31 @@ export const gallerySlice = createSlice({
|
|||||||
state.shouldUseSingleGalleryColumn = action.payload;
|
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 {
|
export const {
|
||||||
|
imageSelected,
|
||||||
addImage,
|
addImage,
|
||||||
clearIntermediateImage,
|
clearIntermediateImage,
|
||||||
removeImage,
|
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';
|
import * as InvokeAI from 'app/invokeai';
|
||||||
|
|
||||||
type ReactPanZoomProps = {
|
type ReactPanZoomProps = {
|
||||||
image: InvokeAI.Image;
|
image: InvokeAI._Image;
|
||||||
styleClass?: string;
|
styleClass?: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
ref?: React.Ref<HTMLImageElement>;
|
ref?: React.Ref<HTMLImageElement>;
|
||||||
|
@ -21,9 +21,10 @@ type ParametersAccordionsType = {
|
|||||||
const ParametersAccordion = (props: ParametersAccordionsType) => {
|
const ParametersAccordion = (props: ParametersAccordionsType) => {
|
||||||
const { accordionInfo } = props;
|
const { accordionInfo } = props;
|
||||||
|
|
||||||
const openAccordions = useAppSelector(
|
const { system, ui } = useAppSelector((state: RootState) => state);
|
||||||
(state: RootState) => state.system.openAccordions
|
|
||||||
);
|
const { openAccordions } = system;
|
||||||
|
const { disabledParameterPanels } = ui;
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@ -39,6 +40,9 @@ const ParametersAccordion = (props: ParametersAccordionsType) => {
|
|||||||
Object.keys(accordionInfo).forEach((key) => {
|
Object.keys(accordionInfo).forEach((key) => {
|
||||||
const { header, feature, content, additionalHeaderComponents } =
|
const { header, feature, content, additionalHeaderComponents } =
|
||||||
accordionInfo[key];
|
accordionInfo[key];
|
||||||
|
|
||||||
|
// do not render if panel is disabled in global state
|
||||||
|
if (disabledParameterPanels.indexOf(key) === -1) {
|
||||||
accordionsToRender.push(
|
accordionsToRender.push(
|
||||||
<InvokeAccordionItem
|
<InvokeAccordionItem
|
||||||
key={key}
|
key={key}
|
||||||
@ -48,6 +52,7 @@ const ParametersAccordion = (props: ParametersAccordionsType) => {
|
|||||||
additionalHeaderComponents={additionalHeaderComponents}
|
additionalHeaderComponents={additionalHeaderComponents}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return accordionsToRender;
|
return accordionsToRender;
|
||||||
|
@ -11,6 +11,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
|||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaPlay } from 'react-icons/fa';
|
import { FaPlay } from 'react-icons/fa';
|
||||||
|
import { createSession } from 'services/thunks/session';
|
||||||
|
|
||||||
interface InvokeButton
|
interface InvokeButton
|
||||||
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
|
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
|
||||||
@ -24,7 +25,8 @@ export default function InvokeButton(props: InvokeButton) {
|
|||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
|
|
||||||
const handleClickGenerate = () => {
|
const handleClickGenerate = () => {
|
||||||
dispatch(generateImage(activeTabName));
|
// dispatch(generateImage(activeTabName));
|
||||||
|
dispatch(createSession());
|
||||||
};
|
};
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { RootState } from 'app/store';
|
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';
|
import { isEqual } from 'lodash';
|
||||||
|
|
||||||
export const generationSelector = (state: RootState) => state.generation;
|
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;
|
height: number;
|
||||||
img2imgStrength: number;
|
img2imgStrength: number;
|
||||||
infillMethod: string;
|
infillMethod: string;
|
||||||
initialImage?: InvokeAI.Image | string; // can be an Image or url
|
initialImage?: InvokeAI._Image | string; // can be an Image or url
|
||||||
iterations: number;
|
iterations: number;
|
||||||
maskPath: string;
|
maskPath: string;
|
||||||
perlin: number;
|
perlin: number;
|
||||||
@ -317,12 +317,12 @@ export const generationSlice = createSlice({
|
|||||||
setShouldRandomizeSeed: (state, action: PayloadAction<boolean>) => {
|
setShouldRandomizeSeed: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldRandomizeSeed = action.payload;
|
state.shouldRandomizeSeed = action.payload;
|
||||||
},
|
},
|
||||||
setInitialImage: (
|
// setInitialImage: (
|
||||||
state,
|
// state,
|
||||||
action: PayloadAction<InvokeAI.Image | string>
|
// action: PayloadAction<InvokeAI._Image | string>
|
||||||
) => {
|
// ) => {
|
||||||
state.initialImage = action.payload;
|
// state.initialImage = action.payload;
|
||||||
},
|
// },
|
||||||
clearInitialImage: (state) => {
|
clearInitialImage: (state) => {
|
||||||
state.initialImage = undefined;
|
state.initialImage = undefined;
|
||||||
},
|
},
|
||||||
@ -353,6 +353,9 @@ export const generationSlice = createSlice({
|
|||||||
setVerticalSymmetrySteps: (state, action: PayloadAction<number>) => {
|
setVerticalSymmetrySteps: (state, action: PayloadAction<number>) => {
|
||||||
state.verticalSymmetrySteps = action.payload;
|
state.verticalSymmetrySteps = action.payload;
|
||||||
},
|
},
|
||||||
|
initialImageSelected: (state, action: PayloadAction<string>) => {
|
||||||
|
state.initialImage = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -368,7 +371,7 @@ export const {
|
|||||||
setHeight,
|
setHeight,
|
||||||
setImg2imgStrength,
|
setImg2imgStrength,
|
||||||
setInfillMethod,
|
setInfillMethod,
|
||||||
setInitialImage,
|
// setInitialImage,
|
||||||
setIterations,
|
setIterations,
|
||||||
setMaskPath,
|
setMaskPath,
|
||||||
setParameter,
|
setParameter,
|
||||||
@ -394,6 +397,7 @@ export const {
|
|||||||
setShouldUseSymmetry,
|
setShouldUseSymmetry,
|
||||||
setHorizontalSymmetrySteps,
|
setHorizontalSymmetrySteps,
|
||||||
setVerticalSymmetrySteps,
|
setVerticalSymmetrySteps,
|
||||||
|
initialImageSelected,
|
||||||
} = generationSlice.actions;
|
} = generationSlice.actions;
|
||||||
|
|
||||||
export default generationSlice.reducer;
|
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 { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||||
import { toastQueueSelector } from 'features/system/store/systemSelectors';
|
import { toastQueueSelector } from 'features/system/store/systemSelectors';
|
||||||
import { clearToastQueue } from 'features/system/store/systemSlice';
|
import { clearToastQueue } from 'features/system/store/systemSlice';
|
||||||
import { useEffect } from 'react';
|
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 useToastWatcher = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const toastQueue = useAppSelector(toastQueueSelector);
|
const toastQueue = useAppSelector(toastQueueSelector);
|
||||||
|
@ -2,7 +2,20 @@ import { ExpandedIndex, UseToastOptions } from '@chakra-ui/react';
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import * as InvokeAI from 'app/invokeai';
|
import * as InvokeAI from 'app/invokeai';
|
||||||
|
import {
|
||||||
|
generatorProgress,
|
||||||
|
invocationComplete,
|
||||||
|
invocationError,
|
||||||
|
invocationStarted,
|
||||||
|
socketConnected,
|
||||||
|
socketDisconnected,
|
||||||
|
} from 'services/events/actions';
|
||||||
|
|
||||||
import i18n from 'i18n';
|
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';
|
export type LogLevel = 'info' | 'warning' | 'error';
|
||||||
|
|
||||||
@ -56,6 +69,10 @@ export interface SystemState
|
|||||||
cancelType: CancelType;
|
cancelType: CancelType;
|
||||||
cancelAfter: number | null;
|
cancelAfter: number | null;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* The current progress image
|
||||||
|
*/
|
||||||
|
progressImage: ProgressImage | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialSystemState: SystemState = {
|
const initialSystemState: SystemState = {
|
||||||
@ -98,6 +115,7 @@ const initialSystemState: SystemState = {
|
|||||||
cancelType: 'immediate',
|
cancelType: 'immediate',
|
||||||
cancelAfter: null,
|
cancelAfter: null,
|
||||||
},
|
},
|
||||||
|
progressImage: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const systemSlice = createSlice({
|
export const systemSlice = createSlice({
|
||||||
@ -272,6 +290,111 @@ export const systemSlice = createSlice({
|
|||||||
state.cancelOptions.cancelAfter = action.payload;
|
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 {
|
export const {
|
||||||
|
@ -45,7 +45,8 @@ const tabIconStyles: ChakraProps['sx'] = {
|
|||||||
boxSize: 6,
|
boxSize: 6,
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabInfo: InvokeTabInfo[] = [
|
const buildTabs = (disabledTabs: InvokeTabName[]): InvokeTabInfo[] => {
|
||||||
|
const tabs: InvokeTabInfo[] = [
|
||||||
{
|
{
|
||||||
id: 'txt2img',
|
id: 'txt2img',
|
||||||
icon: <Icon as={MdTextFields} sx={tabIconStyles} />,
|
icon: <Icon as={MdTextFields} sx={tabIconStyles} />,
|
||||||
@ -77,6 +78,8 @@ const tabInfo: InvokeTabInfo[] = [
|
|||||||
workarea: <TrainingWIP />,
|
workarea: <TrainingWIP />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
return tabs.filter((tab) => !disabledTabs.includes(tab.id));
|
||||||
|
};
|
||||||
|
|
||||||
export default function InvokeTabs() {
|
export default function InvokeTabs() {
|
||||||
const activeTab = useAppSelector(activeTabIndexSelector);
|
const activeTab = useAppSelector(activeTabIndexSelector);
|
||||||
@ -85,13 +88,10 @@ export default function InvokeTabs() {
|
|||||||
(state: RootState) => state.lightbox.isLightboxOpen
|
(state: RootState) => state.lightbox.isLightboxOpen
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldPinGallery = useAppSelector(
|
const { shouldPinGallery, disabledTabs, shouldPinParametersPanel } =
|
||||||
(state: RootState) => state.ui.shouldPinGallery
|
useAppSelector((state: RootState) => state.ui);
|
||||||
);
|
|
||||||
|
|
||||||
const shouldPinParametersPanel = useAppSelector(
|
const activeTabs = buildTabs(disabledTabs);
|
||||||
(state: RootState) => state.ui.shouldPinParametersPanel
|
|
||||||
);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -142,7 +142,7 @@ export default function InvokeTabs() {
|
|||||||
|
|
||||||
const tabs = useMemo(
|
const tabs = useMemo(
|
||||||
() =>
|
() =>
|
||||||
tabInfo.map((tab) => (
|
activeTabs.map((tab) => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
hasArrow
|
hasArrow
|
||||||
@ -157,13 +157,13 @@ export default function InvokeTabs() {
|
|||||||
</Tab>
|
</Tab>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)),
|
)),
|
||||||
[t]
|
[t, activeTabs]
|
||||||
);
|
);
|
||||||
|
|
||||||
const tabPanels = useMemo(
|
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 (
|
return (
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Box, BoxProps, Flex } from '@chakra-ui/react';
|
import { Box, BoxProps, Flex } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||||
import { setInitialImage } from 'features/parameters/store/generationSlice';
|
import { initialImageSelected } from 'features/parameters/store/generationSlice';
|
||||||
import {
|
import {
|
||||||
activeTabNameSelector,
|
activeTabNameSelector,
|
||||||
uiSelector,
|
uiSelector,
|
||||||
@ -47,7 +47,7 @@ const InvokeWorkarea = (props: InvokeWorkareaProps) => {
|
|||||||
const image = getImageByUuid(uuid);
|
const image = getImageByUuid(uuid);
|
||||||
if (!image) return;
|
if (!image) return;
|
||||||
if (activeTabName === 'img2img') {
|
if (activeTabName === 'img2img') {
|
||||||
dispatch(setInitialImage(image));
|
dispatch(initialImageSelected(image.uuid));
|
||||||
} else if (activeTabName === 'unifiedCanvas') {
|
} else if (activeTabName === 'unifiedCanvas') {
|
||||||
dispatch(setInitialCanvasImage(image));
|
dispatch(setInitialCanvasImage(image));
|
||||||
}
|
}
|
||||||
|
@ -96,7 +96,6 @@ const ParametersPanel = ({ children }: ParametersPanelProps) => {
|
|||||||
onClose={closeParametersPanel}
|
onClose={closeParametersPanel}
|
||||||
isPinned={shouldPinParametersPanel || isLightboxOpen}
|
isPinned={shouldPinParametersPanel || isLightboxOpen}
|
||||||
sx={{
|
sx={{
|
||||||
borderColor: 'base.700',
|
|
||||||
p: shouldPinParametersPanel ? 0 : 4,
|
p: shouldPinParametersPanel ? 0 : 4,
|
||||||
bg: 'base.900',
|
bg: 'base.900',
|
||||||
}}
|
}}
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import { Flex, Image, Text, useToast } from '@chakra-ui/react';
|
import { Flex, Image, Text, useToast } from '@chakra-ui/react';
|
||||||
import { RootState } from 'app/store';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||||
import ImageUploaderIconButton from 'common/components/ImageUploaderIconButton';
|
import ImageUploaderIconButton from 'common/components/ImageUploaderIconButton';
|
||||||
|
import { initialImageSelector } from 'features/parameters/store/generationSelectors';
|
||||||
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export default function InitImagePreview() {
|
export default function InitImagePreview() {
|
||||||
const initialImage = useAppSelector(
|
const initialImage = useAppSelector(initialImageSelector);
|
||||||
(state: RootState) => state.generation.initialImage
|
|
||||||
);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
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 type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } 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 { InvokeTabName, tabMap } from './tabMap';
|
||||||
import { AddNewModelType, UIState } from './uiTypes';
|
import { AddNewModelType, UIState } from './uiTypes';
|
||||||
|
|
||||||
@ -16,6 +18,8 @@ const initialtabsState: UIState = {
|
|||||||
addNewModelUIOption: null,
|
addNewModelUIOption: null,
|
||||||
shouldPinGallery: true,
|
shouldPinGallery: true,
|
||||||
shouldShowGallery: true,
|
shouldShowGallery: true,
|
||||||
|
disabledParameterPanels: [],
|
||||||
|
disabledTabs: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: UIState = initialtabsState;
|
const initialState: UIState = initialtabsState;
|
||||||
@ -25,11 +29,7 @@ export const uiSlice = createSlice({
|
|||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setActiveTab: (state, action: PayloadAction<number | InvokeTabName>) => {
|
setActiveTab: (state, action: PayloadAction<number | InvokeTabName>) => {
|
||||||
if (typeof action.payload === 'number') {
|
setActiveTabReducer(state, action.payload);
|
||||||
state.activeTab = action.payload;
|
|
||||||
} else {
|
|
||||||
state.activeTab = tabMap.indexOf(action.payload);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
setCurrentTheme: (state, action: PayloadAction<string>) => {
|
setCurrentTheme: (state, action: PayloadAction<string>) => {
|
||||||
state.currentTheme = action.payload;
|
state.currentTheme = action.payload;
|
||||||
@ -92,6 +92,19 @@ export const uiSlice = createSlice({
|
|||||||
state.shouldShowParametersPanel = true;
|
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,
|
togglePinParametersPanel,
|
||||||
toggleParametersPanel,
|
toggleParametersPanel,
|
||||||
toggleGalleryPanel,
|
toggleGalleryPanel,
|
||||||
|
setDisabledPanels,
|
||||||
|
setDisabledTabs,
|
||||||
} = uiSlice.actions;
|
} = uiSlice.actions;
|
||||||
|
|
||||||
export default uiSlice.reducer;
|
export default uiSlice.reducer;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { InvokeTabName } from './tabMap';
|
||||||
|
|
||||||
export type AddNewModelType = 'ckpt' | 'diffusers' | null;
|
export type AddNewModelType = 'ckpt' | 'diffusers' | null;
|
||||||
|
|
||||||
export interface UIState {
|
export interface UIState {
|
||||||
@ -13,4 +15,6 @@ export interface UIState {
|
|||||||
addNewModelUIOption: AddNewModelType;
|
addNewModelUIOption: AddNewModelType;
|
||||||
shouldPinGallery: boolean;
|
shouldPinGallery: boolean;
|
||||||
shouldShowGallery: 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