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 6af959a5bd..44280c3b41 100644 --- a/invokeai/app/invocations/generate.py +++ b/invokeai/app/invocations/generate.py @@ -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 12cebdf41d..11ea7134bb 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -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..c04a3943f3 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -8,7 +8,6 @@ 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 { addUserInvokedCanvasListener } from './listeners/userInvokedCanvas'; @@ -19,6 +18,16 @@ 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'; export const listenerMiddleware = createListenerMiddleware(); @@ -40,15 +49,27 @@ export type AppListenerEffect = ListenerEffect< addImageUploadedListener(); addInitialImageSelectedListener(); -addImageResultReceivedListener(); addRequestedImageDeletionListener(); addUserInvokedCanvasListener(); addUserInvokedNodesListener(); addUserInvokedTextToImageListener(); addUserInvokedImageToImageListener(); +addSessionReadyToInvokeListener(); addCanvasSavedToGalleryListener(); addCanvasDownloadedAsImageListener(); addCanvasCopiedToClipboardListener(); addCanvasMergedListener(); + +// socketio + +addGeneratorProgressListener(); +addGraphExecutionStateCompleteListener(); +addInvocationCompleteListener(); +addInvocationErrorListener(); +addInvocationStartedListener(); +addSocketConnectedListener(); +addSocketDisconnectedListener(); +addSocketSubscribedListener(); +addSocketUnsubscribedListener(); 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..b37cd3d139 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 @@ -12,7 +12,7 @@ export const addImageUploadedListener = () => { startAppListening({ predicate: (action): action is ReturnType => imageUploaded.fulfilled.match(action) && - action.payload.response.image_type !== 'intermediates', + action.payload.response.is_intermediate === false, effect: (action, { dispatch, getState }) => { const { response: image } = action.payload; 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..eb65017a25 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/sessionReadyToInvoke.ts @@ -0,0 +1,19 @@ +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: 'invoke' }); + +export const addSessionReadyToInvokeListener = () => { + startAppListening({ + actionCreator: sessionReadyToInvoke, + effect: (action, { getState, dispatch }) => { + const { sessionId } = getState().system; + if (sessionId) { + moduleLog.info({ sessionId }, `Session invoked (${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..c8ac46f6f1 --- /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, + `Graph execution state complete (${action.payload.data.graph_execution_state_id})` + ); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/invocationComplete.ts similarity index 59% rename from invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts rename to invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/invocationComplete.ts index 0222eea93c..76ae46c4a2 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/invocationComplete.ts @@ -1,40 +1,49 @@ +import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; +import { startAppListening } from '../..'; +import { log } from 'app/logging/useLogger'; 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'; +import { sessionCanceled } from 'services/thunks/session'; +import { isImageOutput } from 'services/types/guards'; +const moduleLog = log.child({ namespace: 'socketio' }); const nodeDenylist = ['dataURL_image']; -export const addImageResultReceivedListener = () => { +export const addInvocationCompleteListener = () => { 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; + actionCreator: invocationComplete, + effect: async (action, { dispatch, getState, take }) => { + moduleLog.info( + 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 URLS + // TODO: is this extraneous? I think so... dispatch( imageUrlsReceived({ imageName: image_name, imageType: image_type }) ); + // Get its metadata dispatch( imageMetadataReceived({ imageName: image_name, 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..d0e4d975be --- /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.debug( + 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..373802fa16 --- /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.info( + 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..46f6efe934 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,96 @@ export const addUserInvokedCanvasListener = () => { const graph = { nodes, edges }; dispatch(canvasGraphBuilt(graph)); + moduleLog({ data: graph }, 'Canvas graph built'); - // Actually create the session + // 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: basePayload }] = 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 + const { image_name: baseName, image_type: baseType } = + basePayload.response; + + baseNode.image = { + image_name: baseName, + image_type: baseType, + }; + } + + // 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: maskPayload }] = 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 + const { image_name: maskName, image_type: maskType } = + maskPayload.response; + + baseNode.mask = { + image_name: maskName, + image_type: maskType, + }; + } + + // 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 +193,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..8940237782 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,7 +12,7 @@ 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); @@ -19,6 +20,10 @@ export const addUserInvokedImageToImageListener = () => { moduleLog({ 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..45dcf7b0b2 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,7 +12,7 @@ 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); @@ -19,6 +20,10 @@ export const addUserInvokedNodesListener = () => { moduleLog({ 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..f7245b9301 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'); 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/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/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/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..a78e0de97b 100644 --- a/invokeai/frontend/web/src/services/events/middleware.ts +++ b/invokeai/frontend/web/src/services/events/middleware.ts @@ -8,7 +8,11 @@ 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 { + sessionInvoked, + sessionCreated, + sessionWithoutGraphCreated, +} from 'services/thunks/session'; import { OpenAPI } from 'services/api'; import { setEventListeners } from 'services/events/util/setEventListeners'; import { log } from 'app/logging/useLogger'; @@ -62,17 +66,14 @@ export const socketMiddleware = () => { socket.connect(); } - if (sessionCreated.fulfilled.match(action)) { + if ( + sessionCreated.fulfilled.match(action) || + sessionWithoutGraphCreated.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 +86,6 @@ export const socketMiddleware = () => { ); } - sessionLog.debug(`Subscribe to new session (${sessionId})`); - socket.emit('subscribe', { session: sessionId }); dispatch( @@ -95,9 +94,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..5321b7ca3e 100644 --- a/invokeai/frontend/web/src/services/thunks/gallery.ts +++ b/invokeai/frontend/web/src/services/thunks/gallery.ts @@ -12,7 +12,7 @@ export const receivedResultImagesPage = createAppAsyncThunk( const { page, pages, nextPage } = getState().results; if (nextPage === page) { - rejectWithValue([]); + return rejectWithValue([]); } const response = await ImagesService.listImagesWithMetadata({ @@ -30,7 +30,13 @@ export const receivedResultImagesPage = createAppAsyncThunk( export const receivedUploadImagesPage = createAppAsyncThunk( 'uploads/receivedUploadImagesPage', - async (_arg, { getState }) => { + async (_arg, { getState, rejectWithValue }) => { + const { page, pages, nextPage } = getState().uploads; + + if (nextPage === page) { + return rejectWithValue([]); + } + const response = await ImagesService.listImagesWithMetadata({ imageType: 'uploads', imageCategory: 'general', diff --git a/invokeai/frontend/web/src/services/thunks/image.ts b/invokeai/frontend/web/src/services/thunks/image.ts index 6831eb647d..34b369e3eb 100644 --- a/invokeai/frontend/web/src/services/thunks/image.ts +++ b/invokeai/frontend/web/src/services/thunks/image.ts @@ -76,3 +76,19 @@ export const imageDeleted = createAppAsyncThunk( return response; } ); + +type ImageUpdatedArg = Parameters<(typeof ImagesService)['updateImage']>[0]; + +/** + * `ImagesService.deleteImage()` thunk + */ +export const imageUpdated = createAppAsyncThunk( + 'api/imageUpdated', + async (arg: ImageUpdatedArg) => { + const response = await ImagesService.updateImage(arg); + + imagesLog.debug({ arg, response }, 'Image updated'); + + return response; + } +); diff --git a/invokeai/frontend/web/src/services/thunks/session.ts b/invokeai/frontend/web/src/services/thunks/session.ts index dca4134886..a1ee5a34ed 100644 --- a/invokeai/frontend/web/src/services/thunks/session.ts +++ b/invokeai/frontend/web/src/services/thunks/session.ts @@ -35,6 +35,28 @@ export const sessionCreated = createAppAsyncThunk( } ); +/** + * `SessionsService.createSession()` without graph thunk + */ +export const sessionWithoutGraphCreated = createAppAsyncThunk( + 'api/sessionWithoutGraphCreated', + async (_, { rejectWithValue }) => { + try { + const response = await SessionsService.createSession({}); + sessionLog.info({ 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]; /** @@ -57,6 +79,29 @@ export const nodeAdded = createAppAsyncThunk( } ); +type NodeUpdatedArg = Parameters<(typeof SessionsService)['updateNode']>[0]; + +/** + * `SessionsService.addNode()` thunk + */ +export const nodeUpdated = createAppAsyncThunk( + 'api/nodeUpdated', + async ( + arg: { node: NodeUpdatedArg['requestBody']; sessionId: string }, + _thunkApi + ) => { + const response = await SessionsService.updateNode({ + requestBody: arg.node, + sessionId: arg.sessionId, + nodePath: arg.node.id, + }); + + sessionLog.info({ arg, response }, `Node updated (${response})`); + + return response; + } +); + /** * `SessionsService.invokeSession()` thunk */