diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 0615ff187e..920181ff8b 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -1,5 +1,6 @@ import io -from fastapi import HTTPException, Path, Query, Request, Response, UploadFile +from typing import Optional +from fastapi import Body, HTTPException, Path, Query, Request, Response, UploadFile from fastapi.routing import APIRouter from fastapi.responses import FileResponse from PIL import Image @@ -7,7 +8,11 @@ from invokeai.app.models.image import ( ImageCategory, ImageType, ) -from invokeai.app.services.models.image_record import ImageDTO, ImageUrlsDTO +from invokeai.app.services.models.image_record import ( + ImageDTO, + ImageRecordChanges, + ImageUrlsDTO, +) from invokeai.app.services.item_storage import PaginatedResults from ..dependencies import ApiDependencies @@ -27,10 +32,17 @@ images_router = APIRouter(prefix="/v1/images", tags=["images"]) ) async def upload_image( file: UploadFile, - image_type: ImageType, request: Request, response: Response, - image_category: ImageCategory = ImageCategory.GENERAL, + image_category: ImageCategory = Query( + default=ImageCategory.GENERAL, description="The category of the image" + ), + is_intermediate: bool = Query( + default=False, description="Whether this is an intermediate image" + ), + session_id: Optional[str] = Query( + default=None, description="The session ID associated with this upload, if any" + ), ) -> ImageDTO: """Uploads an image""" if not file.content_type.startswith("image"): @@ -46,9 +58,11 @@ async def upload_image( try: image_dto = ApiDependencies.invoker.services.images.create( - pil_image, - image_type, - image_category, + image=pil_image, + image_type=ImageType.UPLOAD, + image_category=image_category, + session_id=session_id, + is_intermediate=is_intermediate, ) response.status_code = 201 @@ -61,7 +75,7 @@ async def upload_image( @images_router.delete("/{image_type}/{image_name}", operation_id="delete_image") async def delete_image( - image_type: ImageType = Query(description="The type of image to delete"), + image_type: ImageType = Path(description="The type of image to delete"), image_name: str = Path(description="The name of the image to delete"), ) -> None: """Deletes an image""" @@ -73,6 +87,28 @@ async def delete_image( pass +@images_router.patch( + "/{image_type}/{image_name}", + operation_id="update_image", + response_model=ImageDTO, +) +async def update_image( + image_type: ImageType = Path(description="The type of image to update"), + image_name: str = Path(description="The name of the image to update"), + image_changes: ImageRecordChanges = Body( + description="The changes to apply to the image" + ), +) -> ImageDTO: + """Updates an image""" + + try: + return ApiDependencies.invoker.services.images.update( + image_type, image_name, image_changes + ) + except Exception as e: + raise HTTPException(status_code=400, detail="Failed to update image") + + @images_router.get( "/{image_type}/{image_name}/metadata", operation_id="get_image_metadata", @@ -85,9 +121,7 @@ async def get_image_metadata( """Gets an image's metadata""" try: - return ApiDependencies.invoker.services.images.get_dto( - image_type, image_name - ) + return ApiDependencies.invoker.services.images.get_dto(image_type, image_name) except Exception as e: raise HTTPException(status_code=404) @@ -113,9 +147,7 @@ async def get_image_full( """Gets a full-resolution image file""" try: - path = ApiDependencies.invoker.services.images.get_path( - image_type, image_name - ) + path = ApiDependencies.invoker.services.images.get_path(image_type, image_name) if not ApiDependencies.invoker.services.images.validate_path(path): raise HTTPException(status_code=404) diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index da61641105..5f75cfd3b8 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -78,6 +78,7 @@ class BaseInvocation(ABC, BaseModel): #fmt: off id: str = Field(description="The id of this node. Must be unique among all nodes.") + is_intermediate: bool = Field(default=False, description="Whether or not this node is an intermediate node.") #fmt: on diff --git a/invokeai/app/invocations/cv.py b/invokeai/app/invocations/cv.py index 26e06a2af8..5e9fe088b5 100644 --- a/invokeai/app/invocations/cv.py +++ b/invokeai/app/invocations/cv.py @@ -57,10 +57,11 @@ class CvInpaintInvocation(BaseInvocation, CvInvocationConfig): image_dto = context.services.images.create( image=image_inpainted, - image_type=ImageType.INTERMEDIATE, + image_type=ImageType.RESULT, image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, + is_intermediate=self.is_intermediate, ) return ImageOutput( diff --git a/invokeai/app/invocations/generate.py b/invokeai/app/invocations/generate.py index aa16243093..44280c3b41 100644 --- a/invokeai/app/invocations/generate.py +++ b/invokeai/app/invocations/generate.py @@ -56,7 +56,7 @@ class TextToImageInvocation(BaseInvocation, SDImageInvocation): width: int = Field(default=512, multiple_of=8, gt=0, description="The width of the resulting image", ) height: int = Field(default=512, multiple_of=8, gt=0, description="The height of the resulting image", ) cfg_scale: float = Field(default=7.5, ge=1, description="The Classifier-Free Guidance, higher values may result in a result closer to the prompt", ) - scheduler: SAMPLER_NAME_VALUES = Field(default="lms", description="The scheduler to use" ) + scheduler: SAMPLER_NAME_VALUES = Field(default="euler", description="The scheduler to use" ) model: str = Field(default="", description="The model to use (currently ignored)") # fmt: on @@ -101,6 +101,7 @@ class TextToImageInvocation(BaseInvocation, SDImageInvocation): image_category=ImageCategory.GENERAL, session_id=context.graph_execution_state_id, node_id=self.id, + is_intermediate=self.is_intermediate, ) return ImageOutput( @@ -181,6 +182,7 @@ class ImageToImageInvocation(TextToImageInvocation): image_category=ImageCategory.GENERAL, session_id=context.graph_execution_state_id, node_id=self.id, + is_intermediate=self.is_intermediate, ) return ImageOutput( @@ -296,6 +298,7 @@ class InpaintInvocation(ImageToImageInvocation): image_category=ImageCategory.GENERAL, session_id=context.graph_execution_state_id, node_id=self.id, + is_intermediate=self.is_intermediate, ) return ImageOutput( diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 21dfb4c1cd..69d51e6158 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -143,6 +143,7 @@ class ImageCropInvocation(BaseInvocation, PILInvocationConfig): image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, + is_intermediate=self.is_intermediate, ) return ImageOutput( @@ -204,6 +205,7 @@ class ImagePasteInvocation(BaseInvocation, PILInvocationConfig): image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, + is_intermediate=self.is_intermediate, ) return ImageOutput( @@ -242,6 +244,7 @@ class MaskFromAlphaInvocation(BaseInvocation, PILInvocationConfig): image_category=ImageCategory.MASK, node_id=self.id, session_id=context.graph_execution_state_id, + is_intermediate=self.is_intermediate, ) return MaskOutput( @@ -280,6 +283,7 @@ class ImageMultiplyInvocation(BaseInvocation, PILInvocationConfig): image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, + is_intermediate=self.is_intermediate, ) return ImageOutput( @@ -318,6 +322,7 @@ class ImageChannelInvocation(BaseInvocation, PILInvocationConfig): image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, + is_intermediate=self.is_intermediate, ) return ImageOutput( @@ -356,6 +361,7 @@ class ImageConvertInvocation(BaseInvocation, PILInvocationConfig): image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, + is_intermediate=self.is_intermediate, ) return ImageOutput( @@ -397,6 +403,7 @@ class ImageBlurInvocation(BaseInvocation, PILInvocationConfig): image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, + is_intermediate=self.is_intermediate, ) return ImageOutput( @@ -437,6 +444,7 @@ class ImageLerpInvocation(BaseInvocation, PILInvocationConfig): image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, + is_intermediate=self.is_intermediate, ) return ImageOutput( @@ -482,6 +490,7 @@ class ImageInverseLerpInvocation(BaseInvocation, PILInvocationConfig): image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, + is_intermediate=self.is_intermediate, ) return ImageOutput( diff --git a/invokeai/app/invocations/infill.py b/invokeai/app/invocations/infill.py index 17a43dbdac..ad60b62633 100644 --- a/invokeai/app/invocations/infill.py +++ b/invokeai/app/invocations/infill.py @@ -149,6 +149,7 @@ class InfillColorInvocation(BaseInvocation): image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, + is_intermediate=self.is_intermediate, ) return ImageOutput( @@ -193,6 +194,7 @@ class InfillTileInvocation(BaseInvocation): image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, + is_intermediate=self.is_intermediate, ) return ImageOutput( @@ -230,6 +232,7 @@ class InfillPatchMatchInvocation(BaseInvocation): image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, + is_intermediate=self.is_intermediate, ) return ImageOutput( diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 34da76d39a..11ea7134bb 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -167,7 +167,7 @@ class TextToLatentsInvocation(BaseInvocation): noise: Optional[LatentsField] = Field(description="The noise to use") steps: int = Field(default=10, gt=0, description="The number of steps to use to generate the image") cfg_scale: float = Field(default=7.5, gt=0, description="The Classifier-Free Guidance, higher values may result in a result closer to the prompt", ) - scheduler: SAMPLER_NAME_VALUES = Field(default="lms", description="The scheduler to use" ) + scheduler: SAMPLER_NAME_VALUES = Field(default="euler", description="The scheduler to use" ) model: str = Field(default="", description="The model to use (currently ignored)") # seamless: bool = Field(default=False, description="Whether or not to generate an image that can tile without seams", ) # seamless_axes: str = Field(default="", description="The axes to tile the image on, 'x' and/or 'y'") @@ -370,6 +370,7 @@ class LatentsToImageInvocation(BaseInvocation): image_category=ImageCategory.GENERAL, session_id=context.graph_execution_state_id, node_id=self.id, + is_intermediate=self.is_intermediate ) return ImageOutput( diff --git a/invokeai/app/invocations/reconstruct.py b/invokeai/app/invocations/reconstruct.py index 024134cd46..db71e4201d 100644 --- a/invokeai/app/invocations/reconstruct.py +++ b/invokeai/app/invocations/reconstruct.py @@ -43,10 +43,11 @@ class RestoreFaceInvocation(BaseInvocation): # TODO: can this return multiple results? image_dto = context.services.images.create( image=results[0][0], - image_type=ImageType.INTERMEDIATE, + image_type=ImageType.RESULT, image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, + is_intermediate=self.is_intermediate, ) return ImageOutput( diff --git a/invokeai/app/invocations/upscale.py b/invokeai/app/invocations/upscale.py index 75aeec784f..90c9e4bf4f 100644 --- a/invokeai/app/invocations/upscale.py +++ b/invokeai/app/invocations/upscale.py @@ -49,6 +49,7 @@ class UpscaleInvocation(BaseInvocation): image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, + is_intermediate=self.is_intermediate, ) return ImageOutput( diff --git a/invokeai/app/models/image.py b/invokeai/app/models/image.py index 544951ea34..46b50145aa 100644 --- a/invokeai/app/models/image.py +++ b/invokeai/app/models/image.py @@ -10,7 +10,6 @@ class ImageType(str, Enum, metaclass=MetaEnum): RESULT = "results" UPLOAD = "uploads" - INTERMEDIATE = "intermediates" class InvalidImageTypeException(ValueError): diff --git a/invokeai/app/services/image_record_storage.py b/invokeai/app/services/image_record_storage.py index 4e1f73978b..188a411a6b 100644 --- a/invokeai/app/services/image_record_storage.py +++ b/invokeai/app/services/image_record_storage.py @@ -12,6 +12,7 @@ from invokeai.app.models.image import ( ) from invokeai.app.services.models.image_record import ( ImageRecord, + ImageRecordChanges, deserialize_image_record, ) from invokeai.app.services.item_storage import PaginatedResults @@ -49,6 +50,16 @@ class ImageRecordStorageBase(ABC): """Gets an image record.""" pass + @abstractmethod + def update( + self, + image_name: str, + image_type: ImageType, + changes: ImageRecordChanges, + ) -> None: + """Updates an image record.""" + pass + @abstractmethod def get_many( self, @@ -78,6 +89,7 @@ class ImageRecordStorageBase(ABC): session_id: Optional[str], node_id: Optional[str], metadata: Optional[ImageMetadata], + is_intermediate: bool = False, ) -> datetime: """Saves an image record.""" pass @@ -125,6 +137,7 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): session_id TEXT, node_id TEXT, metadata TEXT, + is_intermediate BOOLEAN DEFAULT FALSE, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Updated via trigger updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -193,6 +206,42 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): return deserialize_image_record(dict(result)) + def update( + self, + image_name: str, + image_type: ImageType, + changes: ImageRecordChanges, + ) -> None: + try: + self._lock.acquire() + # Change the category of the image + if changes.image_category is not None: + self._cursor.execute( + f"""--sql + UPDATE images + SET image_category = ? + WHERE image_name = ?; + """, + (changes.image_category, image_name), + ) + + # Change the session associated with the image + if changes.session_id is not None: + self._cursor.execute( + f"""--sql + UPDATE images + SET session_id = ? + WHERE image_name = ?; + """, + (changes.session_id, image_name), + ) + self._conn.commit() + except sqlite3.Error as e: + self._conn.rollback() + raise ImageRecordSaveException from e + finally: + self._lock.release() + def get_many( self, image_type: ImageType, @@ -265,6 +314,7 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): height: int, node_id: Optional[str], metadata: Optional[ImageMetadata], + is_intermediate: bool = False, ) -> datetime: try: metadata_json = ( @@ -281,9 +331,10 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): height, node_id, session_id, - metadata + metadata, + is_intermediate ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?); + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); """, ( image_name, @@ -294,6 +345,7 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): node_id, session_id, metadata_json, + is_intermediate, ), ) self._conn.commit() diff --git a/invokeai/app/services/images.py b/invokeai/app/services/images.py index 914dd3b6d3..d0f7236fe2 100644 --- a/invokeai/app/services/images.py +++ b/invokeai/app/services/images.py @@ -20,6 +20,7 @@ from invokeai.app.services.image_record_storage import ( from invokeai.app.services.models.image_record import ( ImageRecord, ImageDTO, + ImageRecordChanges, image_record_to_dto, ) from invokeai.app.services.image_file_storage import ( @@ -31,7 +32,6 @@ from invokeai.app.services.image_file_storage import ( from invokeai.app.services.item_storage import ItemStorageABC, PaginatedResults from invokeai.app.services.metadata import MetadataServiceBase from invokeai.app.services.urls import UrlServiceBase -from invokeai.app.util.misc import get_iso_timestamp if TYPE_CHECKING: from invokeai.app.services.graph import GraphExecutionState @@ -48,11 +48,21 @@ class ImageServiceABC(ABC): image_category: ImageCategory, node_id: Optional[str] = None, session_id: Optional[str] = None, - metadata: Optional[ImageMetadata] = None, + intermediate: bool = False, ) -> ImageDTO: """Creates an image, storing the file and its metadata.""" pass + @abstractmethod + def update( + self, + image_type: ImageType, + image_name: str, + changes: ImageRecordChanges, + ) -> ImageDTO: + """Updates an image.""" + pass + @abstractmethod def get_pil_image(self, image_type: ImageType, image_name: str) -> PILImageType: """Gets an image as a PIL image.""" @@ -157,6 +167,7 @@ class ImageService(ImageServiceABC): image_category: ImageCategory, node_id: Optional[str] = None, session_id: Optional[str] = None, + is_intermediate: bool = False, ) -> ImageDTO: if image_type not in ImageType: raise InvalidImageTypeException @@ -184,6 +195,8 @@ class ImageService(ImageServiceABC): image_category=image_category, width=width, height=height, + # Meta fields + is_intermediate=is_intermediate, # Nullable fields node_id=node_id, session_id=session_id, @@ -217,6 +230,7 @@ class ImageService(ImageServiceABC): created_at=created_at, updated_at=created_at, # this is always the same as the created_at at this time deleted_at=None, + is_intermediate=is_intermediate, # Extra non-nullable fields for DTO image_url=image_url, thumbnail_url=thumbnail_url, @@ -231,6 +245,23 @@ class ImageService(ImageServiceABC): self._services.logger.error("Problem saving image record and file") raise e + def update( + self, + image_type: ImageType, + image_name: str, + changes: ImageRecordChanges, + ) -> ImageDTO: + try: + self._services.records.update(image_name, image_type, changes) + return self.get_dto(image_type, image_name) + except ImageRecordSaveException: + self._services.logger.error("Failed to update image record") + raise + except Exception as e: + self._services.logger.error("Problem updating image record") + raise e + + def get_pil_image(self, image_type: ImageType, image_name: str) -> PILImageType: try: return self._services.files.get(image_type, image_name) diff --git a/invokeai/app/services/models/image_record.py b/invokeai/app/services/models/image_record.py index c1155ff73e..26e4929be2 100644 --- a/invokeai/app/services/models/image_record.py +++ b/invokeai/app/services/models/image_record.py @@ -1,6 +1,6 @@ import datetime from typing import Optional, Union -from pydantic import BaseModel, Field +from pydantic import BaseModel, Extra, Field, StrictStr from invokeai.app.models.image import ImageCategory, ImageType from invokeai.app.models.metadata import ImageMetadata from invokeai.app.util.misc import get_iso_timestamp @@ -31,6 +31,8 @@ class ImageRecord(BaseModel): description="The deleted timestamp of the image." ) """The deleted timestamp of the image.""" + is_intermediate: bool = Field(description="Whether this is an intermediate image.") + """Whether this is an intermediate image.""" session_id: Optional[str] = Field( default=None, description="The session ID that generated this image, if it is a generated image.", @@ -48,6 +50,25 @@ class ImageRecord(BaseModel): """A limited subset of the image's generation metadata. Retrieve the image's session for full metadata.""" +class ImageRecordChanges(BaseModel, extra=Extra.forbid): + """A set of changes to apply to an image record. + + Only limited changes are valid: + - `image_category`: change the category of an image + - `session_id`: change the session associated with an image + """ + + image_category: Optional[ImageCategory] = Field( + description="The image's new category." + ) + """The image's new category.""" + session_id: Optional[StrictStr] = Field( + default=None, + description="The image's new session ID.", + ) + """The image's new session ID.""" + + class ImageUrlsDTO(BaseModel): """The URLs for an image and its thumbnail.""" @@ -95,6 +116,7 @@ def deserialize_image_record(image_dict: dict) -> ImageRecord: created_at = image_dict.get("created_at", get_iso_timestamp()) updated_at = image_dict.get("updated_at", get_iso_timestamp()) deleted_at = image_dict.get("deleted_at", get_iso_timestamp()) + is_intermediate = image_dict.get("is_intermediate", False) raw_metadata = image_dict.get("metadata") @@ -115,4 +137,5 @@ def deserialize_image_record(image_dict: dict) -> ImageRecord: created_at=created_at, updated_at=updated_at, deleted_at=deleted_at, + is_intermediate=is_intermediate, ) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index f23e83a191..1fbc2f978c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -8,9 +8,16 @@ import type { TypedStartListening, TypedAddListener } from '@reduxjs/toolkit'; import type { RootState, AppDispatch } from '../../store'; import { addInitialImageSelectedListener } from './listeners/initialImageSelected'; -import { addImageResultReceivedListener } from './listeners/invocationComplete'; -import { addImageUploadedListener } from './listeners/imageUploaded'; -import { addRequestedImageDeletionListener } from './listeners/imageDeleted'; +import { + addImageUploadedFulfilledListener, + addImageUploadedRejectedListener, +} from './listeners/imageUploaded'; +import { + addImageDeletedFulfilledListener, + addImageDeletedPendingListener, + addImageDeletedRejectedListener, + addRequestedImageDeletionListener, +} from './listeners/imageDeleted'; import { addUserInvokedCanvasListener } from './listeners/userInvokedCanvas'; import { addUserInvokedNodesListener } from './listeners/userInvokedNodes'; import { addUserInvokedTextToImageListener } from './listeners/userInvokedTextToImage'; @@ -19,6 +26,47 @@ import { addCanvasSavedToGalleryListener } from './listeners/canvasSavedToGaller import { addCanvasDownloadedAsImageListener } from './listeners/canvasDownloadedAsImage'; import { addCanvasCopiedToClipboardListener } from './listeners/canvasCopiedToClipboard'; import { addCanvasMergedListener } from './listeners/canvasMerged'; +import { addGeneratorProgressListener } from './listeners/socketio/generatorProgress'; +import { addGraphExecutionStateCompleteListener } from './listeners/socketio/graphExecutionStateComplete'; +import { addInvocationCompleteListener } from './listeners/socketio/invocationComplete'; +import { addInvocationErrorListener } from './listeners/socketio/invocationError'; +import { addInvocationStartedListener } from './listeners/socketio/invocationStarted'; +import { addSocketConnectedListener } from './listeners/socketio/socketConnected'; +import { addSocketDisconnectedListener } from './listeners/socketio/socketDisconnected'; +import { addSocketSubscribedListener } from './listeners/socketio/socketSubscribed'; +import { addSocketUnsubscribedListener } from './listeners/socketio/socketUnsubscribed'; +import { addSessionReadyToInvokeListener } from './listeners/sessionReadyToInvoke'; +import { + addImageMetadataReceivedFulfilledListener, + addImageMetadataReceivedRejectedListener, +} from './listeners/imageMetadataReceived'; +import { + addImageUrlsReceivedFulfilledListener, + addImageUrlsReceivedRejectedListener, +} from './listeners/imageUrlsReceived'; +import { + addSessionCreatedFulfilledListener, + addSessionCreatedPendingListener, + addSessionCreatedRejectedListener, +} from './listeners/sessionCreated'; +import { + addSessionInvokedFulfilledListener, + addSessionInvokedPendingListener, + addSessionInvokedRejectedListener, +} from './listeners/sessionInvoked'; +import { + addSessionCanceledFulfilledListener, + addSessionCanceledPendingListener, + addSessionCanceledRejectedListener, +} from './listeners/sessionCanceled'; +import { + addReceivedResultImagesPageFulfilledListener, + addReceivedResultImagesPageRejectedListener, +} from './listeners/receivedResultImagesPage'; +import { + addReceivedUploadImagesPageFulfilledListener, + addReceivedUploadImagesPageRejectedListener, +} from './listeners/receivedUploadImagesPage'; export const listenerMiddleware = createListenerMiddleware(); @@ -38,17 +86,67 @@ export type AppListenerEffect = ListenerEffect< AppDispatch >; -addImageUploadedListener(); -addInitialImageSelectedListener(); -addImageResultReceivedListener(); -addRequestedImageDeletionListener(); +// Image uploaded +addImageUploadedFulfilledListener(); +addImageUploadedRejectedListener(); +addInitialImageSelectedListener(); + +// Image deleted +addRequestedImageDeletionListener(); +addImageDeletedPendingListener(); +addImageDeletedFulfilledListener(); +addImageDeletedRejectedListener(); + +// Image metadata +addImageMetadataReceivedFulfilledListener(); +addImageMetadataReceivedRejectedListener(); + +// Image URLs +addImageUrlsReceivedFulfilledListener(); +addImageUrlsReceivedRejectedListener(); + +// User Invoked addUserInvokedCanvasListener(); addUserInvokedNodesListener(); addUserInvokedTextToImageListener(); addUserInvokedImageToImageListener(); +addSessionReadyToInvokeListener(); +// Canvas actions addCanvasSavedToGalleryListener(); addCanvasDownloadedAsImageListener(); addCanvasCopiedToClipboardListener(); addCanvasMergedListener(); + +// socketio +addGeneratorProgressListener(); +addGraphExecutionStateCompleteListener(); +addInvocationCompleteListener(); +addInvocationErrorListener(); +addInvocationStartedListener(); +addSocketConnectedListener(); +addSocketDisconnectedListener(); +addSocketSubscribedListener(); +addSocketUnsubscribedListener(); + +// Session Created +addSessionCreatedPendingListener(); +addSessionCreatedFulfilledListener(); +addSessionCreatedRejectedListener(); + +// Session Invoked +addSessionInvokedPendingListener(); +addSessionInvokedFulfilledListener(); +addSessionInvokedRejectedListener(); + +// Session Canceled +addSessionCanceledPendingListener(); +addSessionCanceledFulfilledListener(); +addSessionCanceledRejectedListener(); + +// Gallery pages +addReceivedResultImagesPageFulfilledListener(); +addReceivedResultImagesPageRejectedListener(); +addReceivedUploadImagesPageFulfilledListener(); +addReceivedUploadImagesPageRejectedListener(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts index 1e2d99541c..fbc9c9c225 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts @@ -52,7 +52,6 @@ export const addCanvasMergedListener = () => { dispatch( imageUploaded({ - imageType: 'intermediates', formData: { file: new File([blob], filename, { type: 'image/png' }), }, @@ -65,7 +64,7 @@ export const addCanvasMergedListener = () => { action.meta.arg.formData.file.name === filename ); - const mergedCanvasImage = payload.response; + const mergedCanvasImage = payload; dispatch( setMergedCanvas({ diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts index d8237d1d5c..2df3dacea2 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts @@ -29,7 +29,6 @@ export const addCanvasSavedToGalleryListener = () => { dispatch( imageUploaded({ - imageType: 'results', formData: { file: new File([blob], 'mergedCanvas.png', { type: 'image/png' }), }, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts index 42a62b3d80..cd4771b96a 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts @@ -4,9 +4,14 @@ import { imageDeleted } from 'services/thunks/image'; import { log } from 'app/logging/useLogger'; import { clamp } from 'lodash-es'; import { imageSelected } from 'features/gallery/store/gallerySlice'; +import { uploadsAdapter } from 'features/gallery/store/uploadsSlice'; +import { resultsAdapter } from 'features/gallery/store/resultsSlice'; const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' }); +/** + * Called when the user requests an image deletion + */ export const addRequestedImageDeletionListener = () => { startAppListening({ actionCreator: requestedImageDeletion, @@ -19,11 +24,6 @@ export const addRequestedImageDeletionListener = () => { const { image_name, image_type } = image; - if (image_type !== 'uploads' && image_type !== 'results') { - moduleLog.warn({ data: image }, `Invalid image type ${image_type}`); - return; - } - const selectedImageName = getState().gallery.selectedImage?.image_name; if (selectedImageName === image_name) { @@ -57,3 +57,49 @@ export const addRequestedImageDeletionListener = () => { }, }); }; + +/** + * Called when the actual delete request is sent to the server + */ +export const addImageDeletedPendingListener = () => { + startAppListening({ + actionCreator: imageDeleted.pending, + effect: (action, { dispatch, getState }) => { + const { imageName, imageType } = action.meta.arg; + // Preemptively remove the image from the gallery + if (imageType === 'uploads') { + uploadsAdapter.removeOne(getState().uploads, imageName); + } + if (imageType === 'results') { + resultsAdapter.removeOne(getState().results, imageName); + } + }, + }); +}; + +/** + * Called on successful delete + */ +export const addImageDeletedFulfilledListener = () => { + startAppListening({ + actionCreator: imageDeleted.fulfilled, + effect: (action, { dispatch, getState }) => { + moduleLog.debug({ data: { image: action.meta.arg } }, 'Image deleted'); + }, + }); +}; + +/** + * Called on failed delete + */ +export const addImageDeletedRejectedListener = () => { + startAppListening({ + actionCreator: imageDeleted.rejected, + effect: (action, { dispatch, getState }) => { + moduleLog.debug( + { data: { image: action.meta.arg } }, + 'Unable to delete image' + ); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts new file mode 100644 index 0000000000..c93ed2820f --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts @@ -0,0 +1,43 @@ +import { log } from 'app/logging/useLogger'; +import { startAppListening } from '..'; +import { imageMetadataReceived } from 'services/thunks/image'; +import { + ResultsImageDTO, + resultUpserted, +} from 'features/gallery/store/resultsSlice'; +import { + UploadsImageDTO, + uploadUpserted, +} from 'features/gallery/store/uploadsSlice'; + +const moduleLog = log.child({ namespace: 'image' }); + +export const addImageMetadataReceivedFulfilledListener = () => { + startAppListening({ + actionCreator: imageMetadataReceived.fulfilled, + effect: (action, { getState, dispatch }) => { + const image = action.payload; + moduleLog.debug({ data: { image } }, 'Image metadata received'); + + if (image.image_type === 'results') { + dispatch(resultUpserted(action.payload as ResultsImageDTO)); + } + + if (image.image_type === 'uploads') { + dispatch(uploadUpserted(action.payload as UploadsImageDTO)); + } + }, + }); +}; + +export const addImageMetadataReceivedRejectedListener = () => { + startAppListening({ + actionCreator: imageMetadataReceived.rejected, + effect: (action, { getState, dispatch }) => { + moduleLog.debug( + { data: { image: action.meta.arg } }, + 'Problem receiving image metadata' + ); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index 1d66166c12..5b177eae91 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -1,25 +1,31 @@ import { startAppListening } from '..'; -import { uploadAdded } from 'features/gallery/store/uploadsSlice'; +import { uploadUpserted } from 'features/gallery/store/uploadsSlice'; import { imageSelected } from 'features/gallery/store/gallerySlice'; import { imageUploaded } from 'services/thunks/image'; import { addToast } from 'features/system/store/systemSlice'; import { initialImageSelected } from 'features/parameters/store/actions'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; -import { resultAdded } from 'features/gallery/store/resultsSlice'; +import { resultUpserted } from 'features/gallery/store/resultsSlice'; import { isResultsImageDTO, isUploadsImageDTO } from 'services/types/guards'; +import { log } from 'app/logging/useLogger'; -export const addImageUploadedListener = () => { +const moduleLog = log.child({ namespace: 'image' }); + +export const addImageUploadedFulfilledListener = () => { startAppListening({ predicate: (action): action is ReturnType => imageUploaded.fulfilled.match(action) && - action.payload.response.image_type !== 'intermediates', + action.payload.is_intermediate === false, effect: (action, { dispatch, getState }) => { - const { response: image } = action.payload; + const image = action.payload; + + moduleLog.debug({ arg: '', image }, 'Image uploaded'); const state = getState(); + // Handle uploads if (isUploadsImageDTO(image)) { - dispatch(uploadAdded(image)); + dispatch(uploadUpserted(image)); dispatch(addToast({ title: 'Image Uploaded', status: 'success' })); @@ -36,9 +42,26 @@ export const addImageUploadedListener = () => { } } + // Handle results + // TODO: Can this ever happen? I don't think so... if (isResultsImageDTO(image)) { - dispatch(resultAdded(image)); + dispatch(resultUpserted(image)); } }, }); }; + +export const addImageUploadedRejectedListener = () => { + startAppListening({ + actionCreator: imageUploaded.rejected, + effect: (action, { dispatch }) => { + dispatch( + addToast({ + title: 'Image Upload Failed', + description: action.error.message, + status: 'error', + }) + ); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUrlsReceived.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUrlsReceived.ts new file mode 100644 index 0000000000..4ff2a02118 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUrlsReceived.ts @@ -0,0 +1,51 @@ +import { log } from 'app/logging/useLogger'; +import { startAppListening } from '..'; +import { imageUrlsReceived } from 'services/thunks/image'; +import { resultsAdapter } from 'features/gallery/store/resultsSlice'; +import { uploadsAdapter } from 'features/gallery/store/uploadsSlice'; + +const moduleLog = log.child({ namespace: 'image' }); + +export const addImageUrlsReceivedFulfilledListener = () => { + startAppListening({ + actionCreator: imageUrlsReceived.fulfilled, + effect: (action, { getState, dispatch }) => { + const image = action.payload; + moduleLog.debug({ data: { image } }, 'Image URLs received'); + + const { image_type, image_name, image_url, thumbnail_url } = image; + + if (image_type === 'results') { + resultsAdapter.updateOne(getState().results, { + id: image_name, + changes: { + image_url, + thumbnail_url, + }, + }); + } + + if (image_type === 'uploads') { + uploadsAdapter.updateOne(getState().uploads, { + id: image_name, + changes: { + image_url, + thumbnail_url, + }, + }); + } + }, + }); +}; + +export const addImageUrlsReceivedRejectedListener = () => { + startAppListening({ + actionCreator: imageUrlsReceived.rejected, + effect: (action, { getState, dispatch }) => { + moduleLog.debug( + { data: { image: action.meta.arg } }, + 'Problem getting image URLs' + ); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts deleted file mode 100644 index 0222eea93c..0000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { invocationComplete } from 'services/events/actions'; -import { isImageOutput } from 'services/types/guards'; -import { - imageMetadataReceived, - imageUrlsReceived, -} from 'services/thunks/image'; -import { startAppListening } from '..'; -import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; - -const nodeDenylist = ['dataURL_image']; - -export const addImageResultReceivedListener = () => { - startAppListening({ - predicate: (action) => { - if ( - invocationComplete.match(action) && - isImageOutput(action.payload.data.result) - ) { - return true; - } - return false; - }, - effect: async (action, { getState, dispatch, take }) => { - if (!invocationComplete.match(action)) { - return; - } - - const { data } = action.payload; - const { result, node, graph_execution_state_id } = data; - - if (isImageOutput(result) && !nodeDenylist.includes(node.type)) { - const { image_name, image_type } = result.image; - - dispatch( - imageUrlsReceived({ imageName: image_name, imageType: image_type }) - ); - - dispatch( - imageMetadataReceived({ - imageName: image_name, - imageType: image_type, - }) - ); - - // Handle canvas image - if ( - graph_execution_state_id === - getState().canvas.layerState.stagingArea.sessionId - ) { - const [{ payload: image }] = await take( - ( - action - ): action is ReturnType => - imageMetadataReceived.fulfilled.match(action) && - action.payload.image_name === image_name - ); - dispatch(addImageToStagingArea(image)); - } - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedResultImagesPage.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedResultImagesPage.ts new file mode 100644 index 0000000000..bcdd11ef97 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedResultImagesPage.ts @@ -0,0 +1,33 @@ +import { log } from 'app/logging/useLogger'; +import { startAppListening } from '..'; +import { receivedResultImagesPage } from 'services/thunks/gallery'; +import { serializeError } from 'serialize-error'; + +const moduleLog = log.child({ namespace: 'gallery' }); + +export const addReceivedResultImagesPageFulfilledListener = () => { + startAppListening({ + actionCreator: receivedResultImagesPage.fulfilled, + effect: (action, { getState, dispatch }) => { + const page = action.payload; + moduleLog.debug( + { data: { page } }, + `Received ${page.items.length} results` + ); + }, + }); +}; + +export const addReceivedResultImagesPageRejectedListener = () => { + startAppListening({ + actionCreator: receivedResultImagesPage.rejected, + effect: (action, { getState, dispatch }) => { + if (action.payload) { + moduleLog.debug( + { data: { error: serializeError(action.payload.error) } }, + 'Problem receiving results' + ); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedUploadImagesPage.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedUploadImagesPage.ts new file mode 100644 index 0000000000..68813aae27 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedUploadImagesPage.ts @@ -0,0 +1,33 @@ +import { log } from 'app/logging/useLogger'; +import { startAppListening } from '..'; +import { receivedUploadImagesPage } from 'services/thunks/gallery'; +import { serializeError } from 'serialize-error'; + +const moduleLog = log.child({ namespace: 'gallery' }); + +export const addReceivedUploadImagesPageFulfilledListener = () => { + startAppListening({ + actionCreator: receivedUploadImagesPage.fulfilled, + effect: (action, { getState, dispatch }) => { + const page = action.payload; + moduleLog.debug( + { data: { page } }, + `Received ${page.items.length} uploads` + ); + }, + }); +}; + +export const addReceivedUploadImagesPageRejectedListener = () => { + startAppListening({ + actionCreator: receivedUploadImagesPage.rejected, + effect: (action, { getState, dispatch }) => { + if (action.payload) { + moduleLog.debug( + { data: { error: serializeError(action.payload.error) } }, + 'Problem receiving uploads' + ); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionCanceled.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionCanceled.ts new file mode 100644 index 0000000000..6274ad4dc8 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionCanceled.ts @@ -0,0 +1,48 @@ +import { log } from 'app/logging/useLogger'; +import { startAppListening } from '..'; +import { sessionCanceled } from 'services/thunks/session'; +import { serializeError } from 'serialize-error'; + +const moduleLog = log.child({ namespace: 'session' }); + +export const addSessionCanceledPendingListener = () => { + startAppListening({ + actionCreator: sessionCanceled.pending, + effect: (action, { getState, dispatch }) => { + // + }, + }); +}; + +export const addSessionCanceledFulfilledListener = () => { + startAppListening({ + actionCreator: sessionCanceled.fulfilled, + effect: (action, { getState, dispatch }) => { + const { sessionId } = action.meta.arg; + moduleLog.debug( + { data: { sessionId } }, + `Session canceled (${sessionId})` + ); + }, + }); +}; + +export const addSessionCanceledRejectedListener = () => { + startAppListening({ + actionCreator: sessionCanceled.rejected, + effect: (action, { getState, dispatch }) => { + if (action.payload) { + const { arg, error } = action.payload; + moduleLog.error( + { + data: { + arg, + error: serializeError(error), + }, + }, + `Problem canceling session` + ); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionCreated.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionCreated.ts new file mode 100644 index 0000000000..fb8a64d2e3 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionCreated.ts @@ -0,0 +1,45 @@ +import { log } from 'app/logging/useLogger'; +import { startAppListening } from '..'; +import { sessionCreated } from 'services/thunks/session'; +import { serializeError } from 'serialize-error'; + +const moduleLog = log.child({ namespace: 'session' }); + +export const addSessionCreatedPendingListener = () => { + startAppListening({ + actionCreator: sessionCreated.pending, + effect: (action, { getState, dispatch }) => { + // + }, + }); +}; + +export const addSessionCreatedFulfilledListener = () => { + startAppListening({ + actionCreator: sessionCreated.fulfilled, + effect: (action, { getState, dispatch }) => { + const session = action.payload; + moduleLog.debug({ data: { session } }, `Session created (${session.id})`); + }, + }); +}; + +export const addSessionCreatedRejectedListener = () => { + startAppListening({ + actionCreator: sessionCreated.rejected, + effect: (action, { getState, dispatch }) => { + if (action.payload) { + const { arg, error } = action.payload; + moduleLog.error( + { + data: { + arg, + error: serializeError(error), + }, + }, + `Problem creating session` + ); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionInvoked.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionInvoked.ts new file mode 100644 index 0000000000..272d1d9e1d --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionInvoked.ts @@ -0,0 +1,48 @@ +import { log } from 'app/logging/useLogger'; +import { startAppListening } from '..'; +import { sessionInvoked } from 'services/thunks/session'; +import { serializeError } from 'serialize-error'; + +const moduleLog = log.child({ namespace: 'session' }); + +export const addSessionInvokedPendingListener = () => { + startAppListening({ + actionCreator: sessionInvoked.pending, + effect: (action, { getState, dispatch }) => { + // + }, + }); +}; + +export const addSessionInvokedFulfilledListener = () => { + startAppListening({ + actionCreator: sessionInvoked.fulfilled, + effect: (action, { getState, dispatch }) => { + const { sessionId } = action.meta.arg; + moduleLog.debug( + { data: { sessionId } }, + `Session invoked (${sessionId})` + ); + }, + }); +}; + +export const addSessionInvokedRejectedListener = () => { + startAppListening({ + actionCreator: sessionInvoked.rejected, + effect: (action, { getState, dispatch }) => { + if (action.payload) { + const { arg, error } = action.payload; + moduleLog.error( + { + data: { + arg, + error: serializeError(error), + }, + }, + `Problem invoking session` + ); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionReadyToInvoke.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionReadyToInvoke.ts new file mode 100644 index 0000000000..8d4262e7da --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionReadyToInvoke.ts @@ -0,0 +1,22 @@ +import { startAppListening } from '..'; +import { sessionInvoked } from 'services/thunks/session'; +import { log } from 'app/logging/useLogger'; +import { sessionReadyToInvoke } from 'features/system/store/actions'; + +const moduleLog = log.child({ namespace: 'session' }); + +export const addSessionReadyToInvokeListener = () => { + startAppListening({ + actionCreator: sessionReadyToInvoke, + effect: (action, { getState, dispatch }) => { + const { sessionId } = getState().system; + if (sessionId) { + moduleLog.debug( + { sessionId }, + `Session ready to invoke (${sessionId})})` + ); + dispatch(sessionInvoked({ sessionId })); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/generatorProgress.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/generatorProgress.ts new file mode 100644 index 0000000000..341b5e46d3 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/generatorProgress.ts @@ -0,0 +1,28 @@ +import { startAppListening } from '../..'; +import { log } from 'app/logging/useLogger'; +import { generatorProgress } from 'services/events/actions'; + +const moduleLog = log.child({ namespace: 'socketio' }); + +export const addGeneratorProgressListener = () => { + startAppListening({ + actionCreator: generatorProgress, + effect: (action, { dispatch, getState }) => { + if ( + getState().system.canceledSession === + action.payload.data.graph_execution_state_id + ) { + moduleLog.trace( + action.payload, + 'Ignored generator progress for canceled session' + ); + return; + } + + moduleLog.trace( + action.payload, + `Generator progress (${action.payload.data.node.type})` + ); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/graphExecutionStateComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/graphExecutionStateComplete.ts new file mode 100644 index 0000000000..a66a7fb547 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/graphExecutionStateComplete.ts @@ -0,0 +1,17 @@ +import { log } from 'app/logging/useLogger'; +import { graphExecutionStateComplete } from 'services/events/actions'; +import { startAppListening } from '../..'; + +const moduleLog = log.child({ namespace: 'socketio' }); + +export const addGraphExecutionStateCompleteListener = () => { + startAppListening({ + actionCreator: graphExecutionStateComplete, + effect: (action, { dispatch, getState }) => { + moduleLog.debug( + action.payload, + `Session invocation complete (${action.payload.data.graph_execution_state_id})` + ); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/invocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/invocationComplete.ts new file mode 100644 index 0000000000..95e6d831c0 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/invocationComplete.ts @@ -0,0 +1,74 @@ +import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; +import { startAppListening } from '../..'; +import { log } from 'app/logging/useLogger'; +import { invocationComplete } from 'services/events/actions'; +import { imageMetadataReceived } from 'services/thunks/image'; +import { sessionCanceled } from 'services/thunks/session'; +import { isImageOutput } from 'services/types/guards'; +import { progressImageSet } from 'features/system/store/systemSlice'; +import { imageSelected } from 'features/gallery/store/gallerySlice'; + +const moduleLog = log.child({ namespace: 'socketio' }); +const nodeDenylist = ['dataURL_image']; + +export const addInvocationCompleteListener = () => { + startAppListening({ + actionCreator: invocationComplete, + effect: async (action, { dispatch, getState, take }) => { + moduleLog.debug( + action.payload, + `Invocation complete (${action.payload.data.node.type})` + ); + + const sessionId = action.payload.data.graph_execution_state_id; + + const { cancelType, isCancelScheduled } = getState().system; + + // Handle scheduled cancelation + if (cancelType === 'scheduled' && isCancelScheduled) { + dispatch(sessionCanceled({ sessionId })); + } + + const { data } = action.payload; + const { result, node, graph_execution_state_id } = data; + + // This complete event has an associated image output + if (isImageOutput(result) && !nodeDenylist.includes(node.type)) { + const { image_name, image_type } = result.image; + + // Get its metadata + dispatch( + imageMetadataReceived({ + imageName: image_name, + imageType: image_type, + }) + ); + + const [{ payload: imageDTO }] = await take( + imageMetadataReceived.fulfilled.match + ); + + if (getState().gallery.shouldAutoSwitchToNewImages) { + dispatch(imageSelected(imageDTO)); + } + + // Handle canvas image + if ( + graph_execution_state_id === + getState().canvas.layerState.stagingArea.sessionId + ) { + const [{ payload: image }] = await take( + ( + action + ): action is ReturnType => + imageMetadataReceived.fulfilled.match(action) && + action.payload.image_name === image_name + ); + dispatch(addImageToStagingArea(image)); + } + + dispatch(progressImageSet(null)); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/invocationError.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/invocationError.ts new file mode 100644 index 0000000000..3a98af120a --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/invocationError.ts @@ -0,0 +1,17 @@ +import { startAppListening } from '../..'; +import { log } from 'app/logging/useLogger'; +import { invocationError } from 'services/events/actions'; + +const moduleLog = log.child({ namespace: 'socketio' }); + +export const addInvocationErrorListener = () => { + startAppListening({ + actionCreator: invocationError, + effect: (action, { dispatch, getState }) => { + moduleLog.error( + action.payload, + `Invocation error (${action.payload.data.node.type})` + ); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/invocationStarted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/invocationStarted.ts new file mode 100644 index 0000000000..f898c62b23 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/invocationStarted.ts @@ -0,0 +1,28 @@ +import { startAppListening } from '../..'; +import { log } from 'app/logging/useLogger'; +import { invocationStarted } from 'services/events/actions'; + +const moduleLog = log.child({ namespace: 'socketio' }); + +export const addInvocationStartedListener = () => { + startAppListening({ + actionCreator: invocationStarted, + effect: (action, { dispatch, getState }) => { + if ( + getState().system.canceledSession === + action.payload.data.graph_execution_state_id + ) { + moduleLog.trace( + action.payload, + 'Ignored invocation started for canceled session' + ); + return; + } + + moduleLog.debug( + action.payload, + `Invocation started (${action.payload.data.node.type})` + ); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected.ts new file mode 100644 index 0000000000..bc9ecbec1e --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected.ts @@ -0,0 +1,43 @@ +import { startAppListening } from '../..'; +import { log } from 'app/logging/useLogger'; +import { socketConnected } from 'services/events/actions'; +import { + receivedResultImagesPage, + receivedUploadImagesPage, +} from 'services/thunks/gallery'; +import { receivedModels } from 'services/thunks/model'; +import { receivedOpenAPISchema } from 'services/thunks/schema'; + +const moduleLog = log.child({ namespace: 'socketio' }); + +export const addSocketConnectedListener = () => { + startAppListening({ + actionCreator: socketConnected, + effect: (action, { dispatch, getState }) => { + const { timestamp } = action.payload; + + moduleLog.debug({ timestamp }, 'Connected'); + + const { results, uploads, models, nodes, config } = getState(); + + const { disabledTabs } = config; + + // These thunks need to be dispatch in middleware; cannot handle in a reducer + if (!results.ids.length) { + dispatch(receivedResultImagesPage()); + } + + if (!uploads.ids.length) { + dispatch(receivedUploadImagesPage()); + } + + if (!models.ids.length) { + dispatch(receivedModels()); + } + + if (!nodes.schema && !disabledTabs.includes('nodes')) { + dispatch(receivedOpenAPISchema()); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketDisconnected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketDisconnected.ts new file mode 100644 index 0000000000..131c3ba18f --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketDisconnected.ts @@ -0,0 +1,14 @@ +import { startAppListening } from '../..'; +import { log } from 'app/logging/useLogger'; +import { socketDisconnected } from 'services/events/actions'; + +const moduleLog = log.child({ namespace: 'socketio' }); + +export const addSocketDisconnectedListener = () => { + startAppListening({ + actionCreator: socketDisconnected, + effect: (action, { dispatch, getState }) => { + moduleLog.debug(action.payload, 'Disconnected'); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketSubscribed.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketSubscribed.ts new file mode 100644 index 0000000000..400f8a1689 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketSubscribed.ts @@ -0,0 +1,17 @@ +import { startAppListening } from '../..'; +import { log } from 'app/logging/useLogger'; +import { socketSubscribed } from 'services/events/actions'; + +const moduleLog = log.child({ namespace: 'socketio' }); + +export const addSocketSubscribedListener = () => { + startAppListening({ + actionCreator: socketSubscribed, + effect: (action, { dispatch, getState }) => { + moduleLog.debug( + action.payload, + `Subscribed (${action.payload.sessionId}))` + ); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketUnsubscribed.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketUnsubscribed.ts new file mode 100644 index 0000000000..af15c55d42 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketUnsubscribed.ts @@ -0,0 +1,17 @@ +import { startAppListening } from '../..'; +import { log } from 'app/logging/useLogger'; +import { socketUnsubscribed } from 'services/events/actions'; + +const moduleLog = log.child({ namespace: 'socketio' }); + +export const addSocketUnsubscribedListener = () => { + startAppListening({ + actionCreator: socketUnsubscribed, + effect: (action, { dispatch, getState }) => { + moduleLog.debug( + action.payload, + `Unsubscribed (${action.payload.sessionId})` + ); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCanvas.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCanvas.ts index 2ebd3684e9..ae388b85cf 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCanvas.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCanvas.ts @@ -1,9 +1,9 @@ import { startAppListening } from '..'; -import { sessionCreated, sessionInvoked } from 'services/thunks/session'; +import { sessionCreated } from 'services/thunks/session'; import { buildCanvasGraphComponents } from 'features/nodes/util/graphBuilders/buildCanvasGraph'; import { log } from 'app/logging/useLogger'; import { canvasGraphBuilt } from 'features/nodes/store/actions'; -import { imageUploaded } from 'services/thunks/image'; +import { imageUpdated, imageUploaded } from 'services/thunks/image'; import { v4 as uuidv4 } from 'uuid'; import { Graph } from 'services/api'; import { @@ -15,12 +15,22 @@ import { getCanvasData } from 'features/canvas/util/getCanvasData'; import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode'; import { blobToDataURL } from 'features/canvas/util/blobToDataURL'; import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; +import { sessionReadyToInvoke } from 'features/system/store/actions'; const moduleLog = log.child({ namespace: 'invoke' }); /** - * This listener is responsible for building the canvas graph and blobs when the user invokes the canvas. - * It is also responsible for uploading the base and mask layers to the server. + * This listener is responsible invoking the canvas. This involves a number of steps: + * + * 1. Generate image blobs from the canvas layers + * 2. Determine the generation mode from the layers (txt2img, img2img, inpaint) + * 3. Build the canvas graph + * 4. Create the session with the graph + * 5. Upload the init image if necessary + * 6. Upload the mask image if necessary + * 7. Update the init and mask images with the session ID + * 8. Initialize the staging area if not yet initialized + * 9. Dispatch the sessionReadyToInvoke action to invoke the session */ export const addUserInvokedCanvasListener = () => { startAppListening({ @@ -70,63 +80,7 @@ export const addUserInvokedCanvasListener = () => { const { rangeNode, iterateNode, baseNode, edges } = graphComponents; - // Upload the base layer, to be used as init image - const baseFilename = `${uuidv4()}.png`; - - dispatch( - imageUploaded({ - imageType: 'intermediates', - formData: { - file: new File([baseBlob], baseFilename, { type: 'image/png' }), - }, - }) - ); - - if (baseNode.type === 'img2img' || baseNode.type === 'inpaint') { - const [{ payload: basePayload }] = await take( - (action): action is ReturnType => - imageUploaded.fulfilled.match(action) && - action.meta.arg.formData.file.name === baseFilename - ); - - const { image_name: baseName, image_type: baseType } = - basePayload.response; - - baseNode.image = { - image_name: baseName, - image_type: baseType, - }; - } - - // Upload the mask layer image - const maskFilename = `${uuidv4()}.png`; - - if (baseNode.type === 'inpaint') { - dispatch( - imageUploaded({ - imageType: 'intermediates', - formData: { - file: new File([maskBlob], maskFilename, { type: 'image/png' }), - }, - }) - ); - - const [{ payload: maskPayload }] = await take( - (action): action is ReturnType => - imageUploaded.fulfilled.match(action) && - action.meta.arg.formData.file.name === maskFilename - ); - - const { image_name: maskName, image_type: maskType } = - maskPayload.response; - - baseNode.mask = { - image_name: maskName, - image_type: maskType, - }; - } - - // Assemble! + // Assemble! Note that this graph *does not have the init or mask image set yet!* const nodes: Graph['nodes'] = { [rangeNode.id]: rangeNode, [iterateNode.id]: iterateNode, @@ -136,15 +90,90 @@ export const addUserInvokedCanvasListener = () => { const graph = { nodes, edges }; dispatch(canvasGraphBuilt(graph)); - moduleLog({ data: graph }, 'Canvas graph built'); - // Actually create the session + moduleLog.debug({ data: graph }, 'Canvas graph built'); + + // If we are generating img2img or inpaint, we need to upload the init images + if (baseNode.type === 'img2img' || baseNode.type === 'inpaint') { + const baseFilename = `${uuidv4()}.png`; + dispatch( + imageUploaded({ + formData: { + file: new File([baseBlob], baseFilename, { type: 'image/png' }), + }, + isIntermediate: true, + }) + ); + + // Wait for the image to be uploaded + const [{ payload: baseImageDTO }] = await take( + (action): action is ReturnType => + imageUploaded.fulfilled.match(action) && + action.meta.arg.formData.file.name === baseFilename + ); + + // Update the base node with the image name and type + baseNode.image = { + image_name: baseImageDTO.image_name, + image_type: baseImageDTO.image_type, + }; + } + + // For inpaint, we also need to upload the mask layer + if (baseNode.type === 'inpaint') { + const maskFilename = `${uuidv4()}.png`; + dispatch( + imageUploaded({ + formData: { + file: new File([maskBlob], maskFilename, { type: 'image/png' }), + }, + isIntermediate: true, + }) + ); + + // Wait for the mask to be uploaded + const [{ payload: maskImageDTO }] = await take( + (action): action is ReturnType => + imageUploaded.fulfilled.match(action) && + action.meta.arg.formData.file.name === maskFilename + ); + + // Update the base node with the image name and type + baseNode.mask = { + image_name: maskImageDTO.image_name, + image_type: maskImageDTO.image_type, + }; + } + + // Create the session and wait for response dispatch(sessionCreated({ graph })); + const [sessionCreatedAction] = await take(sessionCreated.fulfilled.match); + const sessionId = sessionCreatedAction.payload.id; - // Wait for the session to be invoked (this is just the HTTP request to start processing) - const [{ meta }] = await take(sessionInvoked.fulfilled.match); + // Associate the init image with the session, now that we have the session ID + if ( + (baseNode.type === 'img2img' || baseNode.type === 'inpaint') && + baseNode.image + ) { + dispatch( + imageUpdated({ + imageName: baseNode.image.image_name, + imageType: baseNode.image.image_type, + requestBody: { session_id: sessionId }, + }) + ); + } - const { sessionId } = meta.arg; + // Associate the mask image with the session, now that we have the session ID + if (baseNode.type === 'inpaint' && baseNode.mask) { + dispatch( + imageUpdated({ + imageName: baseNode.mask.image_name, + imageType: baseNode.mask.image_type, + requestBody: { session_id: sessionId }, + }) + ); + } if (!state.canvas.layerState.stagingArea.boundingBox) { dispatch( @@ -158,7 +187,11 @@ export const addUserInvokedCanvasListener = () => { ); } + // Flag the session with the canvas session ID dispatch(canvasSessionIdChanged(sessionId)); + + // We are ready to invoke the session! + dispatch(sessionReadyToInvoke()); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedImageToImage.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedImageToImage.ts index e747aefa08..7dcbe8a41d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedImageToImage.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedImageToImage.ts @@ -4,6 +4,7 @@ import { sessionCreated } from 'services/thunks/session'; import { log } from 'app/logging/useLogger'; import { imageToImageGraphBuilt } from 'features/nodes/store/actions'; import { userInvoked } from 'app/store/actions'; +import { sessionReadyToInvoke } from 'features/system/store/actions'; const moduleLog = log.child({ namespace: 'invoke' }); @@ -11,14 +12,18 @@ export const addUserInvokedImageToImageListener = () => { startAppListening({ predicate: (action): action is ReturnType => userInvoked.match(action) && action.payload === 'img2img', - effect: (action, { getState, dispatch }) => { + effect: async (action, { getState, dispatch, take }) => { const state = getState(); const graph = buildImageToImageGraph(state); dispatch(imageToImageGraphBuilt(graph)); - moduleLog({ data: graph }, 'Image to Image graph built'); + moduleLog.debug({ data: graph }, 'Image to Image graph built'); dispatch(sessionCreated({ graph })); + + await take(sessionCreated.fulfilled.match); + + dispatch(sessionReadyToInvoke()); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedNodes.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedNodes.ts index 01e532d5ff..6fda3db0d6 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedNodes.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedNodes.ts @@ -4,6 +4,7 @@ import { buildNodesGraph } from 'features/nodes/util/graphBuilders/buildNodesGra import { log } from 'app/logging/useLogger'; import { nodesGraphBuilt } from 'features/nodes/store/actions'; import { userInvoked } from 'app/store/actions'; +import { sessionReadyToInvoke } from 'features/system/store/actions'; const moduleLog = log.child({ namespace: 'invoke' }); @@ -11,14 +12,18 @@ export const addUserInvokedNodesListener = () => { startAppListening({ predicate: (action): action is ReturnType => userInvoked.match(action) && action.payload === 'nodes', - effect: (action, { getState, dispatch }) => { + effect: async (action, { getState, dispatch, take }) => { const state = getState(); const graph = buildNodesGraph(state); dispatch(nodesGraphBuilt(graph)); - moduleLog({ data: graph }, 'Nodes graph built'); + moduleLog.debug({ data: graph }, 'Nodes graph built'); dispatch(sessionCreated({ graph })); + + await take(sessionCreated.fulfilled.match); + + dispatch(sessionReadyToInvoke()); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedTextToImage.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedTextToImage.ts index e3eb5d0b38..6042d86cb7 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedTextToImage.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedTextToImage.ts @@ -4,6 +4,7 @@ import { sessionCreated } from 'services/thunks/session'; import { log } from 'app/logging/useLogger'; import { textToImageGraphBuilt } from 'features/nodes/store/actions'; import { userInvoked } from 'app/store/actions'; +import { sessionReadyToInvoke } from 'features/system/store/actions'; const moduleLog = log.child({ namespace: 'invoke' }); @@ -11,14 +12,20 @@ export const addUserInvokedTextToImageListener = () => { startAppListening({ predicate: (action): action is ReturnType => userInvoked.match(action) && action.payload === 'txt2img', - effect: (action, { getState, dispatch }) => { + effect: async (action, { getState, dispatch, take }) => { const state = getState(); const graph = buildTextToImageGraph(state); + dispatch(textToImageGraphBuilt(graph)); - moduleLog({ data: graph }, 'Text to Image graph built'); + + moduleLog.debug({ data: graph }, 'Text to Image graph built'); dispatch(sessionCreated({ graph })); + + await take(sessionCreated.fulfilled.match); + + dispatch(sessionReadyToInvoke()); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index b89615b2c0..4e9c154f3a 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -16,6 +16,7 @@ import lightboxReducer from 'features/lightbox/store/lightboxSlice'; import generationReducer from 'features/parameters/store/generationSlice'; import postprocessingReducer from 'features/parameters/store/postprocessingSlice'; import systemReducer from 'features/system/store/systemSlice'; +// import sessionReducer from 'features/system/store/sessionSlice'; import configReducer from 'features/system/store/configSlice'; import uiReducer from 'features/ui/store/uiSlice'; import hotkeysReducer from 'features/ui/store/hotkeysSlice'; @@ -46,6 +47,7 @@ const allReducers = { ui: uiReducer, uploads: uploadsReducer, hotkeys: hotkeysReducer, + // session: sessionReducer, }; const rootReducer = combineReducers(allReducers); diff --git a/invokeai/frontend/web/src/common/components/ImageUploader.tsx b/invokeai/frontend/web/src/common/components/ImageUploader.tsx index db6b9ee517..628d44b6f1 100644 --- a/invokeai/frontend/web/src/common/components/ImageUploader.tsx +++ b/invokeai/frontend/web/src/common/components/ImageUploader.tsx @@ -68,7 +68,6 @@ const ImageUploader = (props: ImageUploaderProps) => { async (file: File) => { dispatch( imageUploaded({ - imageType: 'uploads', formData: { file }, activeTabName, }) diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx index f5265b54db..c19a404a37 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx @@ -109,8 +109,9 @@ const currentImageButtonsSelector = createSelector( isLightboxOpen, shouldHidePreview, image: selectedImage, - seed: selectedImage?.metadata?.invokeai?.node?.seed, - prompt: selectedImage?.metadata?.invokeai?.node?.prompt, + seed: selectedImage?.metadata?.seed, + prompt: selectedImage?.metadata?.positive_conditioning, + negativePrompt: selectedImage?.metadata?.negative_conditioning, }; }, { @@ -245,13 +246,16 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { ); const handleUseSeed = useCallback(() => { - recallSeed(image?.metadata?.invokeai?.node?.seed); + recallSeed(image?.metadata?.seed); }, [image, recallSeed]); useHotkeys('s', handleUseSeed, [image]); const handleUsePrompt = useCallback(() => { - recallPrompt(image?.metadata?.invokeai?.node?.prompt); + recallPrompt( + image?.metadata?.positive_conditioning, + image?.metadata?.negative_conditioning + ); }, [image, recallPrompt]); useHotkeys('p', handleUsePrompt, [image]); @@ -454,7 +458,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { {t('parameters.copyImageToLink')} - + } size="sm" w="100%"> {t('parameters.downloadImage')} @@ -500,7 +504,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { icon={} tooltip={`${t('parameters.usePrompt')} (P)`} aria-label={`${t('parameters.usePrompt')} (P)`} - isDisabled={!image?.metadata?.invokeai?.node?.prompt} + isDisabled={!image?.metadata?.positive_conditioning} onClick={handleUsePrompt} /> @@ -508,7 +512,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { icon={} tooltip={`${t('parameters.useSeed')} (S)`} aria-label={`${t('parameters.useSeed')} (S)`} - isDisabled={!image?.metadata?.invokeai?.node?.seed} + isDisabled={!image?.metadata?.seed} onClick={handleUseSeed} /> @@ -517,9 +521,8 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { tooltip={`${t('parameters.useAll')} (A)`} aria-label={`${t('parameters.useAll')} (A)`} isDisabled={ - !['txt2img', 'img2img', 'inpaint'].includes( - String(image?.metadata?.invokeai?.node?.type) - ) + // not sure what this list should be + !['t2l', 'l2l', 'inpaint'].includes(String(image?.metadata?.type)) } onClick={handleClickUseAllParameters} /> diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx index 04fecac463..ed427f4984 100644 --- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx @@ -155,7 +155,10 @@ const HoverableImage = memo((props: HoverableImageProps) => { // Recall parameters handlers const handleRecallPrompt = useCallback(() => { - recallPrompt(image.metadata?.positive_conditioning); + recallPrompt( + image.metadata?.positive_conditioning, + image.metadata?.negative_conditioning + ); }, [image, recallPrompt]); const handleRecallSeed = useCallback(() => { @@ -248,7 +251,8 @@ const HoverableImage = memo((props: HoverableImageProps) => { icon={} onClickCapture={handleUseAllParameters} isDisabled={ - !['txt2img', 'img2img', 'inpaint'].includes( + // what should these be + !['t2l', 'l2l', 'inpaint'].includes( String(image?.metadata?.type) ) } diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx index b4bf9a6d25..b01191105e 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx @@ -8,29 +8,20 @@ import { Text, Tooltip, } from '@chakra-ui/react'; -import * as InvokeAI from 'app/types/invokeai'; import { useAppDispatch } from 'app/store/storeHooks'; import { useGetUrl } from 'common/util/getUrl'; import promptToString from 'common/util/promptToString'; -import { seedWeightsToString } from 'common/util/seedWeightPairs'; -import useSetBothPrompts from 'features/parameters/hooks/usePrompt'; import { setCfgScale, setHeight, setImg2imgStrength, setNegativePrompt, - setPerlin, setPositivePrompt, setScheduler, - setSeamless, setSeed, - setSeedWeights, - setShouldFitToWidthHeight, setSteps, - setThreshold, setWidth, } from 'features/parameters/store/generationSlice'; -import { setHiresFix } from 'features/parameters/store/postprocessingSlice'; import { setShouldShowImageDetails } from 'features/ui/store/uiSlice'; import { memo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -39,7 +30,6 @@ import { FaCopy } from 'react-icons/fa'; import { IoArrowUndoCircleOutline } from 'react-icons/io5'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import { ImageDTO } from 'services/api'; -import { filter } from 'lodash-es'; import { Scheduler } from 'app/constants'; type MetadataItemProps = { @@ -126,8 +116,6 @@ const memoEqualityCheck = ( const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { const dispatch = useAppDispatch(); - const setBothPrompts = useSetBothPrompts(); - useHotkeys('esc', () => { dispatch(setShouldShowImageDetails(false)); }); diff --git a/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx index d0d25f8bc6..fcf8359187 100644 --- a/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx @@ -33,7 +33,7 @@ export const nextPrevImageButtonsSelector = createSelector( } const currentImageIndex = state[currentCategory].ids.findIndex( - (i) => i === selectedImage.name + (i) => i === selectedImage.image_name ); const nextImageIndex = clamp( diff --git a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts index 125f4ff5d5..36f4c49401 100644 --- a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts @@ -1,14 +1,13 @@ -import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; +import { + PayloadAction, + createEntityAdapter, + createSlice, +} from '@reduxjs/toolkit'; import { RootState } from 'app/store/store'; import { receivedResultImagesPage, IMAGES_PER_PAGE, } from 'services/thunks/gallery'; -import { - imageDeleted, - imageMetadataReceived, - imageUrlsReceived, -} from 'services/thunks/image'; import { ImageDTO } from 'services/api'; import { dateComparator } from 'common/util/dateComparator'; @@ -26,6 +25,7 @@ type AdditionalResultsState = { pages: number; isLoading: boolean; nextPage: number; + upsertedImageCount: number; }; export const initialResultsState = @@ -34,6 +34,7 @@ export const initialResultsState = pages: 0, isLoading: false, nextPage: 0, + upsertedImageCount: 0, }); export type ResultsState = typeof initialResultsState; @@ -42,7 +43,10 @@ const resultsSlice = createSlice({ name: 'results', initialState: initialResultsState, reducers: { - resultAdded: resultsAdapter.upsertOne, + resultUpserted: (state, action: PayloadAction) => { + resultsAdapter.upsertOne(state, action.payload); + state.upsertedImageCount += 1; + }, }, extraReducers: (builder) => { /** @@ -68,47 +72,6 @@ const resultsSlice = createSlice({ state.nextPage = items.length < IMAGES_PER_PAGE ? page : page + 1; state.isLoading = false; }); - - /** - * Image Metadata Received - FULFILLED - */ - builder.addCase(imageMetadataReceived.fulfilled, (state, action) => { - const { image_type } = action.payload; - - if (image_type === 'results') { - resultsAdapter.upsertOne(state, action.payload as ResultsImageDTO); - } - }); - - /** - * Image URLs Received - FULFILLED - */ - builder.addCase(imageUrlsReceived.fulfilled, (state, action) => { - const { image_name, image_type, image_url, thumbnail_url } = - action.payload; - - if (image_type === 'results') { - resultsAdapter.updateOne(state, { - id: image_name, - changes: { - image_url: image_url, - thumbnail_url: thumbnail_url, - }, - }); - } - }); - - /** - * Delete Image - PENDING - * Pre-emptively remove the image from the gallery - */ - builder.addCase(imageDeleted.pending, (state, action) => { - const { imageType, imageName } = action.meta.arg; - - if (imageType === 'results') { - resultsAdapter.removeOne(state, imageName); - } - }); }, }); @@ -120,6 +83,6 @@ export const { selectTotal: selectResultsTotal, } = resultsAdapter.getSelectors((state) => state.results); -export const { resultAdded } = resultsSlice.actions; +export const { resultUpserted } = resultsSlice.actions; export default resultsSlice.reducer; diff --git a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts index 5e458503ec..3058e82673 100644 --- a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts @@ -1,11 +1,14 @@ -import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; +import { + PayloadAction, + createEntityAdapter, + createSlice, +} from '@reduxjs/toolkit'; import { RootState } from 'app/store/store'; import { receivedUploadImagesPage, IMAGES_PER_PAGE, } from 'services/thunks/gallery'; -import { imageDeleted, imageUrlsReceived } from 'services/thunks/image'; import { ImageDTO } from 'services/api'; import { dateComparator } from 'common/util/dateComparator'; @@ -23,6 +26,7 @@ type AdditionalUploadsState = { pages: number; isLoading: boolean; nextPage: number; + upsertedImageCount: number; }; export const initialUploadsState = @@ -31,6 +35,7 @@ export const initialUploadsState = pages: 0, nextPage: 0, isLoading: false, + upsertedImageCount: 0, }); export type UploadsState = typeof initialUploadsState; @@ -39,7 +44,10 @@ const uploadsSlice = createSlice({ name: 'uploads', initialState: initialUploadsState, reducers: { - uploadAdded: uploadsAdapter.upsertOne, + uploadUpserted: (state, action: PayloadAction) => { + uploadsAdapter.upsertOne(state, action.payload); + state.upsertedImageCount += 1; + }, }, extraReducers: (builder) => { /** @@ -65,36 +73,6 @@ const uploadsSlice = createSlice({ state.nextPage = items.length < IMAGES_PER_PAGE ? page : page + 1; state.isLoading = false; }); - - /** - * Image URLs Received - FULFILLED - */ - builder.addCase(imageUrlsReceived.fulfilled, (state, action) => { - const { image_name, image_type, image_url, thumbnail_url } = - action.payload; - - if (image_type === 'uploads') { - uploadsAdapter.updateOne(state, { - id: image_name, - changes: { - image_url: image_url, - thumbnail_url: thumbnail_url, - }, - }); - } - }); - - /** - * Delete Image - pending - * Pre-emptively remove the image from the gallery - */ - builder.addCase(imageDeleted.pending, (state, action) => { - const { imageType, imageName } = action.meta.arg; - - if (imageType === 'uploads') { - uploadsAdapter.removeOne(state, imageName); - } - }); }, }); @@ -106,6 +84,6 @@ export const { selectTotal: selectUploadsTotal, } = uploadsAdapter.getSelectors((state) => state.uploads); -export const { uploadAdded } = uploadsSlice.actions; +export const { uploadUpserted } = uploadsSlice.actions; export default uploadsSlice.reducer; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts index cbe16abe28..51f89e8f74 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts @@ -11,7 +11,7 @@ import { addNoiseNodes } from '../nodeBuilders/addNoiseNodes'; const POSITIVE_CONDITIONING = 'positive_conditioning'; const NEGATIVE_CONDITIONING = 'negative_conditioning'; const TEXT_TO_LATENTS = 'text_to_latents'; -const LATENTS_TO_IMAGE = 'latnets_to_image'; +const LATENTS_TO_IMAGE = 'latents_to_image'; /** * Builds the Text to Image tab graph. diff --git a/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts b/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts index ddd19b8749..631552414d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts +++ b/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts @@ -13,7 +13,9 @@ import { buildOutputFieldTemplates, } from './fieldTemplateBuilders'; -const invocationDenylist = ['Graph']; +const RESERVED_FIELD_NAMES = ['id', 'type', 'meta']; + +const invocationDenylist = ['Graph', 'InvocationMeta']; export const parseSchema = (openAPI: OpenAPIV3.Document) => { // filter out non-invocation schemas, plus some tricky invocations for now @@ -73,7 +75,7 @@ export const parseSchema = (openAPI: OpenAPIV3.Document) => { (inputsAccumulator, property, propertyName) => { if ( // `type` and `id` are not valid inputs/outputs - !['type', 'id'].includes(propertyName) && + !RESERVED_FIELD_NAMES.includes(propertyName) && isSchemaObject(property) ) { const field: InputFieldTemplate | undefined = diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts b/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts index ad9985b5de..27ae63e5dd 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts @@ -21,8 +21,8 @@ export const useParameters = () => { * Sets prompt with toast */ const recallPrompt = useCallback( - (prompt: unknown) => { - if (!isString(prompt)) { + (prompt: unknown, negativePrompt?: unknown) => { + if (!isString(prompt) || !isString(negativePrompt)) { toaster({ title: t('toast.promptNotSet'), description: t('toast.promptNotSetDesc'), @@ -33,7 +33,7 @@ export const useParameters = () => { return; } - setBothPrompts(prompt); + setBothPrompts(prompt, negativePrompt); toaster({ title: t('toast.promptSet'), status: 'info', @@ -112,12 +112,13 @@ export const useParameters = () => { const recallAllParameters = useCallback( (image: ImageDTO | undefined) => { const type = image?.metadata?.type; - if (['txt2img', 'img2img', 'inpaint'].includes(String(type))) { + // not sure what this list should be + if (['t2l', 'l2l', 'inpaint'].includes(String(type))) { dispatch(allParametersSet(image)); - if (image?.metadata?.type === 'img2img') { + if (image?.metadata?.type === 'l2l') { dispatch(setActiveTab('img2img')); - } else if (image?.metadata?.type === 'txt2img') { + } else if (image?.metadata?.type === 't2l') { dispatch(setActiveTab('txt2img')); } diff --git a/invokeai/frontend/web/src/features/parameters/hooks/usePrompt.ts b/invokeai/frontend/web/src/features/parameters/hooks/usePrompt.ts index 2a6a832720..3fee0bcdd8 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/usePrompt.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/usePrompt.ts @@ -12,15 +12,8 @@ const useSetBothPrompts = () => { const dispatch = useAppDispatch(); return useCallback( - (inputPrompt: InvokeAI.Prompt) => { - const promptString = - typeof inputPrompt === 'string' - ? inputPrompt - : promptToString(inputPrompt); - - const [prompt, negativePrompt] = getPromptAndNegative(promptString); - - dispatch(setPositivePrompt(prompt)); + (inputPrompt: InvokeAI.Prompt, negativePrompt: InvokeAI.Prompt) => { + dispatch(setPositivePrompt(inputPrompt)); dispatch(setNegativePrompt(negativePrompt)); }, [dispatch] diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index f5054f1969..849f848ff3 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -52,7 +52,7 @@ export const initialGenerationState: GenerationState = { perlin: 0, positivePrompt: '', negativePrompt: '', - scheduler: 'lms', + scheduler: 'euler', seamBlur: 16, seamSize: 96, seamSteps: 30, diff --git a/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts b/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts index d6d1af0f8e..8f06c7d0ef 100644 --- a/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts +++ b/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts @@ -7,19 +7,29 @@ export const setAllParametersReducer = ( state: Draft, action: PayloadAction ) => { - const node = action.payload?.metadata.invokeai?.node; + const metadata = action.payload?.metadata; - if (!node) { + if (!metadata) { return; } + // not sure what this list should be if ( - node.type === 'txt2img' || - node.type === 'img2img' || - node.type === 'inpaint' + metadata.type === 't2l' || + metadata.type === 'l2l' || + metadata.type === 'inpaint' ) { - const { cfg_scale, height, model, prompt, scheduler, seed, steps, width } = - node; + const { + cfg_scale, + height, + model, + positive_conditioning, + negative_conditioning, + scheduler, + seed, + steps, + width, + } = metadata; if (cfg_scale !== undefined) { state.cfgScale = Number(cfg_scale); @@ -30,8 +40,11 @@ export const setAllParametersReducer = ( if (model !== undefined) { state.model = String(model); } - if (prompt !== undefined) { - state.positivePrompt = String(prompt); + if (positive_conditioning !== undefined) { + state.positivePrompt = String(positive_conditioning); + } + if (negative_conditioning !== undefined) { + state.negativePrompt = String(negative_conditioning); } if (scheduler !== undefined) { const schedulerString = String(scheduler); @@ -51,8 +64,8 @@ export const setAllParametersReducer = ( } } - if (node.type === 'img2img') { - const { fit, image } = node as ImageToImageInvocation; + if (metadata.type === 'l2l') { + const { fit, image } = metadata as ImageToImageInvocation; if (fit !== undefined) { state.shouldFitToWidthHeight = Boolean(fit); diff --git a/invokeai/frontend/web/src/features/system/store/actions.ts b/invokeai/frontend/web/src/features/system/store/actions.ts new file mode 100644 index 0000000000..66181bc803 --- /dev/null +++ b/invokeai/frontend/web/src/features/system/store/actions.ts @@ -0,0 +1,3 @@ +import { createAction } from '@reduxjs/toolkit'; + +export const sessionReadyToInvoke = createAction('system/sessionReadyToInvoke'); diff --git a/invokeai/frontend/web/src/features/system/store/sessionSlice.ts b/invokeai/frontend/web/src/features/system/store/sessionSlice.ts new file mode 100644 index 0000000000..40d59c7baa --- /dev/null +++ b/invokeai/frontend/web/src/features/system/store/sessionSlice.ts @@ -0,0 +1,62 @@ +// TODO: split system slice inot this + +// import type { PayloadAction } from '@reduxjs/toolkit'; +// import { createSlice } from '@reduxjs/toolkit'; +// import { socketSubscribed, socketUnsubscribed } from 'services/events/actions'; + +// export type SessionState = { +// /** +// * The current socket session id +// */ +// sessionId: string; +// /** +// * Whether the current session is a canvas session. Needed to manage the staging area. +// */ +// isCanvasSession: boolean; +// /** +// * When a session is canceled, its ID is stored here until a new session is created. +// */ +// canceledSessionId: string; +// }; + +// export const initialSessionState: SessionState = { +// sessionId: '', +// isCanvasSession: false, +// canceledSessionId: '', +// }; + +// export const sessionSlice = createSlice({ +// name: 'session', +// initialState: initialSessionState, +// reducers: { +// sessionIdChanged: (state, action: PayloadAction) => { +// state.sessionId = action.payload; +// }, +// isCanvasSessionChanged: (state, action: PayloadAction) => { +// state.isCanvasSession = action.payload; +// }, +// }, +// extraReducers: (builder) => { +// /** +// * Socket Subscribed +// */ +// builder.addCase(socketSubscribed, (state, action) => { +// state.sessionId = action.payload.sessionId; +// state.canceledSessionId = ''; +// }); + +// /** +// * Socket Unsubscribed +// */ +// builder.addCase(socketUnsubscribed, (state) => { +// state.sessionId = ''; +// }); +// }, +// }); + +// export const { sessionIdChanged, isCanvasSessionChanged } = +// sessionSlice.actions; + +// export default sessionSlice.reducer; + +export default {}; diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts index 7331fcdba9..403fd60501 100644 --- a/invokeai/frontend/web/src/features/system/store/systemSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/systemSlice.ts @@ -1,5 +1,5 @@ import { UseToastOptions } from '@chakra-ui/react'; -import type { PayloadAction } from '@reduxjs/toolkit'; +import { PayloadAction, isAnyOf } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import * as InvokeAI from 'app/types/invokeai'; import { @@ -16,7 +16,11 @@ import { import { ProgressImage } from 'services/events/types'; import { makeToast } from '../../../app/components/Toaster'; -import { sessionCanceled, sessionInvoked } from 'services/thunks/session'; +import { + sessionCanceled, + sessionCreated, + sessionInvoked, +} from 'services/thunks/session'; import { receivedModels } from 'services/thunks/model'; import { parsedOpenAPISchema } from 'features/nodes/store/nodesSlice'; import { LogLevelName } from 'roarr'; @@ -215,6 +219,9 @@ export const systemSlice = createSlice({ languageChanged: (state, action: PayloadAction) => { state.language = action.payload; }, + progressImageSet(state, action: PayloadAction) { + state.progressImage = action.payload; + }, }, extraReducers(builder) { /** @@ -305,7 +312,6 @@ export const systemSlice = createSlice({ state.currentStep = 0; state.totalSteps = 0; state.statusTranslationKey = 'common.statusProcessingComplete'; - state.progressImage = null; if (state.canceledSession === data.graph_execution_state_id) { state.isProcessing = false; @@ -343,15 +349,8 @@ export const systemSlice = createSlice({ state.statusTranslationKey = 'common.statusPreparing'; }); - builder.addCase(sessionInvoked.rejected, (state, action) => { - const error = action.payload as string | undefined; - state.toastQueue.push( - makeToast({ title: error || t('toast.serverError'), status: 'error' }) - ); - }); - /** - * Session Canceled + * Session Canceled - FULFILLED */ builder.addCase(sessionCanceled.fulfilled, (state, action) => { state.canceledSession = action.meta.arg.sessionId; @@ -414,6 +413,26 @@ export const systemSlice = createSlice({ builder.addCase(imageUploaded.fulfilled, (state) => { state.isUploading = false; }); + + // *** Matchers - must be after all cases *** + + /** + * Session Invoked - REJECTED + * Session Created - REJECTED + */ + builder.addMatcher(isAnySessionRejected, (state, action) => { + state.isProcessing = false; + state.isCancelable = false; + state.isCancelScheduled = false; + state.currentStep = 0; + state.totalSteps = 0; + state.statusTranslationKey = 'common.statusConnected'; + state.progressImage = null; + + state.toastQueue.push( + makeToast({ title: t('toast.serverError'), status: 'error' }) + ); + }); }, }); @@ -438,6 +457,12 @@ export const { isPersistedChanged, shouldAntialiasProgressImageChanged, languageChanged, + progressImageSet, } = systemSlice.actions; export default systemSlice.reducer; + +const isAnySessionRejected = isAnyOf( + sessionCreated.rejected, + sessionInvoked.rejected +); diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index ecf8621ed6..e75aeac6cb 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -7,7 +7,6 @@ export { OpenAPI } from './core/OpenAPI'; export type { OpenAPIConfig } from './core/OpenAPI'; export type { AddInvocation } from './models/AddInvocation'; -export type { BlurInvocation } from './models/BlurInvocation'; export type { Body_upload_image } from './models/Body_upload_image'; export type { CkptModelInfo } from './models/CkptModelInfo'; export type { CollectInvocation } from './models/CollectInvocation'; @@ -17,7 +16,6 @@ export type { CompelInvocation } from './models/CompelInvocation'; export type { CompelOutput } from './models/CompelOutput'; export type { ConditioningField } from './models/ConditioningField'; export type { CreateModelRequest } from './models/CreateModelRequest'; -export type { CropImageInvocation } from './models/CropImageInvocation'; export type { CvInpaintInvocation } from './models/CvInpaintInvocation'; export type { DiffusersModelInfo } from './models/DiffusersModelInfo'; export type { DivideInvocation } from './models/DivideInvocation'; @@ -28,11 +26,20 @@ 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 { ImageBlurInvocation } from './models/ImageBlurInvocation'; export type { ImageCategory } from './models/ImageCategory'; +export type { ImageChannelInvocation } from './models/ImageChannelInvocation'; +export type { ImageConvertInvocation } from './models/ImageConvertInvocation'; +export type { ImageCropInvocation } from './models/ImageCropInvocation'; export type { ImageDTO } from './models/ImageDTO'; export type { ImageField } from './models/ImageField'; +export type { ImageInverseLerpInvocation } from './models/ImageInverseLerpInvocation'; +export type { ImageLerpInvocation } from './models/ImageLerpInvocation'; export type { ImageMetadata } from './models/ImageMetadata'; +export type { ImageMultiplyInvocation } from './models/ImageMultiplyInvocation'; export type { ImageOutput } from './models/ImageOutput'; +export type { ImagePasteInvocation } from './models/ImagePasteInvocation'; +export type { ImageRecordChanges } from './models/ImageRecordChanges'; export type { ImageToImageInvocation } from './models/ImageToImageInvocation'; export type { ImageToLatentsInvocation } from './models/ImageToLatentsInvocation'; export type { ImageType } from './models/ImageType'; @@ -43,14 +50,12 @@ export type { InfillTileInvocation } from './models/InfillTileInvocation'; export type { InpaintInvocation } from './models/InpaintInvocation'; export type { IntCollectionOutput } from './models/IntCollectionOutput'; export type { IntOutput } from './models/IntOutput'; -export type { InverseLerpInvocation } from './models/InverseLerpInvocation'; export type { IterateInvocation } from './models/IterateInvocation'; export type { IterateInvocationOutput } from './models/IterateInvocationOutput'; export type { LatentsField } from './models/LatentsField'; export type { LatentsOutput } from './models/LatentsOutput'; export type { LatentsToImageInvocation } from './models/LatentsToImageInvocation'; export type { LatentsToLatentsInvocation } from './models/LatentsToLatentsInvocation'; -export type { LerpInvocation } from './models/LerpInvocation'; export type { LoadImageInvocation } from './models/LoadImageInvocation'; export type { MaskFromAlphaInvocation } from './models/MaskFromAlphaInvocation'; export type { MaskOutput } from './models/MaskOutput'; @@ -61,7 +66,6 @@ export type { NoiseOutput } from './models/NoiseOutput'; export type { PaginatedResults_GraphExecutionState_ } from './models/PaginatedResults_GraphExecutionState_'; export type { PaginatedResults_ImageDTO_ } from './models/PaginatedResults_ImageDTO_'; export type { ParamIntInvocation } from './models/ParamIntInvocation'; -export type { PasteImageInvocation } from './models/PasteImageInvocation'; export type { PromptOutput } from './models/PromptOutput'; export type { RandomIntInvocation } from './models/RandomIntInvocation'; export type { RandomRangeInvocation } from './models/RandomRangeInvocation'; diff --git a/invokeai/frontend/web/src/services/api/models/AddInvocation.ts b/invokeai/frontend/web/src/services/api/models/AddInvocation.ts index 1ff7b010c2..e9671a918f 100644 --- a/invokeai/frontend/web/src/services/api/models/AddInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/AddInvocation.ts @@ -10,6 +10,10 @@ export type AddInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'add'; /** * The first number diff --git a/invokeai/frontend/web/src/services/api/models/CollectInvocation.ts b/invokeai/frontend/web/src/services/api/models/CollectInvocation.ts index d250ae4450..f190ab7073 100644 --- a/invokeai/frontend/web/src/services/api/models/CollectInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/CollectInvocation.ts @@ -10,6 +10,10 @@ export type CollectInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'collect'; /** * The item to collect (all inputs must be of the same type) diff --git a/invokeai/frontend/web/src/services/api/models/CompelInvocation.ts b/invokeai/frontend/web/src/services/api/models/CompelInvocation.ts index f03d53a841..1dc390c1be 100644 --- a/invokeai/frontend/web/src/services/api/models/CompelInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/CompelInvocation.ts @@ -10,6 +10,10 @@ export type CompelInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'compel'; /** * Prompt diff --git a/invokeai/frontend/web/src/services/api/models/CvInpaintInvocation.ts b/invokeai/frontend/web/src/services/api/models/CvInpaintInvocation.ts index 19342acf8f..874df93c30 100644 --- a/invokeai/frontend/web/src/services/api/models/CvInpaintInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/CvInpaintInvocation.ts @@ -12,6 +12,10 @@ export type CvInpaintInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'cv_inpaint'; /** * The image to inpaint diff --git a/invokeai/frontend/web/src/services/api/models/DivideInvocation.ts b/invokeai/frontend/web/src/services/api/models/DivideInvocation.ts index 3cb262e9af..fd5b3475ae 100644 --- a/invokeai/frontend/web/src/services/api/models/DivideInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/DivideInvocation.ts @@ -10,6 +10,10 @@ export type DivideInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'div'; /** * The first number diff --git a/invokeai/frontend/web/src/services/api/models/Graph.ts b/invokeai/frontend/web/src/services/api/models/Graph.ts index 039923e585..6be925841b 100644 --- a/invokeai/frontend/web/src/services/api/models/Graph.ts +++ b/invokeai/frontend/web/src/services/api/models/Graph.ts @@ -3,31 +3,34 @@ /* eslint-disable */ import type { AddInvocation } from './AddInvocation'; -import type { BlurInvocation } from './BlurInvocation'; import type { CollectInvocation } from './CollectInvocation'; import type { CompelInvocation } from './CompelInvocation'; -import type { CropImageInvocation } from './CropImageInvocation'; import type { CvInpaintInvocation } from './CvInpaintInvocation'; import type { DivideInvocation } from './DivideInvocation'; import type { Edge } from './Edge'; import type { GraphInvocation } from './GraphInvocation'; +import type { ImageBlurInvocation } from './ImageBlurInvocation'; +import type { ImageChannelInvocation } from './ImageChannelInvocation'; +import type { ImageConvertInvocation } from './ImageConvertInvocation'; +import type { ImageCropInvocation } from './ImageCropInvocation'; +import type { ImageInverseLerpInvocation } from './ImageInverseLerpInvocation'; +import type { ImageLerpInvocation } from './ImageLerpInvocation'; +import type { ImageMultiplyInvocation } from './ImageMultiplyInvocation'; +import type { ImagePasteInvocation } from './ImagePasteInvocation'; import type { ImageToImageInvocation } from './ImageToImageInvocation'; import type { ImageToLatentsInvocation } from './ImageToLatentsInvocation'; import type { InfillColorInvocation } from './InfillColorInvocation'; import type { InfillPatchMatchInvocation } from './InfillPatchMatchInvocation'; import type { InfillTileInvocation } from './InfillTileInvocation'; import type { InpaintInvocation } from './InpaintInvocation'; -import type { InverseLerpInvocation } from './InverseLerpInvocation'; import type { IterateInvocation } from './IterateInvocation'; import type { LatentsToImageInvocation } from './LatentsToImageInvocation'; import type { LatentsToLatentsInvocation } from './LatentsToLatentsInvocation'; -import type { LerpInvocation } from './LerpInvocation'; import type { LoadImageInvocation } from './LoadImageInvocation'; import type { MaskFromAlphaInvocation } from './MaskFromAlphaInvocation'; import type { MultiplyInvocation } from './MultiplyInvocation'; import type { NoiseInvocation } from './NoiseInvocation'; import type { ParamIntInvocation } from './ParamIntInvocation'; -import type { PasteImageInvocation } from './PasteImageInvocation'; import type { RandomIntInvocation } from './RandomIntInvocation'; import type { RandomRangeInvocation } from './RandomRangeInvocation'; import type { RangeInvocation } from './RangeInvocation'; @@ -49,7 +52,7 @@ export type Graph = { /** * The nodes in this graph */ - nodes?: Record; + nodes?: Record; /** * The connections between nodes and their fields in this graph */ diff --git a/invokeai/frontend/web/src/services/api/models/GraphInvocation.ts b/invokeai/frontend/web/src/services/api/models/GraphInvocation.ts index 5109a49a68..8512faae74 100644 --- a/invokeai/frontend/web/src/services/api/models/GraphInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/GraphInvocation.ts @@ -5,14 +5,17 @@ import type { Graph } from './Graph'; /** - * A node to process inputs and produce outputs. - * May use dependency injection in __init__ to receive providers. + * Execute a graph */ export type GraphInvocation = { /** * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'graph'; /** * The graph to run diff --git a/invokeai/frontend/web/src/services/api/models/BlurInvocation.ts b/invokeai/frontend/web/src/services/api/models/ImageBlurInvocation.ts similarity index 72% rename from invokeai/frontend/web/src/services/api/models/BlurInvocation.ts rename to invokeai/frontend/web/src/services/api/models/ImageBlurInvocation.ts index 0643e4b309..3ba86d8fab 100644 --- a/invokeai/frontend/web/src/services/api/models/BlurInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/ImageBlurInvocation.ts @@ -7,12 +7,16 @@ import type { ImageField } from './ImageField'; /** * Blurs an image */ -export type BlurInvocation = { +export type ImageBlurInvocation = { /** * The id of this node. Must be unique among all nodes. */ id: string; - type?: 'blur'; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; + type?: 'img_blur'; /** * The image to blur */ diff --git a/invokeai/frontend/web/src/services/api/models/ImageCategory.ts b/invokeai/frontend/web/src/services/api/models/ImageCategory.ts index c4edf90fd3..6b04a0b864 100644 --- a/invokeai/frontend/web/src/services/api/models/ImageCategory.ts +++ b/invokeai/frontend/web/src/services/api/models/ImageCategory.ts @@ -5,4 +5,4 @@ /** * The category of an image. Use ImageCategory.OTHER for non-default categories. */ -export type ImageCategory = 'general' | 'control' | 'other'; +export type ImageCategory = 'general' | 'control' | 'mask' | 'other'; diff --git a/invokeai/frontend/web/src/services/api/models/ImageChannelInvocation.ts b/invokeai/frontend/web/src/services/api/models/ImageChannelInvocation.ts new file mode 100644 index 0000000000..47bfd4110f --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageChannelInvocation.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Gets a channel from an image. + */ +export type ImageChannelInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; + type?: 'img_chan'; + /** + * The image to get the channel from + */ + image?: ImageField; + /** + * The channel to get + */ + channel?: 'A' | 'R' | 'G' | 'B'; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ImageConvertInvocation.ts b/invokeai/frontend/web/src/services/api/models/ImageConvertInvocation.ts new file mode 100644 index 0000000000..4bd59d03b0 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageConvertInvocation.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Converts an image to a different mode. + */ +export type ImageConvertInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; + type?: 'img_conv'; + /** + * The image to convert + */ + image?: ImageField; + /** + * The mode to convert to + */ + mode?: 'L' | 'RGB' | 'RGBA' | 'CMYK' | 'YCbCr' | 'LAB' | 'HSV' | 'I' | 'F'; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/CropImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/ImageCropInvocation.ts similarity index 80% rename from invokeai/frontend/web/src/services/api/models/CropImageInvocation.ts rename to invokeai/frontend/web/src/services/api/models/ImageCropInvocation.ts index 2676f5cb87..5207ebbf6d 100644 --- a/invokeai/frontend/web/src/services/api/models/CropImageInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/ImageCropInvocation.ts @@ -7,12 +7,16 @@ import type { ImageField } from './ImageField'; /** * Crops an image to a specified box. The box can be outside of the image. */ -export type CropImageInvocation = { +export type ImageCropInvocation = { /** * The id of this node. Must be unique among all nodes. */ id: string; - type?: 'crop'; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; + type?: 'img_crop'; /** * The image to crop */ diff --git a/invokeai/frontend/web/src/services/api/models/ImageDTO.ts b/invokeai/frontend/web/src/services/api/models/ImageDTO.ts index c5377b4c76..bc2f19f1b5 100644 --- a/invokeai/frontend/web/src/services/api/models/ImageDTO.ts +++ b/invokeai/frontend/web/src/services/api/models/ImageDTO.ts @@ -50,6 +50,10 @@ export type ImageDTO = { * The deleted timestamp of the image. */ deleted_at?: string; + /** + * Whether this is an intermediate image. + */ + is_intermediate: boolean; /** * The session ID that generated this image, if it is a generated image. */ diff --git a/invokeai/frontend/web/src/services/api/models/InverseLerpInvocation.ts b/invokeai/frontend/web/src/services/api/models/ImageInverseLerpInvocation.ts similarity index 73% rename from invokeai/frontend/web/src/services/api/models/InverseLerpInvocation.ts rename to invokeai/frontend/web/src/services/api/models/ImageInverseLerpInvocation.ts index 33c59b7bac..0347d4dc38 100644 --- a/invokeai/frontend/web/src/services/api/models/InverseLerpInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/ImageInverseLerpInvocation.ts @@ -7,12 +7,16 @@ import type { ImageField } from './ImageField'; /** * Inverse linear interpolation of all pixels of an image */ -export type InverseLerpInvocation = { +export type ImageInverseLerpInvocation = { /** * The id of this node. Must be unique among all nodes. */ id: string; - type?: 'ilerp'; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; + type?: 'img_ilerp'; /** * The image to lerp */ diff --git a/invokeai/frontend/web/src/services/api/models/LerpInvocation.ts b/invokeai/frontend/web/src/services/api/models/ImageLerpInvocation.ts similarity index 74% rename from invokeai/frontend/web/src/services/api/models/LerpInvocation.ts rename to invokeai/frontend/web/src/services/api/models/ImageLerpInvocation.ts index f2406c2246..388c86061c 100644 --- a/invokeai/frontend/web/src/services/api/models/LerpInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/ImageLerpInvocation.ts @@ -7,12 +7,16 @@ import type { ImageField } from './ImageField'; /** * Linear interpolation of all pixels of an image */ -export type LerpInvocation = { +export type ImageLerpInvocation = { /** * The id of this node. Must be unique among all nodes. */ id: string; - type?: 'lerp'; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; + type?: 'img_lerp'; /** * The image to lerp */ diff --git a/invokeai/frontend/web/src/services/api/models/ImageMultiplyInvocation.ts b/invokeai/frontend/web/src/services/api/models/ImageMultiplyInvocation.ts new file mode 100644 index 0000000000..751ee49158 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageMultiplyInvocation.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Multiplies two images together using `PIL.ImageChops.multiply()`. + */ +export type ImageMultiplyInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; + type?: 'img_mul'; + /** + * The first image to multiply + */ + image1?: ImageField; + /** + * The second image to multiply + */ + image2?: ImageField; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/PasteImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/ImagePasteInvocation.ts similarity index 79% rename from invokeai/frontend/web/src/services/api/models/PasteImageInvocation.ts rename to invokeai/frontend/web/src/services/api/models/ImagePasteInvocation.ts index 8a181ccf07..c883b9a5d8 100644 --- a/invokeai/frontend/web/src/services/api/models/PasteImageInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/ImagePasteInvocation.ts @@ -7,12 +7,16 @@ import type { ImageField } from './ImageField'; /** * Pastes an image into another image. */ -export type PasteImageInvocation = { +export type ImagePasteInvocation = { /** * The id of this node. Must be unique among all nodes. */ id: string; - type?: 'paste'; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; + type?: 'img_paste'; /** * The base image */ diff --git a/invokeai/frontend/web/src/services/api/models/ImageRecordChanges.ts b/invokeai/frontend/web/src/services/api/models/ImageRecordChanges.ts new file mode 100644 index 0000000000..51f0ee2079 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageRecordChanges.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageCategory } from './ImageCategory'; + +/** + * A set of changes to apply to an image record. + * + * Only limited changes are valid: + * - `image_category`: change the category of an image + * - `session_id`: change the session associated with an image + */ +export type ImageRecordChanges = { + /** + * The image's new category. + */ + image_category?: ImageCategory; + /** + * The image's new session ID. + */ + session_id?: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ImageToImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/ImageToImageInvocation.ts index fb43c76921..7287b4cb71 100644 --- a/invokeai/frontend/web/src/services/api/models/ImageToImageInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/ImageToImageInvocation.ts @@ -12,6 +12,10 @@ export type ImageToImageInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'img2img'; /** * The prompt to generate an image from diff --git a/invokeai/frontend/web/src/services/api/models/ImageToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/models/ImageToLatentsInvocation.ts index f72d446615..5569c2fa86 100644 --- a/invokeai/frontend/web/src/services/api/models/ImageToLatentsInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/ImageToLatentsInvocation.ts @@ -12,6 +12,10 @@ export type ImageToLatentsInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'i2l'; /** * The image to encode diff --git a/invokeai/frontend/web/src/services/api/models/ImageType.ts b/invokeai/frontend/web/src/services/api/models/ImageType.ts index bba9134e63..dfc10bf455 100644 --- a/invokeai/frontend/web/src/services/api/models/ImageType.ts +++ b/invokeai/frontend/web/src/services/api/models/ImageType.ts @@ -5,4 +5,4 @@ /** * The type of an image. */ -export type ImageType = 'results' | 'uploads' | 'intermediates'; +export type ImageType = 'results' | 'uploads'; diff --git a/invokeai/frontend/web/src/services/api/models/InfillColorInvocation.ts b/invokeai/frontend/web/src/services/api/models/InfillColorInvocation.ts index 157c976e11..3e637b299c 100644 --- a/invokeai/frontend/web/src/services/api/models/InfillColorInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/InfillColorInvocation.ts @@ -13,6 +13,10 @@ export type InfillColorInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'infill_rgba'; /** * The image to infill diff --git a/invokeai/frontend/web/src/services/api/models/InfillPatchMatchInvocation.ts b/invokeai/frontend/web/src/services/api/models/InfillPatchMatchInvocation.ts index a4c18ade5d..325bfe2080 100644 --- a/invokeai/frontend/web/src/services/api/models/InfillPatchMatchInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/InfillPatchMatchInvocation.ts @@ -12,6 +12,10 @@ export type InfillPatchMatchInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'infill_patchmatch'; /** * The image to infill diff --git a/invokeai/frontend/web/src/services/api/models/InfillTileInvocation.ts b/invokeai/frontend/web/src/services/api/models/InfillTileInvocation.ts index 12113f57f5..dfb1cbc61d 100644 --- a/invokeai/frontend/web/src/services/api/models/InfillTileInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/InfillTileInvocation.ts @@ -12,6 +12,10 @@ export type InfillTileInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'infill_tile'; /** * The image to infill diff --git a/invokeai/frontend/web/src/services/api/models/InpaintInvocation.ts b/invokeai/frontend/web/src/services/api/models/InpaintInvocation.ts index 88ead9907c..fa5ae01c8f 100644 --- a/invokeai/frontend/web/src/services/api/models/InpaintInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/InpaintInvocation.ts @@ -13,6 +13,10 @@ export type InpaintInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'inpaint'; /** * The prompt to generate an image from diff --git a/invokeai/frontend/web/src/services/api/models/IterateInvocation.ts b/invokeai/frontend/web/src/services/api/models/IterateInvocation.ts index 0ff7a1258d..15bf92dfea 100644 --- a/invokeai/frontend/web/src/services/api/models/IterateInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/IterateInvocation.ts @@ -3,14 +3,17 @@ /* eslint-disable */ /** - * A node to process inputs and produce outputs. - * May use dependency injection in __init__ to receive providers. + * Iterates over a list of items */ export type IterateInvocation = { /** * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'iterate'; /** * The list of items to iterate over diff --git a/invokeai/frontend/web/src/services/api/models/LatentsToImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/LatentsToImageInvocation.ts index 8acd872e28..fcaa37d7e8 100644 --- a/invokeai/frontend/web/src/services/api/models/LatentsToImageInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/LatentsToImageInvocation.ts @@ -12,6 +12,10 @@ export type LatentsToImageInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'l2i'; /** * The latents to generate an image from diff --git a/invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts index 29995c6ad9..6436557f64 100644 --- a/invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts @@ -13,6 +13,10 @@ export type LatentsToLatentsInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'l2l'; /** * Positive conditioning for generation @@ -42,14 +46,6 @@ export type LatentsToLatentsInvocation = { * The model to use (currently ignored) */ model?: string; - /** - * Whether or not to generate an image that can tile without seams - */ - seamless?: boolean; - /** - * The axes to tile the image on, 'x' and/or 'y' - */ - seamless_axes?: string; /** * The latents to use as a base image */ diff --git a/invokeai/frontend/web/src/services/api/models/LoadImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/LoadImageInvocation.ts index 745a9b44e4..f20d983f9b 100644 --- a/invokeai/frontend/web/src/services/api/models/LoadImageInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/LoadImageInvocation.ts @@ -2,7 +2,7 @@ /* tslint:disable */ /* eslint-disable */ -import type { ImageType } from './ImageType'; +import type { ImageField } from './ImageField'; /** * Load an image and provide it as output. @@ -12,14 +12,14 @@ export type LoadImageInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'load_image'; /** - * The type of the image + * The image to load */ - image_type: ImageType; - /** - * The name of the image - */ - image_name: string; + image?: ImageField; }; diff --git a/invokeai/frontend/web/src/services/api/models/MaskFromAlphaInvocation.ts b/invokeai/frontend/web/src/services/api/models/MaskFromAlphaInvocation.ts index e71b1f464b..e3693f6d98 100644 --- a/invokeai/frontend/web/src/services/api/models/MaskFromAlphaInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/MaskFromAlphaInvocation.ts @@ -12,6 +12,10 @@ export type MaskFromAlphaInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'tomask'; /** * The image to create the mask from diff --git a/invokeai/frontend/web/src/services/api/models/MultiplyInvocation.ts b/invokeai/frontend/web/src/services/api/models/MultiplyInvocation.ts index eede8f18d7..9fd716f33d 100644 --- a/invokeai/frontend/web/src/services/api/models/MultiplyInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/MultiplyInvocation.ts @@ -10,6 +10,10 @@ export type MultiplyInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'mul'; /** * The first number diff --git a/invokeai/frontend/web/src/services/api/models/NoiseInvocation.ts b/invokeai/frontend/web/src/services/api/models/NoiseInvocation.ts index 59e50b76f3..239a24bfe5 100644 --- a/invokeai/frontend/web/src/services/api/models/NoiseInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/NoiseInvocation.ts @@ -10,6 +10,10 @@ export type NoiseInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'noise'; /** * The seed to use diff --git a/invokeai/frontend/web/src/services/api/models/ParamIntInvocation.ts b/invokeai/frontend/web/src/services/api/models/ParamIntInvocation.ts index 7047310a87..7a45d0a0ac 100644 --- a/invokeai/frontend/web/src/services/api/models/ParamIntInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/ParamIntInvocation.ts @@ -10,6 +10,10 @@ export type ParamIntInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'param_int'; /** * The integer value diff --git a/invokeai/frontend/web/src/services/api/models/RandomIntInvocation.ts b/invokeai/frontend/web/src/services/api/models/RandomIntInvocation.ts index 0a5220c31d..a2f7c2f02a 100644 --- a/invokeai/frontend/web/src/services/api/models/RandomIntInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/RandomIntInvocation.ts @@ -10,6 +10,10 @@ export type RandomIntInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'rand_int'; /** * The inclusive low value diff --git a/invokeai/frontend/web/src/services/api/models/RandomRangeInvocation.ts b/invokeai/frontend/web/src/services/api/models/RandomRangeInvocation.ts index c1f80042a6..925511578d 100644 --- a/invokeai/frontend/web/src/services/api/models/RandomRangeInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/RandomRangeInvocation.ts @@ -10,6 +10,10 @@ export type RandomRangeInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'random_range'; /** * The inclusive low value diff --git a/invokeai/frontend/web/src/services/api/models/RangeInvocation.ts b/invokeai/frontend/web/src/services/api/models/RangeInvocation.ts index 1c37ca7fe3..3681602a95 100644 --- a/invokeai/frontend/web/src/services/api/models/RangeInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/RangeInvocation.ts @@ -10,6 +10,10 @@ export type RangeInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'range'; /** * The start of the range diff --git a/invokeai/frontend/web/src/services/api/models/RangeOfSizeInvocation.ts b/invokeai/frontend/web/src/services/api/models/RangeOfSizeInvocation.ts index b918f17130..7dfac68d39 100644 --- a/invokeai/frontend/web/src/services/api/models/RangeOfSizeInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/RangeOfSizeInvocation.ts @@ -10,6 +10,10 @@ export type RangeOfSizeInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'range_of_size'; /** * The start of the range diff --git a/invokeai/frontend/web/src/services/api/models/ResizeLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/models/ResizeLatentsInvocation.ts index c0fabb4984..9a7b6c61e4 100644 --- a/invokeai/frontend/web/src/services/api/models/ResizeLatentsInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/ResizeLatentsInvocation.ts @@ -12,6 +12,10 @@ export type ResizeLatentsInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'lresize'; /** * The latents to resize diff --git a/invokeai/frontend/web/src/services/api/models/RestoreFaceInvocation.ts b/invokeai/frontend/web/src/services/api/models/RestoreFaceInvocation.ts index e03ed01c81..0bacb5d805 100644 --- a/invokeai/frontend/web/src/services/api/models/RestoreFaceInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/RestoreFaceInvocation.ts @@ -12,6 +12,10 @@ export type RestoreFaceInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'restore_face'; /** * The input image diff --git a/invokeai/frontend/web/src/services/api/models/ScaleLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/models/ScaleLatentsInvocation.ts index f398eaf408..506b21e540 100644 --- a/invokeai/frontend/web/src/services/api/models/ScaleLatentsInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/ScaleLatentsInvocation.ts @@ -12,6 +12,10 @@ export type ScaleLatentsInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'lscale'; /** * The latents to scale diff --git a/invokeai/frontend/web/src/services/api/models/ShowImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/ShowImageInvocation.ts index 145895ad75..1b73055584 100644 --- a/invokeai/frontend/web/src/services/api/models/ShowImageInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/ShowImageInvocation.ts @@ -12,6 +12,10 @@ export type ShowImageInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'show_image'; /** * The image to show diff --git a/invokeai/frontend/web/src/services/api/models/SubtractInvocation.ts b/invokeai/frontend/web/src/services/api/models/SubtractInvocation.ts index 6f2da116a2..23334bd891 100644 --- a/invokeai/frontend/web/src/services/api/models/SubtractInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/SubtractInvocation.ts @@ -10,6 +10,10 @@ export type SubtractInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'sub'; /** * The first number diff --git a/invokeai/frontend/web/src/services/api/models/TextToImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/TextToImageInvocation.ts index 184e35693b..de95ff738c 100644 --- a/invokeai/frontend/web/src/services/api/models/TextToImageInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/TextToImageInvocation.ts @@ -10,6 +10,10 @@ export type TextToImageInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'txt2img'; /** * The prompt to generate an image from diff --git a/invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts index d1ec5ed08c..33eedc0f02 100644 --- a/invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts @@ -13,6 +13,10 @@ export type TextToLatentsInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 't2l'; /** * Positive conditioning for generation @@ -42,13 +46,5 @@ export type TextToLatentsInvocation = { * The model to use (currently ignored) */ model?: string; - /** - * Whether or not to generate an image that can tile without seams - */ - seamless?: boolean; - /** - * The axes to tile the image on, 'x' and/or 'y' - */ - seamless_axes?: string; }; diff --git a/invokeai/frontend/web/src/services/api/models/UpscaleInvocation.ts b/invokeai/frontend/web/src/services/api/models/UpscaleInvocation.ts index 8416c2454d..d0aca63964 100644 --- a/invokeai/frontend/web/src/services/api/models/UpscaleInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/UpscaleInvocation.ts @@ -12,6 +12,10 @@ export type UpscaleInvocation = { * The id of this node. Must be unique among all nodes. */ id: string; + /** + * Whether or not this node is an intermediate node. + */ + is_intermediate?: boolean; type?: 'upscale'; /** * The input image diff --git a/invokeai/frontend/web/src/services/api/services/ImagesService.ts b/invokeai/frontend/web/src/services/api/services/ImagesService.ts index 13b2ef836a..d01a97a45e 100644 --- a/invokeai/frontend/web/src/services/api/services/ImagesService.ts +++ b/invokeai/frontend/web/src/services/api/services/ImagesService.ts @@ -4,6 +4,7 @@ import type { Body_upload_image } from '../models/Body_upload_image'; import type { ImageCategory } from '../models/ImageCategory'; import type { ImageDTO } from '../models/ImageDTO'; +import type { ImageRecordChanges } from '../models/ImageRecordChanges'; import type { ImageType } from '../models/ImageType'; import type { ImageUrlsDTO } from '../models/ImageUrlsDTO'; import type { PaginatedResults_ImageDTO_ } from '../models/PaginatedResults_ImageDTO_'; @@ -65,20 +66,32 @@ export class ImagesService { * @throws ApiError */ public static uploadImage({ - imageType, formData, imageCategory, + isIntermediate = false, + sessionId, }: { - imageType: ImageType, formData: Body_upload_image, + /** + * The category of the image + */ imageCategory?: ImageCategory, + /** + * Whether this is an intermediate image + */ + isIntermediate?: boolean, + /** + * The session ID associated with this upload, if any + */ + sessionId?: string, }): CancelablePromise { return __request(OpenAPI, { method: 'POST', url: '/api/v1/images/', query: { - 'image_type': imageType, 'image_category': imageCategory, + 'is_intermediate': isIntermediate, + 'session_id': sessionId, }, formData: formData, mediaType: 'multipart/form-data', @@ -132,6 +145,9 @@ export class ImagesService { imageType, imageName, }: { + /** + * The type of image to delete + */ imageType: ImageType, /** * The name of the image to delete @@ -151,6 +167,42 @@ export class ImagesService { }); } + /** + * Update Image + * Updates an image + * @returns ImageDTO Successful Response + * @throws ApiError + */ + public static updateImage({ + imageType, + imageName, + requestBody, + }: { + /** + * The type of image to update + */ + imageType: ImageType, + /** + * The name of the image to update + */ + imageName: string, + requestBody: ImageRecordChanges, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'PATCH', + url: '/api/v1/images/{image_type}/{image_name}', + path: { + 'image_type': imageType, + 'image_name': imageName, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } + /** * Get Image Metadata * Gets an image's metadata diff --git a/invokeai/frontend/web/src/services/api/services/SessionsService.ts b/invokeai/frontend/web/src/services/api/services/SessionsService.ts index 23597c9e9e..1c55d36502 100644 --- a/invokeai/frontend/web/src/services/api/services/SessionsService.ts +++ b/invokeai/frontend/web/src/services/api/services/SessionsService.ts @@ -2,34 +2,37 @@ /* tslint:disable */ /* eslint-disable */ import type { AddInvocation } from '../models/AddInvocation'; -import type { BlurInvocation } from '../models/BlurInvocation'; import type { CollectInvocation } from '../models/CollectInvocation'; import type { CompelInvocation } from '../models/CompelInvocation'; -import type { CropImageInvocation } from '../models/CropImageInvocation'; import type { CvInpaintInvocation } from '../models/CvInpaintInvocation'; import type { DivideInvocation } from '../models/DivideInvocation'; import type { Edge } from '../models/Edge'; import type { Graph } from '../models/Graph'; import type { GraphExecutionState } from '../models/GraphExecutionState'; import type { GraphInvocation } from '../models/GraphInvocation'; +import type { ImageBlurInvocation } from '../models/ImageBlurInvocation'; +import type { ImageChannelInvocation } from '../models/ImageChannelInvocation'; +import type { ImageConvertInvocation } from '../models/ImageConvertInvocation'; +import type { ImageCropInvocation } from '../models/ImageCropInvocation'; +import type { ImageInverseLerpInvocation } from '../models/ImageInverseLerpInvocation'; +import type { ImageLerpInvocation } from '../models/ImageLerpInvocation'; +import type { ImageMultiplyInvocation } from '../models/ImageMultiplyInvocation'; +import type { ImagePasteInvocation } from '../models/ImagePasteInvocation'; import type { ImageToImageInvocation } from '../models/ImageToImageInvocation'; import type { ImageToLatentsInvocation } from '../models/ImageToLatentsInvocation'; import type { InfillColorInvocation } from '../models/InfillColorInvocation'; import type { InfillPatchMatchInvocation } from '../models/InfillPatchMatchInvocation'; import type { InfillTileInvocation } from '../models/InfillTileInvocation'; import type { InpaintInvocation } from '../models/InpaintInvocation'; -import type { InverseLerpInvocation } from '../models/InverseLerpInvocation'; import type { IterateInvocation } from '../models/IterateInvocation'; import type { LatentsToImageInvocation } from '../models/LatentsToImageInvocation'; import type { LatentsToLatentsInvocation } from '../models/LatentsToLatentsInvocation'; -import type { LerpInvocation } from '../models/LerpInvocation'; import type { LoadImageInvocation } from '../models/LoadImageInvocation'; import type { MaskFromAlphaInvocation } from '../models/MaskFromAlphaInvocation'; import type { MultiplyInvocation } from '../models/MultiplyInvocation'; import type { NoiseInvocation } from '../models/NoiseInvocation'; import type { PaginatedResults_GraphExecutionState_ } from '../models/PaginatedResults_GraphExecutionState_'; import type { ParamIntInvocation } from '../models/ParamIntInvocation'; -import type { PasteImageInvocation } from '../models/PasteImageInvocation'; import type { RandomIntInvocation } from '../models/RandomIntInvocation'; import type { RandomRangeInvocation } from '../models/RandomRangeInvocation'; import type { RangeInvocation } from '../models/RangeInvocation'; @@ -151,7 +154,7 @@ export class SessionsService { * The id of the session */ sessionId: string, - requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), + requestBody: (LoadImageInvocation | ShowImageInvocation | ImageCropInvocation | ImagePasteInvocation | MaskFromAlphaInvocation | ImageMultiplyInvocation | ImageChannelInvocation | ImageConvertInvocation | ImageBlurInvocation | ImageLerpInvocation | ImageInverseLerpInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), }): CancelablePromise { return __request(OpenAPI, { method: 'POST', @@ -188,7 +191,7 @@ export class SessionsService { * The path to the node in the graph */ nodePath: string, - requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), + requestBody: (LoadImageInvocation | ShowImageInvocation | ImageCropInvocation | ImagePasteInvocation | MaskFromAlphaInvocation | ImageMultiplyInvocation | ImageChannelInvocation | ImageConvertInvocation | ImageBlurInvocation | ImageLerpInvocation | ImageInverseLerpInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), }): CancelablePromise { return __request(OpenAPI, { method: 'PUT', diff --git a/invokeai/frontend/web/src/services/events/middleware.ts b/invokeai/frontend/web/src/services/events/middleware.ts index bd1d60099a..f1eb844f2c 100644 --- a/invokeai/frontend/web/src/services/events/middleware.ts +++ b/invokeai/frontend/web/src/services/events/middleware.ts @@ -8,7 +8,7 @@ import { import { socketSubscribed, socketUnsubscribed } from './actions'; import { AppThunkDispatch, RootState } from 'app/store/store'; import { getTimestamp } from 'common/util/getTimestamp'; -import { sessionInvoked, sessionCreated } from 'services/thunks/session'; +import { sessionCreated } from 'services/thunks/session'; import { OpenAPI } from 'services/api'; import { setEventListeners } from 'services/events/util/setEventListeners'; import { log } from 'app/logging/useLogger'; @@ -64,15 +64,9 @@ export const socketMiddleware = () => { if (sessionCreated.fulfilled.match(action)) { const sessionId = action.payload.id; - const sessionLog = socketioLog.child({ sessionId }); const oldSessionId = getState().system.sessionId; if (oldSessionId) { - sessionLog.debug( - { oldSessionId }, - `Unsubscribed from old session (${oldSessionId})` - ); - socket.emit('unsubscribe', { session: oldSessionId, }); @@ -85,8 +79,6 @@ export const socketMiddleware = () => { ); } - sessionLog.debug(`Subscribe to new session (${sessionId})`); - socket.emit('subscribe', { session: sessionId }); dispatch( @@ -95,9 +87,6 @@ export const socketMiddleware = () => { timestamp: getTimestamp(), }) ); - - // Finally we actually invoke the session, starting processing - dispatch(sessionInvoked({ sessionId })); } next(action); diff --git a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts index 4431a9fd8b..5262b26d1e 100644 --- a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts +++ b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts @@ -1,7 +1,6 @@ import { MiddlewareAPI } from '@reduxjs/toolkit'; import { AppDispatch, RootState } from 'app/store/store'; import { getTimestamp } from 'common/util/getTimestamp'; -import { sessionCanceled } from 'services/thunks/session'; import { Socket } from 'socket.io-client'; import { generatorProgress, @@ -16,12 +15,6 @@ import { import { ClientToServerEvents, ServerToClientEvents } from '../types'; import { Logger } from 'roarr'; import { JsonObject } from 'roarr/dist/types'; -import { - receivedResultImagesPage, - receivedUploadImagesPage, -} from 'services/thunks/gallery'; -import { receivedModels } from 'services/thunks/model'; -import { receivedOpenAPISchema } from 'services/thunks/schema'; import { makeToast } from '../../../app/components/Toaster'; import { addToast } from '../../../features/system/store/systemSlice'; @@ -43,37 +36,13 @@ export const setEventListeners = (arg: SetEventListenersArg) => { dispatch(socketConnected({ timestamp: getTimestamp() })); - const { results, uploads, models, nodes, config, system } = getState(); + const { sessionId } = getState().system; - const { disabledTabs } = config; - - // These thunks need to be dispatch in middleware; cannot handle in a reducer - if (!results.ids.length) { - dispatch(receivedResultImagesPage()); - } - - if (!uploads.ids.length) { - dispatch(receivedUploadImagesPage()); - } - - if (!models.ids.length) { - dispatch(receivedModels()); - } - - if (!nodes.schema && !disabledTabs.includes('nodes')) { - dispatch(receivedOpenAPISchema()); - } - - if (system.sessionId) { - log.debug( - { sessionId: system.sessionId }, - `Subscribed to existing session (${system.sessionId})` - ); - - socket.emit('subscribe', { session: system.sessionId }); + if (sessionId) { + socket.emit('subscribe', { session: sessionId }); dispatch( socketSubscribed({ - sessionId: system.sessionId, + sessionId, timestamp: getTimestamp(), }) ); @@ -101,7 +70,6 @@ export const setEventListeners = (arg: SetEventListenersArg) => { * Disconnect */ socket.on('disconnect', () => { - log.debug('Disconnected'); dispatch(socketDisconnected({ timestamp: getTimestamp() })); }); @@ -109,18 +77,6 @@ export const setEventListeners = (arg: SetEventListenersArg) => { * Invocation started */ socket.on('invocation_started', (data) => { - if (getState().system.canceledSession === data.graph_execution_state_id) { - log.trace( - { data, sessionId: data.graph_execution_state_id }, - `Ignored invocation started (${data.node.type}) for canceled session (${data.graph_execution_state_id})` - ); - return; - } - - log.info( - { data, sessionId: data.graph_execution_state_id }, - `Invocation started (${data.node.type})` - ); dispatch(invocationStarted({ data, timestamp: getTimestamp() })); }); @@ -128,18 +84,6 @@ export const setEventListeners = (arg: SetEventListenersArg) => { * Generator progress */ socket.on('generator_progress', (data) => { - if (getState().system.canceledSession === data.graph_execution_state_id) { - log.trace( - { data, sessionId: data.graph_execution_state_id }, - `Ignored generator progress (${data.node.type}) for canceled session (${data.graph_execution_state_id})` - ); - return; - } - - log.trace( - { data, sessionId: data.graph_execution_state_id }, - `Generator progress (${data.node.type})` - ); dispatch(generatorProgress({ data, timestamp: getTimestamp() })); }); @@ -147,10 +91,6 @@ export const setEventListeners = (arg: SetEventListenersArg) => { * Invocation error */ socket.on('invocation_error', (data) => { - log.error( - { data, sessionId: data.graph_execution_state_id }, - `Invocation error (${data.node.type})` - ); dispatch(invocationError({ data, timestamp: getTimestamp() })); }); @@ -158,19 +98,6 @@ export const setEventListeners = (arg: SetEventListenersArg) => { * Invocation complete */ socket.on('invocation_complete', (data) => { - log.info( - { data, sessionId: data.graph_execution_state_id }, - `Invocation complete (${data.node.type})` - ); - const sessionId = data.graph_execution_state_id; - - const { cancelType, isCancelScheduled } = getState().system; - - // Handle scheduled cancelation - if (cancelType === 'scheduled' && isCancelScheduled) { - dispatch(sessionCanceled({ sessionId })); - } - dispatch( invocationComplete({ data, @@ -183,10 +110,6 @@ export const setEventListeners = (arg: SetEventListenersArg) => { * Graph complete */ socket.on('graph_execution_state_complete', (data) => { - log.info( - { data, sessionId: data.graph_execution_state_id }, - `Graph execution state complete (${data.graph_execution_state_id})` - ); dispatch(graphExecutionStateComplete({ data, timestamp: getTimestamp() })); }); }; diff --git a/invokeai/frontend/web/src/services/thunks/gallery.ts b/invokeai/frontend/web/src/services/thunks/gallery.ts index 01e8a986b2..11960e00d2 100644 --- a/invokeai/frontend/web/src/services/thunks/gallery.ts +++ b/invokeai/frontend/web/src/services/thunks/gallery.ts @@ -1,45 +1,64 @@ -import { log } from 'app/logging/useLogger'; import { createAppAsyncThunk } from 'app/store/storeUtils'; -import { ImagesService } from 'services/api'; +import { ImagesService, PaginatedResults_ImageDTO_ } from 'services/api'; export const IMAGES_PER_PAGE = 20; -const galleryLog = log.child({ namespace: 'gallery' }); +type ReceivedResultImagesPageThunkConfig = { + rejectValue: { + error: unknown; + }; +}; -export const receivedResultImagesPage = createAppAsyncThunk( +export const receivedResultImagesPage = createAppAsyncThunk< + PaginatedResults_ImageDTO_, + void, + ReceivedResultImagesPageThunkConfig +>( 'results/receivedResultImagesPage', async (_arg, { getState, rejectWithValue }) => { - const { page, pages, nextPage } = getState().results; + const { page, pages, nextPage, upsertedImageCount } = getState().results; - if (nextPage === page) { - rejectWithValue([]); - } + // If many images have been upserted, we need to offset the page number + // TODO: add an offset param to the list images endpoint + const pageOffset = Math.floor(upsertedImageCount / IMAGES_PER_PAGE); const response = await ImagesService.listImagesWithMetadata({ imageType: 'results', imageCategory: 'general', - page: getState().results.nextPage, + page: nextPage + pageOffset, perPage: IMAGES_PER_PAGE, }); - galleryLog.info({ response }, `Received ${response.items.length} results`); - return response; } ); -export const receivedUploadImagesPage = createAppAsyncThunk( +type ReceivedUploadImagesPageThunkConfig = { + rejectValue: { + error: unknown; + }; +}; + +export const receivedUploadImagesPage = createAppAsyncThunk< + PaginatedResults_ImageDTO_, + void, + ReceivedUploadImagesPageThunkConfig +>( 'uploads/receivedUploadImagesPage', - async (_arg, { getState }) => { + async (_arg, { getState, rejectWithValue }) => { + const { page, pages, nextPage, upsertedImageCount } = getState().uploads; + + // If many images have been upserted, we need to offset the page number + // TODO: add an offset param to the list images endpoint + const pageOffset = Math.floor(upsertedImageCount / IMAGES_PER_PAGE); + const response = await ImagesService.listImagesWithMetadata({ imageType: 'uploads', imageCategory: 'general', - page: getState().uploads.nextPage, + page: nextPage + pageOffset, perPage: IMAGES_PER_PAGE, }); - galleryLog.info({ response }, `Received ${response.items.length} uploads`); - return response; } ); diff --git a/invokeai/frontend/web/src/services/thunks/image.ts b/invokeai/frontend/web/src/services/thunks/image.ts index 6831eb647d..f0c0456202 100644 --- a/invokeai/frontend/web/src/services/thunks/image.ts +++ b/invokeai/frontend/web/src/services/thunks/image.ts @@ -1,10 +1,6 @@ -import { log } from 'app/logging/useLogger'; import { createAppAsyncThunk } from 'app/store/storeUtils'; import { InvokeTabName } from 'features/ui/store/tabMap'; import { ImagesService } from 'services/api'; -import { getHeaders } from 'services/util/getHeaders'; - -const imagesLog = log.child({ namespace: 'image' }); type imageUrlsReceivedArg = Parameters< (typeof ImagesService)['getImageUrls'] @@ -17,7 +13,6 @@ export const imageUrlsReceived = createAppAsyncThunk( 'api/imageUrlsReceived', async (arg: imageUrlsReceivedArg) => { const response = await ImagesService.getImageUrls(arg); - imagesLog.info({ arg, response }, 'Received image urls'); return response; } ); @@ -33,7 +28,6 @@ export const imageMetadataReceived = createAppAsyncThunk( 'api/imageMetadataReceived', async (arg: imageMetadataReceivedArg) => { const response = await ImagesService.getImageMetadata(arg); - imagesLog.info({ arg, response }, 'Received image record'); return response; } ); @@ -53,11 +47,7 @@ export const imageUploaded = createAppAsyncThunk( // strip out `activeTabName` from arg - the route does not need it const { activeTabName, ...rest } = arg; const response = await ImagesService.uploadImage(rest); - const { location } = getHeaders(response); - - imagesLog.debug({ arg: '', response, location }, 'Image uploaded'); - - return { response, location }; + return response; } ); @@ -70,9 +60,19 @@ export const imageDeleted = createAppAsyncThunk( 'api/imageDeleted', async (arg: ImageDeletedArg) => { const response = await ImagesService.deleteImage(arg); - - imagesLog.debug({ arg, response }, 'Image deleted'); - + return response; + } +); + +type ImageUpdatedArg = Parameters<(typeof ImagesService)['updateImage']>[0]; + +/** + * `ImagesService.updateImage()` thunk + */ +export const imageUpdated = createAppAsyncThunk( + 'api/imageUpdated', + async (arg: ImageUpdatedArg) => { + const response = await ImagesService.updateImage(arg); return response; } ); diff --git a/invokeai/frontend/web/src/services/thunks/session.ts b/invokeai/frontend/web/src/services/thunks/session.ts index dca4134886..cf87fb30f5 100644 --- a/invokeai/frontend/web/src/services/thunks/session.ts +++ b/invokeai/frontend/web/src/services/thunks/session.ts @@ -1,7 +1,7 @@ import { createAppAsyncThunk } from 'app/store/storeUtils'; -import { SessionsService } from 'services/api'; +import { GraphExecutionState, SessionsService } from 'services/api'; import { log } from 'app/logging/useLogger'; -import { serializeError } from 'serialize-error'; +import { isObject } from 'lodash-es'; const sessionLog = log.child({ namespace: 'session' }); @@ -11,99 +11,89 @@ type SessionCreatedArg = { >[0]['requestBody']; }; +type SessionCreatedThunkConfig = { + rejectValue: { arg: SessionCreatedArg; error: unknown }; +}; + /** * `SessionsService.createSession()` thunk */ -export const sessionCreated = createAppAsyncThunk( - 'api/sessionCreated', - async (arg: SessionCreatedArg, { rejectWithValue }) => { - try { - const response = await SessionsService.createSession({ - requestBody: arg.graph, - }); - sessionLog.info({ arg, response }, `Session created (${response.id})`); - return response; - } catch (err: any) { - sessionLog.error( - { - error: serializeError(err), - }, - 'Problem creating session' - ); - return rejectWithValue(err.message); - } - } -); - -type NodeAddedArg = Parameters<(typeof SessionsService)['addNode']>[0]; - -/** - * `SessionsService.addNode()` thunk - */ -export const nodeAdded = createAppAsyncThunk( - 'api/nodeAdded', - async ( - arg: { node: NodeAddedArg['requestBody']; sessionId: string }, - _thunkApi - ) => { - const response = await SessionsService.addNode({ - requestBody: arg.node, - sessionId: arg.sessionId, +export const sessionCreated = createAppAsyncThunk< + GraphExecutionState, + SessionCreatedArg, + SessionCreatedThunkConfig +>('api/sessionCreated', async (arg, { rejectWithValue }) => { + try { + const response = await SessionsService.createSession({ + requestBody: arg.graph, }); - - sessionLog.info({ arg, response }, `Node added (${response})`); - return response; + } catch (error) { + return rejectWithValue({ arg, error }); } -); +}); + +type SessionInvokedArg = { sessionId: string }; + +type SessionInvokedThunkConfig = { + rejectValue: { + arg: SessionInvokedArg; + error: unknown; + }; +}; + +const isErrorWithStatus = (error: unknown): error is { status: number } => + isObject(error) && 'status' in error; /** * `SessionsService.invokeSession()` thunk */ -export const sessionInvoked = createAppAsyncThunk( - 'api/sessionInvoked', - async (arg: { sessionId: string }, { rejectWithValue }) => { - const { sessionId } = arg; +export const sessionInvoked = createAppAsyncThunk< + void, + SessionInvokedArg, + SessionInvokedThunkConfig +>('api/sessionInvoked', async (arg, { rejectWithValue }) => { + const { sessionId } = arg; - try { - const response = await SessionsService.invokeSession({ - sessionId, - all: true, - }); - sessionLog.info({ arg, response }, `Session invoked (${sessionId})`); - - return response; - } catch (error) { - const err = error as any; - if (err.status === 403) { - return rejectWithValue(err.body.detail); - } - throw error; + try { + const response = await SessionsService.invokeSession({ + sessionId, + all: true, + }); + return response; + } catch (error) { + if (isErrorWithStatus(error) && error.status === 403) { + return rejectWithValue({ arg, error: (error as any).body.detail }); } + return rejectWithValue({ arg, error }); } -); +}); type SessionCanceledArg = Parameters< (typeof SessionsService)['cancelSessionInvoke'] >[0]; - +type SessionCanceledThunkConfig = { + rejectValue: { + arg: SessionCanceledArg; + error: unknown; + }; +}; /** * `SessionsService.cancelSession()` thunk */ -export const sessionCanceled = createAppAsyncThunk( - 'api/sessionCanceled', - async (arg: SessionCanceledArg, _thunkApi) => { - const { sessionId } = arg; +export const sessionCanceled = createAppAsyncThunk< + void, + SessionCanceledArg, + SessionCanceledThunkConfig +>('api/sessionCanceled', async (arg: SessionCanceledArg, _thunkApi) => { + const { sessionId } = arg; - const response = await SessionsService.cancelSessionInvoke({ - sessionId, - }); + const response = await SessionsService.cancelSessionInvoke({ + sessionId, + }); - sessionLog.info({ arg, response }, `Session canceled (${sessionId})`); - - return response; - } -); + return response; +}); type SessionsListedArg = Parameters< (typeof SessionsService)['listSessions']