diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index b739327368..b7b6477937 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -2,7 +2,6 @@ from logging import Logger -from invokeai.app.services.workflow_image_records.workflow_image_records_sqlite import SqliteWorkflowImageRecordsStorage from invokeai.backend.util.logging import InvokeAILogger from invokeai.version.invokeai_version import __version__ @@ -30,7 +29,7 @@ from ..services.session_processor.session_processor_default import DefaultSessio from ..services.session_queue.session_queue_sqlite import SqliteSessionQueue from ..services.shared.default_graphs import create_system_graphs from ..services.shared.graph import GraphExecutionState, LibraryGraph -from ..services.shared.sqlite import SqliteDatabase +from ..services.shared.sqlite.sqlite_database import SqliteDatabase from ..services.urls.urls_default import LocalUrlService from ..services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage from .events import FastAPIEventService @@ -94,7 +93,6 @@ class ApiDependencies: session_processor = DefaultSessionProcessor() session_queue = SqliteSessionQueue(db=db) urls = LocalUrlService() - workflow_image_records = SqliteWorkflowImageRecordsStorage(db=db) workflow_records = SqliteWorkflowRecordsStorage(db=db) services = InvocationServices( @@ -121,14 +119,12 @@ class ApiDependencies: session_processor=session_processor, session_queue=session_queue, urls=urls, - workflow_image_records=workflow_image_records, workflow_records=workflow_records, ) create_system_graphs(services.graph_library) ApiDependencies.invoker = Invoker(services) - db.clean() @staticmethod diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index e8c8c693b3..125896b8d3 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -8,10 +8,11 @@ from fastapi.routing import APIRouter from PIL import Image from pydantic import BaseModel, Field, ValidationError -from invokeai.app.invocations.baseinvocation import MetadataField, MetadataFieldValidator, WorkflowFieldValidator +from invokeai.app.invocations.baseinvocation import MetadataField, MetadataFieldValidator from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO from invokeai.app.services.shared.pagination import OffsetPaginatedResults +from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID, WorkflowWithoutIDValidator from ..dependencies import ApiDependencies @@ -73,7 +74,7 @@ async def upload_image( workflow_raw = pil_image.info.get("invokeai_workflow", None) if workflow_raw is not None: try: - workflow = WorkflowFieldValidator.validate_json(workflow_raw) + workflow = WorkflowWithoutIDValidator.validate_json(workflow_raw) except ValidationError: ApiDependencies.invoker.services.logger.warn("Failed to parse metadata for uploaded image") pass @@ -184,6 +185,18 @@ async def get_image_metadata( raise HTTPException(status_code=404) +@images_router.get( + "/i/{image_name}/workflow", operation_id="get_image_workflow", response_model=Optional[WorkflowWithoutID] +) +async def get_image_workflow( + image_name: str = Path(description="The name of image whose workflow to get"), +) -> Optional[WorkflowWithoutID]: + try: + return ApiDependencies.invoker.services.images.get_workflow(image_name) + except Exception: + raise HTTPException(status_code=404) + + @images_router.api_route( "/i/{image_name}/full", methods=["GET", "HEAD"], diff --git a/invokeai/app/api/routers/workflows.py b/invokeai/app/api/routers/workflows.py index 36de31fb51..6e93d6d0ce 100644 --- a/invokeai/app/api/routers/workflows.py +++ b/invokeai/app/api/routers/workflows.py @@ -1,7 +1,19 @@ -from fastapi import APIRouter, Path +from typing import Optional + +from fastapi import APIRouter, Body, HTTPException, Path, Query from invokeai.app.api.dependencies import ApiDependencies -from invokeai.app.invocations.baseinvocation import WorkflowField +from invokeai.app.services.shared.pagination import PaginatedResults +from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection +from invokeai.app.services.workflow_records.workflow_records_common import ( + Workflow, + WorkflowCategory, + WorkflowNotFoundError, + WorkflowRecordDTO, + WorkflowRecordListItemDTO, + WorkflowRecordOrderBy, + WorkflowWithoutID, +) workflows_router = APIRouter(prefix="/v1/workflows", tags=["workflows"]) @@ -10,11 +22,76 @@ workflows_router = APIRouter(prefix="/v1/workflows", tags=["workflows"]) "/i/{workflow_id}", operation_id="get_workflow", responses={ - 200: {"model": WorkflowField}, + 200: {"model": WorkflowRecordDTO}, }, ) async def get_workflow( workflow_id: str = Path(description="The workflow to get"), -) -> WorkflowField: +) -> WorkflowRecordDTO: """Gets a workflow""" - return ApiDependencies.invoker.services.workflow_records.get(workflow_id) + try: + return ApiDependencies.invoker.services.workflow_records.get(workflow_id) + except WorkflowNotFoundError: + raise HTTPException(status_code=404, detail="Workflow not found") + + +@workflows_router.patch( + "/i/{workflow_id}", + operation_id="update_workflow", + responses={ + 200: {"model": WorkflowRecordDTO}, + }, +) +async def update_workflow( + workflow: Workflow = Body(description="The updated workflow", embed=True), +) -> WorkflowRecordDTO: + """Updates a workflow""" + return ApiDependencies.invoker.services.workflow_records.update(workflow=workflow) + + +@workflows_router.delete( + "/i/{workflow_id}", + operation_id="delete_workflow", +) +async def delete_workflow( + workflow_id: str = Path(description="The workflow to delete"), +) -> None: + """Deletes a workflow""" + ApiDependencies.invoker.services.workflow_records.delete(workflow_id) + + +@workflows_router.post( + "/", + operation_id="create_workflow", + responses={ + 200: {"model": WorkflowRecordDTO}, + }, +) +async def create_workflow( + workflow: WorkflowWithoutID = Body(description="The workflow to create", embed=True), +) -> WorkflowRecordDTO: + """Creates a workflow""" + return ApiDependencies.invoker.services.workflow_records.create(workflow=workflow) + + +@workflows_router.get( + "/", + operation_id="list_workflows", + responses={ + 200: {"model": PaginatedResults[WorkflowRecordListItemDTO]}, + }, +) +async def list_workflows( + page: int = Query(default=0, description="The page to get"), + per_page: int = Query(default=10, description="The number of workflows per page"), + order_by: WorkflowRecordOrderBy = Query( + default=WorkflowRecordOrderBy.Name, description="The attribute to order by" + ), + direction: SQLiteDirection = Query(default=SQLiteDirection.Ascending, description="The direction to order by"), + category: WorkflowCategory = Query(default=WorkflowCategory.User, description="The category of workflow to get"), + query: Optional[str] = Query(default=None, description="The text to query by (matches name and description)"), +) -> PaginatedResults[WorkflowRecordListItemDTO]: + """Gets a page of workflows""" + return ApiDependencies.invoker.services.workflow_records.get_many( + page=page, per_page=per_page, order_by=order_by, direction=direction, query=query, category=category + ) diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index b93e4f922f..5e1d86994d 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -16,6 +16,7 @@ from pydantic.fields import FieldInfo, _Unset from pydantic_core import PydanticUndefined from invokeai.app.services.config.config_default import InvokeAIAppConfig +from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID from invokeai.app.shared.fields import FieldDescriptions from invokeai.app.util.metaenum import MetaEnum from invokeai.app.util.misc import uuid_string @@ -452,6 +453,7 @@ class InvocationContext: queue_id: str queue_item_id: int queue_batch_id: str + workflow: Optional[WorkflowWithoutID] def __init__( self, @@ -460,12 +462,14 @@ class InvocationContext: queue_item_id: int, queue_batch_id: str, graph_execution_state_id: str, + workflow: Optional[WorkflowWithoutID], ): self.services = services self.graph_execution_state_id = graph_execution_state_id self.queue_id = queue_id self.queue_item_id = queue_item_id self.queue_batch_id = queue_batch_id + self.workflow = workflow class BaseInvocationOutput(BaseModel): @@ -807,9 +811,9 @@ def invocation( cls.UIConfig.category = category # Grab the node pack's name from the module name, if it's a custom node - module_name = cls.__module__.split(".")[0] - if module_name.endswith(CUSTOM_NODE_PACK_SUFFIX): - cls.UIConfig.node_pack = module_name.split(CUSTOM_NODE_PACK_SUFFIX)[0] + is_custom_node = cls.__module__.rsplit(".", 1)[0] == "invokeai.app.invocations" + if is_custom_node: + cls.UIConfig.node_pack = cls.__module__.split(".")[0] else: cls.UIConfig.node_pack = None @@ -903,24 +907,6 @@ def invocation_output( return wrapper -class WorkflowField(RootModel): - """ - Pydantic model for workflows with custom root of type dict[str, Any]. - Workflows are stored without a strict schema. - """ - - root: dict[str, Any] = Field(description="The workflow") - - -WorkflowFieldValidator = TypeAdapter(WorkflowField) - - -class WithWorkflow(BaseModel): - workflow: Optional[WorkflowField] = Field( - default=None, description=FieldDescriptions.workflow, json_schema_extra={"field_kind": FieldKind.NodeAttribute} - ) - - class MetadataField(RootModel): """ Pydantic model for metadata with custom root of type dict[str, Any]. @@ -943,3 +929,13 @@ class WithMetadata(BaseModel): orig_required=False, ).model_dump(exclude_none=True), ) + + +class WithWorkflow: + workflow = None + + def __init_subclass__(cls) -> None: + logger.warn( + f"{cls.__module__.split('.')[0]}.{cls.__name__}: WithWorkflow is deprecated. Use `context.workflow` to access the workflow." + ) + super().__init_subclass__() diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index d57de57a37..5c112242d9 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -39,7 +39,6 @@ from .baseinvocation import ( InvocationContext, OutputField, WithMetadata, - WithWorkflow, invocation, invocation_output, ) @@ -129,7 +128,7 @@ class ControlNetInvocation(BaseInvocation): # This invocation exists for other invocations to subclass it - do not register with @invocation! -class ImageProcessorInvocation(BaseInvocation, WithMetadata, WithWorkflow): +class ImageProcessorInvocation(BaseInvocation, WithMetadata): """Base class for invocations that preprocess images for ControlNet""" image: ImageField = InputField(description="The image to process") @@ -153,7 +152,7 @@ class ImageProcessorInvocation(BaseInvocation, WithMetadata, WithWorkflow): node_id=self.id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) """Builds an ImageOutput and its ImageField""" @@ -173,7 +172,7 @@ class ImageProcessorInvocation(BaseInvocation, WithMetadata, WithWorkflow): title="Canny Processor", tags=["controlnet", "canny"], category="controlnet", - version="1.1.0", + version="1.2.0", ) class CannyImageProcessorInvocation(ImageProcessorInvocation): """Canny edge detection for ControlNet""" @@ -196,7 +195,7 @@ class CannyImageProcessorInvocation(ImageProcessorInvocation): title="HED (softedge) Processor", tags=["controlnet", "hed", "softedge"], category="controlnet", - version="1.1.0", + version="1.2.0", ) class HedImageProcessorInvocation(ImageProcessorInvocation): """Applies HED edge detection to image""" @@ -225,7 +224,7 @@ class HedImageProcessorInvocation(ImageProcessorInvocation): title="Lineart Processor", tags=["controlnet", "lineart"], category="controlnet", - version="1.1.0", + version="1.2.0", ) class LineartImageProcessorInvocation(ImageProcessorInvocation): """Applies line art processing to image""" @@ -247,7 +246,7 @@ class LineartImageProcessorInvocation(ImageProcessorInvocation): title="Lineart Anime Processor", tags=["controlnet", "lineart", "anime"], category="controlnet", - version="1.1.0", + version="1.2.0", ) class LineartAnimeImageProcessorInvocation(ImageProcessorInvocation): """Applies line art anime processing to image""" @@ -270,7 +269,7 @@ class LineartAnimeImageProcessorInvocation(ImageProcessorInvocation): title="Openpose Processor", tags=["controlnet", "openpose", "pose"], category="controlnet", - version="1.1.0", + version="1.2.0", ) class OpenposeImageProcessorInvocation(ImageProcessorInvocation): """Applies Openpose processing to image""" @@ -295,7 +294,7 @@ class OpenposeImageProcessorInvocation(ImageProcessorInvocation): title="Midas Depth Processor", tags=["controlnet", "midas"], category="controlnet", - version="1.1.0", + version="1.2.0", ) class MidasDepthImageProcessorInvocation(ImageProcessorInvocation): """Applies Midas depth processing to image""" @@ -322,7 +321,7 @@ class MidasDepthImageProcessorInvocation(ImageProcessorInvocation): title="Normal BAE Processor", tags=["controlnet"], category="controlnet", - version="1.1.0", + version="1.2.0", ) class NormalbaeImageProcessorInvocation(ImageProcessorInvocation): """Applies NormalBae processing to image""" @@ -339,7 +338,7 @@ class NormalbaeImageProcessorInvocation(ImageProcessorInvocation): @invocation( - "mlsd_image_processor", title="MLSD Processor", tags=["controlnet", "mlsd"], category="controlnet", version="1.1.0" + "mlsd_image_processor", title="MLSD Processor", tags=["controlnet", "mlsd"], category="controlnet", version="1.2.0" ) class MlsdImageProcessorInvocation(ImageProcessorInvocation): """Applies MLSD processing to image""" @@ -362,7 +361,7 @@ class MlsdImageProcessorInvocation(ImageProcessorInvocation): @invocation( - "pidi_image_processor", title="PIDI Processor", tags=["controlnet", "pidi"], category="controlnet", version="1.1.0" + "pidi_image_processor", title="PIDI Processor", tags=["controlnet", "pidi"], category="controlnet", version="1.2.0" ) class PidiImageProcessorInvocation(ImageProcessorInvocation): """Applies PIDI processing to image""" @@ -389,7 +388,7 @@ class PidiImageProcessorInvocation(ImageProcessorInvocation): title="Content Shuffle Processor", tags=["controlnet", "contentshuffle"], category="controlnet", - version="1.1.0", + version="1.2.0", ) class ContentShuffleImageProcessorInvocation(ImageProcessorInvocation): """Applies content shuffle processing to image""" @@ -419,7 +418,7 @@ class ContentShuffleImageProcessorInvocation(ImageProcessorInvocation): title="Zoe (Depth) Processor", tags=["controlnet", "zoe", "depth"], category="controlnet", - version="1.1.0", + version="1.2.0", ) class ZoeDepthImageProcessorInvocation(ImageProcessorInvocation): """Applies Zoe depth processing to image""" @@ -435,7 +434,7 @@ class ZoeDepthImageProcessorInvocation(ImageProcessorInvocation): title="Mediapipe Face Processor", tags=["controlnet", "mediapipe", "face"], category="controlnet", - version="1.1.0", + version="1.2.0", ) class MediapipeFaceProcessorInvocation(ImageProcessorInvocation): """Applies mediapipe face processing to image""" @@ -458,7 +457,7 @@ class MediapipeFaceProcessorInvocation(ImageProcessorInvocation): title="Leres (Depth) Processor", tags=["controlnet", "leres", "depth"], category="controlnet", - version="1.1.0", + version="1.2.0", ) class LeresImageProcessorInvocation(ImageProcessorInvocation): """Applies leres processing to image""" @@ -487,7 +486,7 @@ class LeresImageProcessorInvocation(ImageProcessorInvocation): title="Tile Resample Processor", tags=["controlnet", "tile"], category="controlnet", - version="1.1.0", + version="1.2.0", ) class TileResamplerProcessorInvocation(ImageProcessorInvocation): """Tile resampler processor""" @@ -527,7 +526,7 @@ class TileResamplerProcessorInvocation(ImageProcessorInvocation): title="Segment Anything Processor", tags=["controlnet", "segmentanything"], category="controlnet", - version="1.1.0", + version="1.2.0", ) class SegmentAnythingProcessorInvocation(ImageProcessorInvocation): """Applies segment anything processing to image""" @@ -569,7 +568,7 @@ class SamDetectorReproducibleColors(SamDetector): title="Color Map Processor", tags=["controlnet"], category="controlnet", - version="1.1.0", + version="1.2.0", ) class ColorMapImageProcessorInvocation(ImageProcessorInvocation): """Generates a color map from the provided image""" diff --git a/invokeai/app/invocations/custom_nodes/init.py b/invokeai/app/invocations/custom_nodes/init.py index e26bdaf568..e0c174013f 100644 --- a/invokeai/app/invocations/custom_nodes/init.py +++ b/invokeai/app/invocations/custom_nodes/init.py @@ -6,7 +6,6 @@ import sys from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path -from invokeai.app.invocations.baseinvocation import CUSTOM_NODE_PACK_SUFFIX from invokeai.backend.util.logging import InvokeAILogger logger = InvokeAILogger.get_logger() @@ -34,7 +33,7 @@ for d in Path(__file__).parent.iterdir(): continue # load the module, appending adding a suffix to identify it as a custom node pack - spec = spec_from_file_location(f"{module_name}{CUSTOM_NODE_PACK_SUFFIX}", init.absolute()) + spec = spec_from_file_location(module_name, init.absolute()) if spec is None or spec.loader is None: logger.warn(f"Could not load {init}") diff --git a/invokeai/app/invocations/cv.py b/invokeai/app/invocations/cv.py index b9764285f5..cb6828d21a 100644 --- a/invokeai/app/invocations/cv.py +++ b/invokeai/app/invocations/cv.py @@ -8,11 +8,11 @@ from PIL import Image, ImageOps from invokeai.app.invocations.primitives import ImageField, ImageOutput from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin -from .baseinvocation import BaseInvocation, InputField, InvocationContext, WithMetadata, WithWorkflow, invocation +from .baseinvocation import BaseInvocation, InputField, InvocationContext, WithMetadata, invocation -@invocation("cv_inpaint", title="OpenCV Inpaint", tags=["opencv", "inpaint"], category="inpaint", version="1.1.0") -class CvInpaintInvocation(BaseInvocation, WithMetadata, WithWorkflow): +@invocation("cv_inpaint", title="OpenCV Inpaint", tags=["opencv", "inpaint"], category="inpaint", version="1.2.0") +class CvInpaintInvocation(BaseInvocation, WithMetadata): """Simple inpaint using opencv.""" image: ImageField = InputField(description="The image to inpaint") @@ -41,7 +41,7 @@ class CvInpaintInvocation(BaseInvocation, WithMetadata, WithWorkflow): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( diff --git a/invokeai/app/invocations/facetools.py b/invokeai/app/invocations/facetools.py index ef3d0aa9a1..e0c89b4de5 100644 --- a/invokeai/app/invocations/facetools.py +++ b/invokeai/app/invocations/facetools.py @@ -17,7 +17,6 @@ from invokeai.app.invocations.baseinvocation import ( InvocationContext, OutputField, WithMetadata, - WithWorkflow, invocation, invocation_output, ) @@ -438,8 +437,8 @@ def get_faces_list( return all_faces -@invocation("face_off", title="FaceOff", tags=["image", "faceoff", "face", "mask"], category="image", version="1.1.0") -class FaceOffInvocation(BaseInvocation, WithWorkflow, WithMetadata): +@invocation("face_off", title="FaceOff", tags=["image", "faceoff", "face", "mask"], category="image", version="1.2.0") +class FaceOffInvocation(BaseInvocation, WithMetadata): """Bound, extract, and mask a face from an image using MediaPipe detection""" image: ImageField = InputField(description="Image for face detection") @@ -508,7 +507,7 @@ class FaceOffInvocation(BaseInvocation, WithWorkflow, WithMetadata): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, - workflow=self.workflow, + workflow=context.workflow, ) mask_dto = context.services.images.create( @@ -532,8 +531,8 @@ class FaceOffInvocation(BaseInvocation, WithWorkflow, WithMetadata): return output -@invocation("face_mask_detection", title="FaceMask", tags=["image", "face", "mask"], category="image", version="1.1.0") -class FaceMaskInvocation(BaseInvocation, WithWorkflow, WithMetadata): +@invocation("face_mask_detection", title="FaceMask", tags=["image", "face", "mask"], category="image", version="1.2.0") +class FaceMaskInvocation(BaseInvocation, WithMetadata): """Face mask creation using mediapipe face detection""" image: ImageField = InputField(description="Image to face detect") @@ -627,7 +626,7 @@ class FaceMaskInvocation(BaseInvocation, WithWorkflow, WithMetadata): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, - workflow=self.workflow, + workflow=context.workflow, ) mask_dto = context.services.images.create( @@ -650,9 +649,9 @@ class FaceMaskInvocation(BaseInvocation, WithWorkflow, WithMetadata): @invocation( - "face_identifier", title="FaceIdentifier", tags=["image", "face", "identifier"], category="image", version="1.1.0" + "face_identifier", title="FaceIdentifier", tags=["image", "face", "identifier"], category="image", version="1.2.0" ) -class FaceIdentifierInvocation(BaseInvocation, WithWorkflow, WithMetadata): +class FaceIdentifierInvocation(BaseInvocation, WithMetadata): """Outputs an image with detected face IDs printed on each face. For use with other FaceTools.""" image: ImageField = InputField(description="Image to face detect") @@ -716,7 +715,7 @@ class FaceIdentifierInvocation(BaseInvocation, WithWorkflow, WithMetadata): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index ad3b3aec71..9f37aca13f 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -13,7 +13,7 @@ from invokeai.app.shared.fields import FieldDescriptions from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark from invokeai.backend.image_util.safety_checker import SafetyChecker -from .baseinvocation import BaseInvocation, Input, InputField, InvocationContext, WithMetadata, WithWorkflow, invocation +from .baseinvocation import BaseInvocation, Input, InputField, InvocationContext, WithMetadata, invocation @invocation("show_image", title="Show Image", tags=["image"], category="image", version="1.0.0") @@ -36,8 +36,14 @@ class ShowImageInvocation(BaseInvocation): ) -@invocation("blank_image", title="Blank Image", tags=["image"], category="image", version="1.1.0") -class BlankImageInvocation(BaseInvocation, WithMetadata, WithWorkflow): +@invocation( + "blank_image", + title="Blank Image", + tags=["image"], + category="image", + version="1.2.0", +) +class BlankImageInvocation(BaseInvocation, WithMetadata): """Creates a blank image and forwards it to the pipeline""" width: int = InputField(default=512, description="The width of the image") @@ -56,7 +62,7 @@ class BlankImageInvocation(BaseInvocation, WithMetadata, WithWorkflow): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -66,8 +72,14 @@ class BlankImageInvocation(BaseInvocation, WithMetadata, WithWorkflow): ) -@invocation("img_crop", title="Crop Image", tags=["image", "crop"], category="image", version="1.1.0") -class ImageCropInvocation(BaseInvocation, WithWorkflow, WithMetadata): +@invocation( + "img_crop", + title="Crop Image", + tags=["image", "crop"], + category="image", + version="1.2.0", +) +class ImageCropInvocation(BaseInvocation, WithMetadata): """Crops an image to a specified box. The box can be outside of the image.""" image: ImageField = InputField(description="The image to crop") @@ -90,7 +102,7 @@ class ImageCropInvocation(BaseInvocation, WithWorkflow, WithMetadata): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -101,11 +113,11 @@ class ImageCropInvocation(BaseInvocation, WithWorkflow, WithMetadata): @invocation( - invocation_type="img_pad_crop", - title="Center Pad or Crop Image", + "img_paste", + title="Paste Image", + tags=["image", "paste"], category="image", - tags=["image", "pad", "crop"], - version="1.0.0", + version="1.2.0", ) class CenterPadCropInvocation(BaseInvocation): """Pad or crop an image's sides from the center by specified pixels. Positive values are outside of the image.""" @@ -155,8 +167,14 @@ class CenterPadCropInvocation(BaseInvocation): ) -@invocation("img_paste", title="Paste Image", tags=["image", "paste"], category="image", version="1.1.0") -class ImagePasteInvocation(BaseInvocation, WithWorkflow, WithMetadata): +@invocation( + invocation_type="img_pad_crop", + title="Center Pad or Crop Image", + category="image", + tags=["image", "pad", "crop"], + version="1.0.0", +) +class ImagePasteInvocation(BaseInvocation, WithMetadata): """Pastes an image into another image.""" base_image: ImageField = InputField(description="The base image") @@ -199,7 +217,7 @@ class ImagePasteInvocation(BaseInvocation, WithWorkflow, WithMetadata): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -209,8 +227,14 @@ class ImagePasteInvocation(BaseInvocation, WithWorkflow, WithMetadata): ) -@invocation("tomask", title="Mask from Alpha", tags=["image", "mask"], category="image", version="1.1.0") -class MaskFromAlphaInvocation(BaseInvocation, WithWorkflow, WithMetadata): +@invocation( + "tomask", + title="Mask from Alpha", + tags=["image", "mask"], + category="image", + version="1.2.0", +) +class MaskFromAlphaInvocation(BaseInvocation, WithMetadata): """Extracts the alpha channel of an image as a mask.""" image: ImageField = InputField(description="The image to create the mask from") @@ -231,7 +255,7 @@ class MaskFromAlphaInvocation(BaseInvocation, WithWorkflow, WithMetadata): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -241,8 +265,14 @@ class MaskFromAlphaInvocation(BaseInvocation, WithWorkflow, WithMetadata): ) -@invocation("img_mul", title="Multiply Images", tags=["image", "multiply"], category="image", version="1.1.0") -class ImageMultiplyInvocation(BaseInvocation, WithWorkflow, WithMetadata): +@invocation( + "img_mul", + title="Multiply Images", + tags=["image", "multiply"], + category="image", + version="1.2.0", +) +class ImageMultiplyInvocation(BaseInvocation, WithMetadata): """Multiplies two images together using `PIL.ImageChops.multiply()`.""" image1: ImageField = InputField(description="The first image to multiply") @@ -262,7 +292,7 @@ class ImageMultiplyInvocation(BaseInvocation, WithWorkflow, WithMetadata): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -275,8 +305,14 @@ class ImageMultiplyInvocation(BaseInvocation, WithWorkflow, WithMetadata): IMAGE_CHANNELS = Literal["A", "R", "G", "B"] -@invocation("img_chan", title="Extract Image Channel", tags=["image", "channel"], category="image", version="1.1.0") -class ImageChannelInvocation(BaseInvocation, WithWorkflow, WithMetadata): +@invocation( + "img_chan", + title="Extract Image Channel", + tags=["image", "channel"], + category="image", + version="1.2.0", +) +class ImageChannelInvocation(BaseInvocation, WithMetadata): """Gets a channel from an image.""" image: ImageField = InputField(description="The image to get the channel from") @@ -295,7 +331,7 @@ class ImageChannelInvocation(BaseInvocation, WithWorkflow, WithMetadata): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -308,8 +344,14 @@ class ImageChannelInvocation(BaseInvocation, WithWorkflow, WithMetadata): IMAGE_MODES = Literal["L", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F"] -@invocation("img_conv", title="Convert Image Mode", tags=["image", "convert"], category="image", version="1.1.0") -class ImageConvertInvocation(BaseInvocation, WithWorkflow, WithMetadata): +@invocation( + "img_conv", + title="Convert Image Mode", + tags=["image", "convert"], + category="image", + version="1.2.0", +) +class ImageConvertInvocation(BaseInvocation, WithMetadata): """Converts an image to a different mode.""" image: ImageField = InputField(description="The image to convert") @@ -328,7 +370,7 @@ class ImageConvertInvocation(BaseInvocation, WithWorkflow, WithMetadata): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -338,8 +380,14 @@ class ImageConvertInvocation(BaseInvocation, WithWorkflow, WithMetadata): ) -@invocation("img_blur", title="Blur Image", tags=["image", "blur"], category="image", version="1.1.0") -class ImageBlurInvocation(BaseInvocation, WithWorkflow, WithMetadata): +@invocation( + "img_blur", + title="Blur Image", + tags=["image", "blur"], + category="image", + version="1.2.0", +) +class ImageBlurInvocation(BaseInvocation, WithMetadata): """Blurs an image""" image: ImageField = InputField(description="The image to blur") @@ -363,7 +411,7 @@ class ImageBlurInvocation(BaseInvocation, WithWorkflow, WithMetadata): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -393,8 +441,14 @@ PIL_RESAMPLING_MAP = { } -@invocation("img_resize", title="Resize Image", tags=["image", "resize"], category="image", version="1.1.0") -class ImageResizeInvocation(BaseInvocation, WithMetadata, WithWorkflow): +@invocation( + "img_resize", + title="Resize Image", + tags=["image", "resize"], + category="image", + version="1.2.0", +) +class ImageResizeInvocation(BaseInvocation, WithMetadata): """Resizes an image to specific dimensions""" image: ImageField = InputField(description="The image to resize") @@ -420,7 +474,7 @@ class ImageResizeInvocation(BaseInvocation, WithMetadata, WithWorkflow): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -430,8 +484,14 @@ class ImageResizeInvocation(BaseInvocation, WithMetadata, WithWorkflow): ) -@invocation("img_scale", title="Scale Image", tags=["image", "scale"], category="image", version="1.1.0") -class ImageScaleInvocation(BaseInvocation, WithMetadata, WithWorkflow): +@invocation( + "img_scale", + title="Scale Image", + tags=["image", "scale"], + category="image", + version="1.2.0", +) +class ImageScaleInvocation(BaseInvocation, WithMetadata): """Scales an image by a factor""" image: ImageField = InputField(description="The image to scale") @@ -462,7 +522,7 @@ class ImageScaleInvocation(BaseInvocation, WithMetadata, WithWorkflow): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -472,8 +532,14 @@ class ImageScaleInvocation(BaseInvocation, WithMetadata, WithWorkflow): ) -@invocation("img_lerp", title="Lerp Image", tags=["image", "lerp"], category="image", version="1.1.0") -class ImageLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata): +@invocation( + "img_lerp", + title="Lerp Image", + tags=["image", "lerp"], + category="image", + version="1.2.0", +) +class ImageLerpInvocation(BaseInvocation, WithMetadata): """Linear interpolation of all pixels of an image""" image: ImageField = InputField(description="The image to lerp") @@ -496,7 +562,7 @@ class ImageLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -506,8 +572,14 @@ class ImageLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata): ) -@invocation("img_ilerp", title="Inverse Lerp Image", tags=["image", "ilerp"], category="image", version="1.1.0") -class ImageInverseLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata): +@invocation( + "img_ilerp", + title="Inverse Lerp Image", + tags=["image", "ilerp"], + category="image", + version="1.2.0", +) +class ImageInverseLerpInvocation(BaseInvocation, WithMetadata): """Inverse linear interpolation of all pixels of an image""" image: ImageField = InputField(description="The image to lerp") @@ -530,7 +602,7 @@ class ImageInverseLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -540,8 +612,14 @@ class ImageInverseLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata): ) -@invocation("img_nsfw", title="Blur NSFW Image", tags=["image", "nsfw"], category="image", version="1.1.0") -class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithWorkflow): +@invocation( + "img_nsfw", + title="Blur NSFW Image", + tags=["image", "nsfw"], + category="image", + version="1.2.0", +) +class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata): """Add blur to NSFW-flagged images""" image: ImageField = InputField(description="The image to check") @@ -566,7 +644,7 @@ class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithWorkflow): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -587,9 +665,9 @@ class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithWorkflow): title="Add Invisible Watermark", tags=["image", "watermark"], category="image", - version="1.1.0", + version="1.2.0", ) -class ImageWatermarkInvocation(BaseInvocation, WithMetadata, WithWorkflow): +class ImageWatermarkInvocation(BaseInvocation, WithMetadata): """Add an invisible watermark to an image""" image: ImageField = InputField(description="The image to check") @@ -606,7 +684,7 @@ class ImageWatermarkInvocation(BaseInvocation, WithMetadata, WithWorkflow): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -616,8 +694,14 @@ class ImageWatermarkInvocation(BaseInvocation, WithMetadata, WithWorkflow): ) -@invocation("mask_edge", title="Mask Edge", tags=["image", "mask", "inpaint"], category="image", version="1.1.0") -class MaskEdgeInvocation(BaseInvocation, WithWorkflow, WithMetadata): +@invocation( + "mask_edge", + title="Mask Edge", + tags=["image", "mask", "inpaint"], + category="image", + version="1.2.0", +) +class MaskEdgeInvocation(BaseInvocation, WithMetadata): """Applies an edge mask to an image""" image: ImageField = InputField(description="The image to apply the mask to") @@ -652,7 +736,7 @@ class MaskEdgeInvocation(BaseInvocation, WithWorkflow, WithMetadata): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -667,9 +751,9 @@ class MaskEdgeInvocation(BaseInvocation, WithWorkflow, WithMetadata): title="Combine Masks", tags=["image", "mask", "multiply"], category="image", - version="1.1.0", + version="1.2.0", ) -class MaskCombineInvocation(BaseInvocation, WithWorkflow, WithMetadata): +class MaskCombineInvocation(BaseInvocation, WithMetadata): """Combine two masks together by multiplying them using `PIL.ImageChops.multiply()`.""" mask1: ImageField = InputField(description="The first mask to combine") @@ -689,7 +773,7 @@ class MaskCombineInvocation(BaseInvocation, WithWorkflow, WithMetadata): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -699,8 +783,14 @@ class MaskCombineInvocation(BaseInvocation, WithWorkflow, WithMetadata): ) -@invocation("color_correct", title="Color Correct", tags=["image", "color"], category="image", version="1.1.0") -class ColorCorrectInvocation(BaseInvocation, WithWorkflow, WithMetadata): +@invocation( + "color_correct", + title="Color Correct", + tags=["image", "color"], + category="image", + version="1.2.0", +) +class ColorCorrectInvocation(BaseInvocation, WithMetadata): """ Shifts the colors of a target image to match the reference image, optionally using a mask to only color-correct certain regions of the target image. @@ -800,7 +890,7 @@ class ColorCorrectInvocation(BaseInvocation, WithWorkflow, WithMetadata): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -810,8 +900,14 @@ class ColorCorrectInvocation(BaseInvocation, WithWorkflow, WithMetadata): ) -@invocation("img_hue_adjust", title="Adjust Image Hue", tags=["image", "hue"], category="image", version="1.1.0") -class ImageHueAdjustmentInvocation(BaseInvocation, WithWorkflow, WithMetadata): +@invocation( + "img_hue_adjust", + title="Adjust Image Hue", + tags=["image", "hue"], + category="image", + version="1.2.0", +) +class ImageHueAdjustmentInvocation(BaseInvocation, WithMetadata): """Adjusts the Hue of an image.""" image: ImageField = InputField(description="The image to adjust") @@ -840,7 +936,7 @@ class ImageHueAdjustmentInvocation(BaseInvocation, WithWorkflow, WithMetadata): is_intermediate=self.is_intermediate, session_id=context.graph_execution_state_id, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -913,9 +1009,9 @@ CHANNEL_FORMATS = { "value", ], category="image", - version="1.1.0", + version="1.2.0", ) -class ImageChannelOffsetInvocation(BaseInvocation, WithWorkflow, WithMetadata): +class ImageChannelOffsetInvocation(BaseInvocation, WithMetadata): """Add or subtract a value from a specific color channel of an image.""" image: ImageField = InputField(description="The image to adjust") @@ -950,7 +1046,7 @@ class ImageChannelOffsetInvocation(BaseInvocation, WithWorkflow, WithMetadata): is_intermediate=self.is_intermediate, session_id=context.graph_execution_state_id, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -984,9 +1080,9 @@ class ImageChannelOffsetInvocation(BaseInvocation, WithWorkflow, WithMetadata): "value", ], category="image", - version="1.1.0", + version="1.2.0", ) -class ImageChannelMultiplyInvocation(BaseInvocation, WithWorkflow, WithMetadata): +class ImageChannelMultiplyInvocation(BaseInvocation, WithMetadata): """Scale a specific color channel of an image.""" image: ImageField = InputField(description="The image to adjust") @@ -1025,7 +1121,7 @@ class ImageChannelMultiplyInvocation(BaseInvocation, WithWorkflow, WithMetadata) node_id=self.id, is_intermediate=self.is_intermediate, session_id=context.graph_execution_state_id, - workflow=self.workflow, + workflow=context.workflow, metadata=self.metadata, ) @@ -1043,10 +1139,10 @@ class ImageChannelMultiplyInvocation(BaseInvocation, WithWorkflow, WithMetadata) title="Save Image", tags=["primitives", "image"], category="primitives", - version="1.1.0", + version="1.2.0", use_cache=False, ) -class SaveImageInvocation(BaseInvocation, WithWorkflow, WithMetadata): +class SaveImageInvocation(BaseInvocation, WithMetadata): """Saves an image. Unlike an image primitive, this invocation stores a copy of the image.""" image: ImageField = InputField(description=FieldDescriptions.image) @@ -1064,7 +1160,7 @@ class SaveImageInvocation(BaseInvocation, WithWorkflow, WithMetadata): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -1082,7 +1178,7 @@ class SaveImageInvocation(BaseInvocation, WithWorkflow, WithMetadata): version="1.0.1", use_cache=False, ) -class LinearUIOutputInvocation(BaseInvocation, WithWorkflow, WithMetadata): +class LinearUIOutputInvocation(BaseInvocation, WithMetadata): """Handles Linear UI Image Outputting tasks.""" image: ImageField = InputField(description=FieldDescriptions.image) diff --git a/invokeai/app/invocations/infill.py b/invokeai/app/invocations/infill.py index 0822a4ce2d..c3d00bb133 100644 --- a/invokeai/app/invocations/infill.py +++ b/invokeai/app/invocations/infill.py @@ -13,7 +13,7 @@ from invokeai.backend.image_util.cv2_inpaint import cv2_inpaint from invokeai.backend.image_util.lama import LaMA from invokeai.backend.image_util.patchmatch import PatchMatch -from .baseinvocation import BaseInvocation, InputField, InvocationContext, WithMetadata, WithWorkflow, invocation +from .baseinvocation import BaseInvocation, InputField, InvocationContext, WithMetadata, invocation from .image import PIL_RESAMPLING_MAP, PIL_RESAMPLING_MODES @@ -118,8 +118,8 @@ def tile_fill_missing(im: Image.Image, tile_size: int = 16, seed: Optional[int] return si -@invocation("infill_rgba", title="Solid Color Infill", tags=["image", "inpaint"], category="inpaint", version="1.1.0") -class InfillColorInvocation(BaseInvocation, WithWorkflow, WithMetadata): +@invocation("infill_rgba", title="Solid Color Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.0") +class InfillColorInvocation(BaseInvocation, WithMetadata): """Infills transparent areas of an image with a solid color""" image: ImageField = InputField(description="The image to infill") @@ -144,7 +144,7 @@ class InfillColorInvocation(BaseInvocation, WithWorkflow, WithMetadata): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -154,8 +154,8 @@ class InfillColorInvocation(BaseInvocation, WithWorkflow, WithMetadata): ) -@invocation("infill_tile", title="Tile Infill", tags=["image", "inpaint"], category="inpaint", version="1.1.1") -class InfillTileInvocation(BaseInvocation, WithWorkflow, WithMetadata): +@invocation("infill_tile", title="Tile Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.1") +class InfillTileInvocation(BaseInvocation, WithMetadata): """Infills transparent areas of an image with tiles of the image""" image: ImageField = InputField(description="The image to infill") @@ -181,7 +181,7 @@ class InfillTileInvocation(BaseInvocation, WithWorkflow, WithMetadata): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -192,9 +192,9 @@ class InfillTileInvocation(BaseInvocation, WithWorkflow, WithMetadata): @invocation( - "infill_patchmatch", title="PatchMatch Infill", tags=["image", "inpaint"], category="inpaint", version="1.1.0" + "infill_patchmatch", title="PatchMatch Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.0" ) -class InfillPatchMatchInvocation(BaseInvocation, WithWorkflow, WithMetadata): +class InfillPatchMatchInvocation(BaseInvocation, WithMetadata): """Infills transparent areas of an image using the PatchMatch algorithm""" image: ImageField = InputField(description="The image to infill") @@ -235,7 +235,7 @@ class InfillPatchMatchInvocation(BaseInvocation, WithWorkflow, WithMetadata): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -245,8 +245,8 @@ class InfillPatchMatchInvocation(BaseInvocation, WithWorkflow, WithMetadata): ) -@invocation("infill_lama", title="LaMa Infill", tags=["image", "inpaint"], category="inpaint", version="1.1.0") -class LaMaInfillInvocation(BaseInvocation, WithWorkflow, WithMetadata): +@invocation("infill_lama", title="LaMa Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.0") +class LaMaInfillInvocation(BaseInvocation, WithMetadata): """Infills transparent areas of an image using the LaMa model""" image: ImageField = InputField(description="The image to infill") @@ -264,7 +264,7 @@ class LaMaInfillInvocation(BaseInvocation, WithWorkflow, WithMetadata): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( @@ -274,8 +274,8 @@ class LaMaInfillInvocation(BaseInvocation, WithWorkflow, WithMetadata): ) -@invocation("infill_cv2", title="CV2 Infill", tags=["image", "inpaint"], category="inpaint", version="1.1.0") -class CV2InfillInvocation(BaseInvocation, WithWorkflow, WithMetadata): +@invocation("infill_cv2", title="CV2 Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.0") +class CV2InfillInvocation(BaseInvocation, WithMetadata): """Infills transparent areas of an image using OpenCV Inpainting""" image: ImageField = InputField(description="The image to infill") @@ -293,7 +293,7 @@ class CV2InfillInvocation(BaseInvocation, WithWorkflow, WithMetadata): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 218e05a986..796ef82dcd 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -64,7 +64,6 @@ from .baseinvocation import ( OutputField, UIType, WithMetadata, - WithWorkflow, invocation, invocation_output, ) @@ -802,9 +801,9 @@ class DenoiseLatentsInvocation(BaseInvocation): title="Latents to Image", tags=["latents", "image", "vae", "l2i"], category="latents", - version="1.1.0", + version="1.2.0", ) -class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow): +class LatentsToImageInvocation(BaseInvocation, WithMetadata): """Generates an image from latents.""" latents: LatentsField = InputField( @@ -886,7 +885,7 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( diff --git a/invokeai/app/invocations/onnx.py b/invokeai/app/invocations/onnx.py index 37b63fe692..9eca5e083e 100644 --- a/invokeai/app/invocations/onnx.py +++ b/invokeai/app/invocations/onnx.py @@ -31,7 +31,6 @@ from .baseinvocation import ( UIComponent, UIType, WithMetadata, - WithWorkflow, invocation, invocation_output, ) @@ -326,9 +325,9 @@ class ONNXTextToLatentsInvocation(BaseInvocation): title="ONNX Latents to Image", tags=["latents", "image", "vae", "onnx"], category="image", - version="1.1.0", + version="1.2.0", ) -class ONNXLatentsToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow): +class ONNXLatentsToImageInvocation(BaseInvocation, WithMetadata): """Generates an image from latents.""" latents: LatentsField = InputField( @@ -378,7 +377,7 @@ class ONNXLatentsToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( diff --git a/invokeai/app/invocations/tiles.py b/invokeai/app/invocations/tiles.py index 3055c1baae..e59a0530ee 100644 --- a/invokeai/app/invocations/tiles.py +++ b/invokeai/app/invocations/tiles.py @@ -9,7 +9,6 @@ from invokeai.app.invocations.baseinvocation import ( InvocationContext, OutputField, WithMetadata, - WithWorkflow, invocation, invocation_output, ) @@ -122,8 +121,8 @@ class PairTileImageInvocation(BaseInvocation): ) -@invocation("merge_tiles_to_image", title="Merge Tiles to Image", tags=["tiles"], category="tiles", version="1.0.0") -class MergeTilesToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow): +@invocation("merge_tiles_to_image", title="Merge Tiles to Image", tags=["tiles"], category="tiles", version="1.1.0") +class MergeTilesToImageInvocation(BaseInvocation, WithMetadata): """Merge multiple tile images into a single image.""" # Inputs @@ -172,7 +171,7 @@ class MergeTilesToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( image=ImageField(image_name=image_dto.image_name), diff --git a/invokeai/app/invocations/upscale.py b/invokeai/app/invocations/upscale.py index 0f699b2d15..fa86dead55 100644 --- a/invokeai/app/invocations/upscale.py +++ b/invokeai/app/invocations/upscale.py @@ -14,7 +14,7 @@ from invokeai.app.services.image_records.image_records_common import ImageCatego from invokeai.backend.image_util.realesrgan.realesrgan import RealESRGAN from invokeai.backend.util.devices import choose_torch_device -from .baseinvocation import BaseInvocation, InputField, InvocationContext, WithMetadata, WithWorkflow, invocation +from .baseinvocation import BaseInvocation, InputField, InvocationContext, WithMetadata, invocation # TODO: Populate this from disk? # TODO: Use model manager to load? @@ -29,8 +29,8 @@ if choose_torch_device() == torch.device("mps"): from torch import mps -@invocation("esrgan", title="Upscale (RealESRGAN)", tags=["esrgan", "upscale"], category="esrgan", version="1.2.0") -class ESRGANInvocation(BaseInvocation, WithWorkflow, WithMetadata): +@invocation("esrgan", title="Upscale (RealESRGAN)", tags=["esrgan", "upscale"], category="esrgan", version="1.3.0") +class ESRGANInvocation(BaseInvocation, WithMetadata): """Upscales an image using RealESRGAN.""" image: ImageField = InputField(description="The input image") @@ -118,7 +118,7 @@ class ESRGANInvocation(BaseInvocation, WithWorkflow, WithMetadata): session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, metadata=self.metadata, - workflow=self.workflow, + workflow=context.workflow, ) return ImageOutput( diff --git a/invokeai/app/services/board_image_records/board_image_records_sqlite.py b/invokeai/app/services/board_image_records/board_image_records_sqlite.py index 02bafd00ec..54d9a0af04 100644 --- a/invokeai/app/services/board_image_records/board_image_records_sqlite.py +++ b/invokeai/app/services/board_image_records/board_image_records_sqlite.py @@ -4,7 +4,7 @@ from typing import Optional, cast from invokeai.app.services.image_records.image_records_common import ImageRecord, deserialize_image_record from invokeai.app.services.shared.pagination import OffsetPaginatedResults -from invokeai.app.services.shared.sqlite import SqliteDatabase +from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase from .board_image_records_base import BoardImageRecordStorageBase diff --git a/invokeai/app/services/board_records/board_records_sqlite.py b/invokeai/app/services/board_records/board_records_sqlite.py index ef507def2a..165ce8df0c 100644 --- a/invokeai/app/services/board_records/board_records_sqlite.py +++ b/invokeai/app/services/board_records/board_records_sqlite.py @@ -3,7 +3,7 @@ import threading from typing import Union, cast from invokeai.app.services.shared.pagination import OffsetPaginatedResults -from invokeai.app.services.shared.sqlite import SqliteDatabase +from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase from invokeai.app.util.misc import uuid_string from .board_records_base import BoardRecordStorageBase diff --git a/invokeai/app/services/image_files/image_files_base.py b/invokeai/app/services/image_files/image_files_base.py index 91e18f30fc..27dd67531f 100644 --- a/invokeai/app/services/image_files/image_files_base.py +++ b/invokeai/app/services/image_files/image_files_base.py @@ -4,7 +4,8 @@ from typing import Optional from PIL.Image import Image as PILImageType -from invokeai.app.invocations.baseinvocation import MetadataField, WorkflowField +from invokeai.app.invocations.baseinvocation import MetadataField +from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID class ImageFileStorageBase(ABC): @@ -33,7 +34,7 @@ class ImageFileStorageBase(ABC): image: PILImageType, image_name: str, metadata: Optional[MetadataField] = None, - workflow: Optional[WorkflowField] = None, + workflow: Optional[WorkflowWithoutID] = None, thumbnail_size: int = 256, ) -> None: """Saves an image and a 256x256 WEBP thumbnail. Returns a tuple of the image name, thumbnail name, and created timestamp.""" @@ -43,3 +44,8 @@ class ImageFileStorageBase(ABC): def delete(self, image_name: str) -> None: """Deletes an image and its thumbnail (if one exists).""" pass + + @abstractmethod + def get_workflow(self, image_name: str) -> Optional[WorkflowWithoutID]: + """Gets the workflow of an image.""" + pass diff --git a/invokeai/app/services/image_files/image_files_disk.py b/invokeai/app/services/image_files/image_files_disk.py index cffcb702c9..0844821672 100644 --- a/invokeai/app/services/image_files/image_files_disk.py +++ b/invokeai/app/services/image_files/image_files_disk.py @@ -7,8 +7,9 @@ from PIL import Image, PngImagePlugin from PIL.Image import Image as PILImageType from send2trash import send2trash -from invokeai.app.invocations.baseinvocation import MetadataField, WorkflowField +from invokeai.app.invocations.baseinvocation import MetadataField from invokeai.app.services.invoker import Invoker +from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail from .image_files_base import ImageFileStorageBase @@ -56,7 +57,7 @@ class DiskImageFileStorage(ImageFileStorageBase): image: PILImageType, image_name: str, metadata: Optional[MetadataField] = None, - workflow: Optional[WorkflowField] = None, + workflow: Optional[WorkflowWithoutID] = None, thumbnail_size: int = 256, ) -> None: try: @@ -64,12 +65,19 @@ class DiskImageFileStorage(ImageFileStorageBase): image_path = self.get_path(image_name) pnginfo = PngImagePlugin.PngInfo() + info_dict = {} if metadata is not None: - pnginfo.add_text("invokeai_metadata", metadata.model_dump_json()) + metadata_json = metadata.model_dump_json() + info_dict["invokeai_metadata"] = metadata_json + pnginfo.add_text("invokeai_metadata", metadata_json) if workflow is not None: - pnginfo.add_text("invokeai_workflow", workflow.model_dump_json()) + workflow_json = workflow.model_dump_json() + info_dict["invokeai_workflow"] = workflow_json + pnginfo.add_text("invokeai_workflow", workflow_json) + # When saving the image, the image object's info field is not populated. We need to set it + image.info = info_dict image.save( image_path, "PNG", @@ -121,6 +129,13 @@ class DiskImageFileStorage(ImageFileStorageBase): path = path if isinstance(path, Path) else Path(path) return path.exists() + def get_workflow(self, image_name: str) -> WorkflowWithoutID | None: + image = self.get(image_name) + workflow = image.info.get("invokeai_workflow", None) + if workflow is not None: + return WorkflowWithoutID.model_validate_json(workflow) + return None + def __validate_storage_folders(self) -> None: """Checks if the required output folders exist and create them if they don't""" folders: list[Path] = [self.__output_folder, self.__thumbnails_folder] diff --git a/invokeai/app/services/image_records/image_records_base.py b/invokeai/app/services/image_records/image_records_base.py index 655e4b4fb8..727f4977fb 100644 --- a/invokeai/app/services/image_records/image_records_base.py +++ b/invokeai/app/services/image_records/image_records_base.py @@ -75,6 +75,7 @@ class ImageRecordStorageBase(ABC): image_category: ImageCategory, width: int, height: int, + has_workflow: bool, is_intermediate: Optional[bool] = False, starred: Optional[bool] = False, session_id: Optional[str] = None, diff --git a/invokeai/app/services/image_records/image_records_common.py b/invokeai/app/services/image_records/image_records_common.py index 61b97c6032..af681e90e1 100644 --- a/invokeai/app/services/image_records/image_records_common.py +++ b/invokeai/app/services/image_records/image_records_common.py @@ -100,6 +100,7 @@ IMAGE_DTO_COLS = ", ".join( "height", "session_id", "node_id", + "has_workflow", "is_intermediate", "created_at", "updated_at", @@ -145,6 +146,7 @@ class ImageRecord(BaseModelExcludeNull): """The node ID that generated this image, if it is a generated image.""" starred: bool = Field(description="Whether this image is starred.") """Whether this image is starred.""" + has_workflow: bool = Field(description="Whether this image has a workflow.") class ImageRecordChanges(BaseModelExcludeNull, extra="allow"): @@ -188,6 +190,7 @@ def deserialize_image_record(image_dict: dict) -> ImageRecord: deleted_at = image_dict.get("deleted_at", get_iso_timestamp()) is_intermediate = image_dict.get("is_intermediate", False) starred = image_dict.get("starred", False) + has_workflow = image_dict.get("has_workflow", False) return ImageRecord( image_name=image_name, @@ -202,4 +205,5 @@ def deserialize_image_record(image_dict: dict) -> ImageRecord: deleted_at=deleted_at, is_intermediate=is_intermediate, starred=starred, + has_workflow=has_workflow, ) diff --git a/invokeai/app/services/image_records/image_records_sqlite.py b/invokeai/app/services/image_records/image_records_sqlite.py index e0dabf1657..b14c322f50 100644 --- a/invokeai/app/services/image_records/image_records_sqlite.py +++ b/invokeai/app/services/image_records/image_records_sqlite.py @@ -5,7 +5,7 @@ from typing import Optional, Union, cast from invokeai.app.invocations.baseinvocation import MetadataField, MetadataFieldValidator from invokeai.app.services.shared.pagination import OffsetPaginatedResults -from invokeai.app.services.shared.sqlite import SqliteDatabase +from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase from .image_records_base import ImageRecordStorageBase from .image_records_common import ( @@ -117,6 +117,16 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): """ ) + self._cursor.execute("PRAGMA table_info(images)") + columns = [column[1] for column in self._cursor.fetchall()] + if "has_workflow" not in columns: + self._cursor.execute( + """--sql + ALTER TABLE images + ADD COLUMN has_workflow BOOLEAN DEFAULT FALSE; + """ + ) + def get(self, image_name: str) -> ImageRecord: try: self._lock.acquire() @@ -408,6 +418,7 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): image_category: ImageCategory, width: int, height: int, + has_workflow: bool, is_intermediate: Optional[bool] = False, starred: Optional[bool] = False, session_id: Optional[str] = None, @@ -429,9 +440,10 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): session_id, metadata, is_intermediate, - starred + starred, + has_workflow ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); """, ( image_name, @@ -444,6 +456,7 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): metadata_json, is_intermediate, starred, + has_workflow, ), ) self._conn.commit() diff --git a/invokeai/app/services/images/images_base.py b/invokeai/app/services/images/images_base.py index b3990d08f5..df71dadb5b 100644 --- a/invokeai/app/services/images/images_base.py +++ b/invokeai/app/services/images/images_base.py @@ -3,7 +3,7 @@ from typing import Callable, Optional from PIL.Image import Image as PILImageType -from invokeai.app.invocations.baseinvocation import MetadataField, WorkflowField +from invokeai.app.invocations.baseinvocation import MetadataField from invokeai.app.services.image_records.image_records_common import ( ImageCategory, ImageRecord, @@ -12,6 +12,7 @@ from invokeai.app.services.image_records.image_records_common import ( ) from invokeai.app.services.images.images_common import ImageDTO from invokeai.app.services.shared.pagination import OffsetPaginatedResults +from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID class ImageServiceABC(ABC): @@ -51,7 +52,7 @@ class ImageServiceABC(ABC): board_id: Optional[str] = None, is_intermediate: Optional[bool] = False, metadata: Optional[MetadataField] = None, - workflow: Optional[WorkflowField] = None, + workflow: Optional[WorkflowWithoutID] = None, ) -> ImageDTO: """Creates an image, storing the file and its metadata.""" pass @@ -85,6 +86,11 @@ class ImageServiceABC(ABC): """Gets an image's metadata.""" pass + @abstractmethod + def get_workflow(self, image_name: str) -> Optional[WorkflowWithoutID]: + """Gets an image's workflow.""" + pass + @abstractmethod def get_path(self, image_name: str, thumbnail: bool = False) -> str: """Gets an image's path.""" diff --git a/invokeai/app/services/images/images_common.py b/invokeai/app/services/images/images_common.py index 198c26c3a2..0464244b94 100644 --- a/invokeai/app/services/images/images_common.py +++ b/invokeai/app/services/images/images_common.py @@ -24,11 +24,6 @@ class ImageDTO(ImageRecord, ImageUrlsDTO): default=None, description="The id of the board the image belongs to, if one exists." ) """The id of the board the image belongs to, if one exists.""" - workflow_id: Optional[str] = Field( - default=None, - description="The workflow that generated this image.", - ) - """The workflow that generated this image.""" def image_record_to_dto( @@ -36,7 +31,6 @@ def image_record_to_dto( image_url: str, thumbnail_url: str, board_id: Optional[str], - workflow_id: Optional[str], ) -> ImageDTO: """Converts an image record to an image DTO.""" return ImageDTO( @@ -44,5 +38,4 @@ def image_record_to_dto( image_url=image_url, thumbnail_url=thumbnail_url, board_id=board_id, - workflow_id=workflow_id, ) diff --git a/invokeai/app/services/images/images_default.py b/invokeai/app/services/images/images_default.py index 63fa78d6c8..74aeeccca5 100644 --- a/invokeai/app/services/images/images_default.py +++ b/invokeai/app/services/images/images_default.py @@ -2,9 +2,10 @@ from typing import Optional from PIL.Image import Image as PILImageType -from invokeai.app.invocations.baseinvocation import MetadataField, WorkflowField +from invokeai.app.invocations.baseinvocation import MetadataField from invokeai.app.services.invoker import Invoker from invokeai.app.services.shared.pagination import OffsetPaginatedResults +from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID from ..image_files.image_files_common import ( ImageFileDeleteException, @@ -42,7 +43,7 @@ class ImageService(ImageServiceABC): board_id: Optional[str] = None, is_intermediate: Optional[bool] = False, metadata: Optional[MetadataField] = None, - workflow: Optional[WorkflowField] = None, + workflow: Optional[WorkflowWithoutID] = None, ) -> ImageDTO: if image_origin not in ResourceOrigin: raise InvalidOriginException @@ -55,12 +56,6 @@ class ImageService(ImageServiceABC): (width, height) = image.size try: - if workflow is not None: - created_workflow = self.__invoker.services.workflow_records.create(workflow) - workflow_id = created_workflow.model_dump()["id"] - else: - workflow_id = None - # TODO: Consider using a transaction here to ensure consistency between storage and database self.__invoker.services.image_records.save( # Non-nullable fields @@ -69,6 +64,7 @@ class ImageService(ImageServiceABC): image_category=image_category, width=width, height=height, + has_workflow=workflow is not None, # Meta fields is_intermediate=is_intermediate, # Nullable fields @@ -78,8 +74,6 @@ class ImageService(ImageServiceABC): ) if board_id is not None: self.__invoker.services.board_image_records.add_image_to_board(board_id=board_id, image_name=image_name) - if workflow_id is not None: - self.__invoker.services.workflow_image_records.create(workflow_id=workflow_id, image_name=image_name) self.__invoker.services.image_files.save( image_name=image_name, image=image, metadata=metadata, workflow=workflow ) @@ -143,7 +137,6 @@ class ImageService(ImageServiceABC): image_url=self.__invoker.services.urls.get_image_url(image_name), thumbnail_url=self.__invoker.services.urls.get_image_url(image_name, True), board_id=self.__invoker.services.board_image_records.get_board_for_image(image_name), - workflow_id=self.__invoker.services.workflow_image_records.get_workflow_for_image(image_name), ) return image_dto @@ -164,18 +157,15 @@ class ImageService(ImageServiceABC): self.__invoker.services.logger.error("Problem getting image DTO") raise e - def get_workflow(self, image_name: str) -> Optional[WorkflowField]: + def get_workflow(self, image_name: str) -> Optional[WorkflowWithoutID]: try: - workflow_id = self.__invoker.services.workflow_image_records.get_workflow_for_image(image_name) - if workflow_id is None: - return None - return self.__invoker.services.workflow_records.get(workflow_id) - except ImageRecordNotFoundException: - self.__invoker.services.logger.error("Image record not found") + return self.__invoker.services.image_files.get_workflow(image_name) + except ImageFileNotFoundException: + self.__invoker.services.logger.error("Image file not found") + raise + except Exception: + self.__invoker.services.logger.error("Problem getting image workflow") raise - except Exception as e: - self.__invoker.services.logger.error("Problem getting image DTO") - raise e def get_path(self, image_name: str, thumbnail: bool = False) -> str: try: @@ -223,7 +213,6 @@ class ImageService(ImageServiceABC): image_url=self.__invoker.services.urls.get_image_url(r.image_name), thumbnail_url=self.__invoker.services.urls.get_image_url(r.image_name, True), board_id=self.__invoker.services.board_image_records.get_board_for_image(r.image_name), - workflow_id=self.__invoker.services.workflow_image_records.get_workflow_for_image(r.image_name), ) for r in results.items ] diff --git a/invokeai/app/services/invocation_processor/invocation_processor_default.py b/invokeai/app/services/invocation_processor/invocation_processor_default.py index 6e0d3075ea..50657b0984 100644 --- a/invokeai/app/services/invocation_processor/invocation_processor_default.py +++ b/invokeai/app/services/invocation_processor/invocation_processor_default.py @@ -108,6 +108,7 @@ class DefaultInvocationProcessor(InvocationProcessorABC): queue_item_id=queue_item.session_queue_item_id, queue_id=queue_item.session_queue_id, queue_batch_id=queue_item.session_queue_batch_id, + workflow=queue_item.workflow, ) ) @@ -178,6 +179,7 @@ class DefaultInvocationProcessor(InvocationProcessorABC): session_queue_item_id=queue_item.session_queue_item_id, session_queue_id=queue_item.session_queue_id, graph_execution_state=graph_execution_state, + workflow=queue_item.workflow, invoke_all=True, ) except Exception as e: diff --git a/invokeai/app/services/invocation_queue/invocation_queue_common.py b/invokeai/app/services/invocation_queue/invocation_queue_common.py index 88e72886f7..696f6a981d 100644 --- a/invokeai/app/services/invocation_queue/invocation_queue_common.py +++ b/invokeai/app/services/invocation_queue/invocation_queue_common.py @@ -1,9 +1,12 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) import time +from typing import Optional from pydantic import BaseModel, Field +from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID + class InvocationQueueItem(BaseModel): graph_execution_state_id: str = Field(description="The ID of the graph execution state") @@ -15,5 +18,6 @@ class InvocationQueueItem(BaseModel): session_queue_batch_id: str = Field( description="The ID of the session batch from which this invocation queue item came" ) + workflow: Optional[WorkflowWithoutID] = Field(description="The workflow associated with this queue item") invoke_all: bool = Field(default=False) timestamp: float = Field(default_factory=time.time) diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index 366e2d0f2b..86299c5a6b 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -28,7 +28,6 @@ if TYPE_CHECKING: from .session_queue.session_queue_base import SessionQueueBase from .shared.graph import GraphExecutionState, LibraryGraph from .urls.urls_base import UrlServiceBase - from .workflow_image_records.workflow_image_records_base import WorkflowImageRecordsStorageBase from .workflow_records.workflow_records_base import WorkflowRecordsStorageBase @@ -59,7 +58,6 @@ class InvocationServices: invocation_cache: "InvocationCacheBase" names: "NameServiceBase" urls: "UrlServiceBase" - workflow_image_records: "WorkflowImageRecordsStorageBase" workflow_records: "WorkflowRecordsStorageBase" def __init__( @@ -87,7 +85,6 @@ class InvocationServices: invocation_cache: "InvocationCacheBase", names: "NameServiceBase", urls: "UrlServiceBase", - workflow_image_records: "WorkflowImageRecordsStorageBase", workflow_records: "WorkflowRecordsStorageBase", ): self.board_images = board_images @@ -113,5 +110,4 @@ class InvocationServices: self.invocation_cache = invocation_cache self.names = names self.urls = urls - self.workflow_image_records = workflow_image_records self.workflow_records = workflow_records diff --git a/invokeai/app/services/invoker.py b/invokeai/app/services/invoker.py index 134bec2693..a04c6f2059 100644 --- a/invokeai/app/services/invoker.py +++ b/invokeai/app/services/invoker.py @@ -2,6 +2,8 @@ from typing import Optional +from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID + from .invocation_queue.invocation_queue_common import InvocationQueueItem from .invocation_services import InvocationServices from .shared.graph import Graph, GraphExecutionState @@ -22,6 +24,7 @@ class Invoker: session_queue_item_id: int, session_queue_batch_id: str, graph_execution_state: GraphExecutionState, + workflow: Optional[WorkflowWithoutID] = None, invoke_all: bool = False, ) -> Optional[str]: """Determines the next node to invoke and enqueues it, preparing if needed. @@ -43,6 +46,7 @@ class Invoker: session_queue_batch_id=session_queue_batch_id, graph_execution_state_id=graph_execution_state.id, invocation_id=invocation.id, + workflow=workflow, invoke_all=invoke_all, ) ) diff --git a/invokeai/app/services/item_storage/item_storage_sqlite.py b/invokeai/app/services/item_storage/item_storage_sqlite.py index d5a1b7f730..e02d3bdbb2 100644 --- a/invokeai/app/services/item_storage/item_storage_sqlite.py +++ b/invokeai/app/services/item_storage/item_storage_sqlite.py @@ -5,7 +5,7 @@ from typing import Generic, Optional, TypeVar, get_args from pydantic import BaseModel, TypeAdapter from invokeai.app.services.shared.pagination import PaginatedResults -from invokeai.app.services.shared.sqlite import SqliteDatabase +from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase from .item_storage_base import ItemStorageABC diff --git a/invokeai/app/services/model_records/model_records_sql.py b/invokeai/app/services/model_records/model_records_sql.py index d05e1418d8..69ac0e158f 100644 --- a/invokeai/app/services/model_records/model_records_sql.py +++ b/invokeai/app/services/model_records/model_records_sql.py @@ -52,7 +52,7 @@ from invokeai.backend.model_manager.config import ( ModelType, ) -from ..shared.sqlite import SqliteDatabase +from ..shared.sqlite.sqlite_database import SqliteDatabase from .model_records_base import ( CONFIG_FILE_VERSION, DuplicateModelException, diff --git a/invokeai/app/services/session_processor/session_processor_default.py b/invokeai/app/services/session_processor/session_processor_default.py index 028f91fec3..32e94a305d 100644 --- a/invokeai/app/services/session_processor/session_processor_default.py +++ b/invokeai/app/services/session_processor/session_processor_default.py @@ -114,6 +114,7 @@ class DefaultSessionProcessor(SessionProcessorBase): session_queue_id=queue_item.queue_id, session_queue_item_id=queue_item.item_id, graph_execution_state=queue_item.session, + workflow=queue_item.workflow, invoke_all=True, ) queue_item = None diff --git a/invokeai/app/services/session_queue/session_queue_common.py b/invokeai/app/services/session_queue/session_queue_common.py index e7d7cdda46..94db6999c2 100644 --- a/invokeai/app/services/session_queue/session_queue_common.py +++ b/invokeai/app/services/session_queue/session_queue_common.py @@ -8,6 +8,10 @@ from pydantic_core import to_jsonable_python from invokeai.app.invocations.baseinvocation import BaseInvocation from invokeai.app.services.shared.graph import Graph, GraphExecutionState, NodeNotFoundError +from invokeai.app.services.workflow_records.workflow_records_common import ( + WorkflowWithoutID, + WorkflowWithoutIDValidator, +) from invokeai.app.util.misc import uuid_string # region Errors @@ -66,6 +70,9 @@ class Batch(BaseModel): batch_id: str = Field(default_factory=uuid_string, description="The ID of the batch") data: Optional[BatchDataCollection] = Field(default=None, description="The batch data collection.") graph: Graph = Field(description="The graph to initialize the session with") + workflow: Optional[WorkflowWithoutID] = Field( + default=None, description="The workflow to initialize the session with" + ) runs: int = Field( default=1, ge=1, description="Int stating how many times to iterate through all possible batch indices" ) @@ -164,6 +171,14 @@ def get_session(queue_item_dict: dict) -> GraphExecutionState: return session +def get_workflow(queue_item_dict: dict) -> Optional[WorkflowWithoutID]: + workflow_raw = queue_item_dict.get("workflow", None) + if workflow_raw is not None: + workflow = WorkflowWithoutIDValidator.validate_json(workflow_raw, strict=False) + return workflow + return None + + class SessionQueueItemWithoutGraph(BaseModel): """Session queue item without the full graph. Used for serialization.""" @@ -213,12 +228,16 @@ class SessionQueueItemDTO(SessionQueueItemWithoutGraph): class SessionQueueItem(SessionQueueItemWithoutGraph): session: GraphExecutionState = Field(description="The fully-populated session to be executed") + workflow: Optional[WorkflowWithoutID] = Field( + default=None, description="The workflow associated with this queue item" + ) @classmethod def queue_item_from_dict(cls, queue_item_dict: dict) -> "SessionQueueItem": # must parse these manually queue_item_dict["field_values"] = get_field_values(queue_item_dict) queue_item_dict["session"] = get_session(queue_item_dict) + queue_item_dict["workflow"] = get_workflow(queue_item_dict) return SessionQueueItem(**queue_item_dict) model_config = ConfigDict( @@ -334,7 +353,7 @@ def populate_graph(graph: Graph, node_field_values: Iterable[NodeFieldValue]) -> def create_session_nfv_tuples( batch: Batch, maximum: int -) -> Generator[tuple[GraphExecutionState, list[NodeFieldValue]], None, None]: +) -> Generator[tuple[GraphExecutionState, list[NodeFieldValue], Optional[WorkflowWithoutID]], None, None]: """ Create all graph permutations from the given batch data and graph. Yields tuples of the form (graph, batch_data_items) where batch_data_items is the list of BatchDataItems @@ -365,7 +384,7 @@ def create_session_nfv_tuples( return flat_node_field_values = list(chain.from_iterable(d)) graph = populate_graph(batch.graph, flat_node_field_values) - yield (GraphExecutionState(graph=graph), flat_node_field_values) + yield (GraphExecutionState(graph=graph), flat_node_field_values, batch.workflow) count += 1 @@ -391,12 +410,14 @@ def calc_session_count(batch: Batch) -> int: class SessionQueueValueToInsert(NamedTuple): """A tuple of values to insert into the session_queue table""" + # Careful with the ordering of this - it must match the insert statement queue_id: str # queue_id session: str # session json session_id: str # session_id batch_id: str # batch_id field_values: Optional[str] # field_values json priority: int # priority + workflow: Optional[str] # workflow json ValuesToInsert: TypeAlias = list[SessionQueueValueToInsert] @@ -404,7 +425,7 @@ ValuesToInsert: TypeAlias = list[SessionQueueValueToInsert] def prepare_values_to_insert(queue_id: str, batch: Batch, priority: int, max_new_queue_items: int) -> ValuesToInsert: values_to_insert: ValuesToInsert = [] - for session, field_values in create_session_nfv_tuples(batch, max_new_queue_items): + for session, field_values, workflow in create_session_nfv_tuples(batch, max_new_queue_items): # sessions must have unique id session.id = uuid_string() values_to_insert.append( @@ -416,6 +437,7 @@ def prepare_values_to_insert(queue_id: str, batch: Batch, priority: int, max_new # must use pydantic_encoder bc field_values is a list of models json.dumps(field_values, default=to_jsonable_python) if field_values else None, # field_values (json) priority, # priority + json.dumps(workflow, default=to_jsonable_python) if workflow else None, # workflow (json) ) ) return values_to_insert diff --git a/invokeai/app/services/session_queue/session_queue_sqlite.py b/invokeai/app/services/session_queue/session_queue_sqlite.py index 58d9d461ec..71f28c102b 100644 --- a/invokeai/app/services/session_queue/session_queue_sqlite.py +++ b/invokeai/app/services/session_queue/session_queue_sqlite.py @@ -28,7 +28,7 @@ from invokeai.app.services.session_queue.session_queue_common import ( prepare_values_to_insert, ) from invokeai.app.services.shared.pagination import CursorPaginatedResults -from invokeai.app.services.shared.sqlite import SqliteDatabase +from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase class SqliteSessionQueue(SessionQueueBase): @@ -199,6 +199,15 @@ class SqliteSessionQueue(SessionQueueBase): """ ) + self.__cursor.execute("PRAGMA table_info(session_queue)") + columns = [column[1] for column in self.__cursor.fetchall()] + if "workflow" not in columns: + self.__cursor.execute( + """--sql + ALTER TABLE session_queue ADD COLUMN workflow TEXT; + """ + ) + self.__conn.commit() except Exception: self.__conn.rollback() @@ -281,8 +290,8 @@ class SqliteSessionQueue(SessionQueueBase): self.__cursor.executemany( """--sql - INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority) - VALUES (?, ?, ?, ?, ?, ?) + INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow) + VALUES (?, ?, ?, ?, ?, ?, ?) """, values_to_insert, ) diff --git a/invokeai/app/services/workflow_image_records/__init__.py b/invokeai/app/services/shared/sqlite/__init__.py similarity index 100% rename from invokeai/app/services/workflow_image_records/__init__.py rename to invokeai/app/services/shared/sqlite/__init__.py diff --git a/invokeai/app/services/shared/sqlite/sqlite_common.py b/invokeai/app/services/shared/sqlite/sqlite_common.py new file mode 100644 index 0000000000..2520695201 --- /dev/null +++ b/invokeai/app/services/shared/sqlite/sqlite_common.py @@ -0,0 +1,10 @@ +from enum import Enum + +from invokeai.app.util.metaenum import MetaEnum + +sqlite_memory = ":memory:" + + +class SQLiteDirection(str, Enum, metaclass=MetaEnum): + Ascending = "ASC" + Descending = "DESC" diff --git a/invokeai/app/services/shared/sqlite.py b/invokeai/app/services/shared/sqlite/sqlite_database.py similarity index 56% rename from invokeai/app/services/shared/sqlite.py rename to invokeai/app/services/shared/sqlite/sqlite_database.py index 9cddb2b926..006eb61cbd 100644 --- a/invokeai/app/services/shared/sqlite.py +++ b/invokeai/app/services/shared/sqlite/sqlite_database.py @@ -4,8 +4,7 @@ from logging import Logger from pathlib import Path from invokeai.app.services.config import InvokeAIAppConfig - -sqlite_memory = ":memory:" +from invokeai.app.services.shared.sqlite.sqlite_common import sqlite_memory class SqliteDatabase: @@ -32,19 +31,17 @@ class SqliteDatabase: self.conn.execute("PRAGMA foreign_keys = ON;") def clean(self) -> None: - try: - if self.db_path == sqlite_memory: - return - initial_db_size = Path(self.db_path).stat().st_size - self.lock.acquire() - self.conn.execute("VACUUM;") - self.conn.commit() - final_db_size = Path(self.db_path).stat().st_size - freed_space_in_mb = round((initial_db_size - final_db_size) / 1024 / 1024, 2) - if freed_space_in_mb > 0: - self._logger.info(f"Cleaned database (freed {freed_space_in_mb}MB)") - except Exception as e: - self._logger.error(f"Error cleaning database: {e}") - raise e - finally: - self.lock.release() + with self.lock: + try: + if self.db_path == sqlite_memory: + return + initial_db_size = Path(self.db_path).stat().st_size + self.conn.execute("VACUUM;") + self.conn.commit() + final_db_size = Path(self.db_path).stat().st_size + freed_space_in_mb = round((initial_db_size - final_db_size) / 1024 / 1024, 2) + if freed_space_in_mb > 0: + self._logger.info(f"Cleaned database (freed {freed_space_in_mb}MB)") + except Exception as e: + self._logger.error(f"Error cleaning database: {e}") + raise diff --git a/invokeai/app/services/workflow_image_records/workflow_image_records_base.py b/invokeai/app/services/workflow_image_records/workflow_image_records_base.py deleted file mode 100644 index d99a2ba106..0000000000 --- a/invokeai/app/services/workflow_image_records/workflow_image_records_base.py +++ /dev/null @@ -1,23 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Optional - - -class WorkflowImageRecordsStorageBase(ABC): - """Abstract base class for the one-to-many workflow-image relationship record storage.""" - - @abstractmethod - def create( - self, - workflow_id: str, - image_name: str, - ) -> None: - """Creates a workflow-image record.""" - pass - - @abstractmethod - def get_workflow_for_image( - self, - image_name: str, - ) -> Optional[str]: - """Gets an image's workflow id, if it has one.""" - pass diff --git a/invokeai/app/services/workflow_image_records/workflow_image_records_sqlite.py b/invokeai/app/services/workflow_image_records/workflow_image_records_sqlite.py deleted file mode 100644 index ec7a73f1d5..0000000000 --- a/invokeai/app/services/workflow_image_records/workflow_image_records_sqlite.py +++ /dev/null @@ -1,122 +0,0 @@ -import sqlite3 -import threading -from typing import Optional, cast - -from invokeai.app.services.shared.sqlite import SqliteDatabase -from invokeai.app.services.workflow_image_records.workflow_image_records_base import WorkflowImageRecordsStorageBase - - -class SqliteWorkflowImageRecordsStorage(WorkflowImageRecordsStorageBase): - """SQLite implementation of WorkflowImageRecordsStorageBase.""" - - _conn: sqlite3.Connection - _cursor: sqlite3.Cursor - _lock: threading.RLock - - def __init__(self, db: SqliteDatabase) -> None: - super().__init__() - self._lock = db.lock - self._conn = db.conn - self._cursor = self._conn.cursor() - - try: - self._lock.acquire() - self._create_tables() - self._conn.commit() - finally: - self._lock.release() - - def _create_tables(self) -> None: - # Create the `workflow_images` junction table. - self._cursor.execute( - """--sql - CREATE TABLE IF NOT EXISTS workflow_images ( - workflow_id TEXT NOT NULL, - image_name TEXT NOT NULL, - created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), - -- updated via trigger - updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), - -- Soft delete, currently unused - deleted_at DATETIME, - -- enforce one-to-many relationship between workflows and images using PK - -- (we can extend this to many-to-many later) - PRIMARY KEY (image_name), - FOREIGN KEY (workflow_id) REFERENCES workflows (workflow_id) ON DELETE CASCADE, - FOREIGN KEY (image_name) REFERENCES images (image_name) ON DELETE CASCADE - ); - """ - ) - - # Add index for workflow id - self._cursor.execute( - """--sql - CREATE INDEX IF NOT EXISTS idx_workflow_images_workflow_id ON workflow_images (workflow_id); - """ - ) - - # Add index for workflow id, sorted by created_at - self._cursor.execute( - """--sql - CREATE INDEX IF NOT EXISTS idx_workflow_images_workflow_id_created_at ON workflow_images (workflow_id, created_at); - """ - ) - - # Add trigger for `updated_at`. - self._cursor.execute( - """--sql - CREATE TRIGGER IF NOT EXISTS tg_workflow_images_updated_at - AFTER UPDATE - ON workflow_images FOR EACH ROW - BEGIN - UPDATE workflow_images SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') - WHERE workflow_id = old.workflow_id AND image_name = old.image_name; - END; - """ - ) - - def create( - self, - workflow_id: str, - image_name: str, - ) -> None: - """Creates a workflow-image record.""" - try: - self._lock.acquire() - self._cursor.execute( - """--sql - INSERT INTO workflow_images (workflow_id, image_name) - VALUES (?, ?); - """, - (workflow_id, image_name), - ) - self._conn.commit() - except sqlite3.Error as e: - self._conn.rollback() - raise e - finally: - self._lock.release() - - def get_workflow_for_image( - self, - image_name: str, - ) -> Optional[str]: - """Gets an image's workflow id, if it has one.""" - try: - self._lock.acquire() - self._cursor.execute( - """--sql - SELECT workflow_id - FROM workflow_images - WHERE image_name = ?; - """, - (image_name,), - ) - result = self._cursor.fetchone() - if result is None: - return None - return cast(str, result[0]) - except sqlite3.Error as e: - self._conn.rollback() - raise e - finally: - self._lock.release() diff --git a/invokeai/app/services/workflow_records/default_workflows/README.md b/invokeai/app/services/workflow_records/default_workflows/README.md new file mode 100644 index 0000000000..3901ead1cd --- /dev/null +++ b/invokeai/app/services/workflow_records/default_workflows/README.md @@ -0,0 +1,17 @@ +# Default Workflows + +Workflows placed in this directory will be synced to the `workflow_library` as +_default workflows_ on app startup. + +- Default workflows are not editable by users. If they are loaded and saved, + they will save as a copy of the default workflow. +- Default workflows must have the `meta.category` property set to `"default"`. + An exception will be raised during sync if this is not set correctly. +- Default workflows appear on the "Default Workflows" tab of the Workflow + Library. + +After adding or updating default workflows, you **must** start the app up and +load them to ensure: + +- The workflow loads without warning or errors +- The workflow runs successfully diff --git a/invokeai/app/services/workflow_records/default_workflows/TextToImage_SD15.json b/invokeai/app/services/workflow_records/default_workflows/TextToImage_SD15.json new file mode 100644 index 0000000000..1e42df6e07 --- /dev/null +++ b/invokeai/app/services/workflow_records/default_workflows/TextToImage_SD15.json @@ -0,0 +1,798 @@ +{ + "name": "Text to Image - SD1.5", + "author": "InvokeAI", + "description": "Sample text to image workflow for Stable Diffusion 1.5/2", + "version": "1.1.0", + "contact": "invoke@invoke.ai", + "tags": "text2image, SD1.5, SD2, default", + "notes": "", + "exposedFields": [ + { + "nodeId": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8", + "fieldName": "model" + }, + { + "nodeId": "7d8bf987-284f-413a-b2fd-d825445a5d6c", + "fieldName": "prompt" + }, + { + "nodeId": "93dc02a4-d05b-48ed-b99c-c9b616af3402", + "fieldName": "prompt" + }, + { + "nodeId": "55705012-79b9-4aac-9f26-c0b10309785b", + "fieldName": "width" + }, + { + "nodeId": "55705012-79b9-4aac-9f26-c0b10309785b", + "fieldName": "height" + } + ], + "meta": { + "category": "default", + "version": "2.0.0" + }, + "nodes": [ + { + "id": "93dc02a4-d05b-48ed-b99c-c9b616af3402", + "type": "invocation", + "data": { + "id": "93dc02a4-d05b-48ed-b99c-c9b616af3402", + "type": "compel", + "label": "Negative Compel Prompt", + "isOpen": true, + "notes": "", + "isIntermediate": true, + "useCache": true, + "version": "1.0.0", + "nodePack": "invokeai", + "inputs": { + "prompt": { + "id": "7739aff6-26cb-4016-8897-5a1fb2305e4e", + "name": "prompt", + "fieldKind": "input", + "label": "Negative Prompt", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "StringField" + }, + "value": "" + }, + "clip": { + "id": "48d23dce-a6ae-472a-9f8c-22a714ea5ce0", + "name": "clip", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "ClipField" + } + } + }, + "outputs": { + "conditioning": { + "id": "37cf3a9d-f6b7-4b64-8ff6-2558c5ecc447", + "name": "conditioning", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "ConditioningField" + } + } + } + }, + "width": 320, + "height": 259, + "position": { + "x": 1000, + "y": 350 + } + }, + { + "id": "55705012-79b9-4aac-9f26-c0b10309785b", + "type": "invocation", + "data": { + "id": "55705012-79b9-4aac-9f26-c0b10309785b", + "type": "noise", + "label": "", + "isOpen": true, + "notes": "", + "isIntermediate": true, + "useCache": true, + "version": "1.0.1", + "nodePack": "invokeai", + "inputs": { + "seed": { + "id": "6431737c-918a-425d-a3b4-5d57e2f35d4d", + "name": "seed", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 0 + }, + "width": { + "id": "38fc5b66-fe6e-47c8-bba9-daf58e454ed7", + "name": "width", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 512 + }, + "height": { + "id": "16298330-e2bf-4872-a514-d6923df53cbb", + "name": "height", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 512 + }, + "use_cpu": { + "id": "c7c436d3-7a7a-4e76-91e4-c6deb271623c", + "name": "use_cpu", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "BooleanField" + }, + "value": true + } + }, + "outputs": { + "noise": { + "id": "50f650dc-0184-4e23-a927-0497a96fe954", + "name": "noise", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "LatentsField" + } + }, + "width": { + "id": "bb8a452b-133d-42d1-ae4a-3843d7e4109a", + "name": "width", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + } + }, + "height": { + "id": "35cfaa12-3b8b-4b7a-a884-327ff3abddd9", + "name": "height", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + } + } + } + }, + "width": 320, + "height": 388, + "position": { + "x": 600, + "y": 325 + } + }, + { + "id": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8", + "type": "invocation", + "data": { + "id": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8", + "type": "main_model_loader", + "label": "", + "isOpen": true, + "notes": "", + "isIntermediate": true, + "useCache": true, + "version": "1.0.0", + "nodePack": "invokeai", + "inputs": { + "model": { + "id": "993eabd2-40fd-44fe-bce7-5d0c7075ddab", + "name": "model", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "MainModelField" + }, + "value": { + "model_name": "stable-diffusion-v1-5", + "base_model": "sd-1", + "model_type": "main" + } + } + }, + "outputs": { + "unet": { + "id": "5c18c9db-328d-46d0-8cb9-143391c410be", + "name": "unet", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "UNetField" + } + }, + "clip": { + "id": "6effcac0-ec2f-4bf5-a49e-a2c29cf921f4", + "name": "clip", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "ClipField" + } + }, + "vae": { + "id": "57683ba3-f5f5-4f58-b9a2-4b83dacad4a1", + "name": "vae", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "VaeField" + } + } + } + }, + "width": 320, + "height": 226, + "position": { + "x": 600, + "y": 25 + } + }, + { + "id": "7d8bf987-284f-413a-b2fd-d825445a5d6c", + "type": "invocation", + "data": { + "id": "7d8bf987-284f-413a-b2fd-d825445a5d6c", + "type": "compel", + "label": "Positive Compel Prompt", + "isOpen": true, + "notes": "", + "isIntermediate": true, + "useCache": true, + "version": "1.0.0", + "nodePack": "invokeai", + "inputs": { + "prompt": { + "id": "7739aff6-26cb-4016-8897-5a1fb2305e4e", + "name": "prompt", + "fieldKind": "input", + "label": "Positive Prompt", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "StringField" + }, + "value": "Super cute tiger cub, national geographic award-winning photograph" + }, + "clip": { + "id": "48d23dce-a6ae-472a-9f8c-22a714ea5ce0", + "name": "clip", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "ClipField" + } + } + }, + "outputs": { + "conditioning": { + "id": "37cf3a9d-f6b7-4b64-8ff6-2558c5ecc447", + "name": "conditioning", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "ConditioningField" + } + } + } + }, + "width": 320, + "height": 259, + "position": { + "x": 1000, + "y": 25 + } + }, + { + "id": "ea94bc37-d995-4a83-aa99-4af42479f2f2", + "type": "invocation", + "data": { + "id": "ea94bc37-d995-4a83-aa99-4af42479f2f2", + "type": "rand_int", + "label": "Random Seed", + "isOpen": false, + "notes": "", + "isIntermediate": true, + "useCache": false, + "version": "1.0.0", + "nodePack": "invokeai", + "inputs": { + "low": { + "id": "3ec65a37-60ba-4b6c-a0b2-553dd7a84b84", + "name": "low", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 0 + }, + "high": { + "id": "085f853a-1a5f-494d-8bec-e4ba29a3f2d1", + "name": "high", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 2147483647 + } + }, + "outputs": { + "value": { + "id": "812ade4d-7699-4261-b9fc-a6c9d2ab55ee", + "name": "value", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + } + } + } + }, + "width": 320, + "height": 32, + "position": { + "x": 600, + "y": 275 + } + }, + { + "id": "eea2702a-19fb-45b5-9d75-56b4211ec03c", + "type": "invocation", + "data": { + "id": "eea2702a-19fb-45b5-9d75-56b4211ec03c", + "type": "denoise_latents", + "label": "", + "isOpen": true, + "notes": "", + "isIntermediate": true, + "useCache": true, + "version": "1.5.0", + "nodePack": "invokeai", + "inputs": { + "positive_conditioning": { + "id": "90b7f4f8-ada7-4028-8100-d2e54f192052", + "name": "positive_conditioning", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "ConditioningField" + } + }, + "negative_conditioning": { + "id": "9393779e-796c-4f64-b740-902a1177bf53", + "name": "negative_conditioning", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "ConditioningField" + } + }, + "noise": { + "id": "8e17f1e5-4f98-40b1-b7f4-86aeeb4554c1", + "name": "noise", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "LatentsField" + } + }, + "steps": { + "id": "9b63302d-6bd2-42c9-ac13-9b1afb51af88", + "name": "steps", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 50 + }, + "cfg_scale": { + "id": "87dd04d3-870e-49e1-98bf-af003a810109", + "name": "cfg_scale", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": true, + "name": "FloatField" + }, + "value": 7.5 + }, + "denoising_start": { + "id": "f369d80f-4931-4740-9bcd-9f0620719fab", + "name": "denoising_start", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "FloatField" + }, + "value": 0 + }, + "denoising_end": { + "id": "747d10e5-6f02-445c-994c-0604d814de8c", + "name": "denoising_end", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "FloatField" + }, + "value": 1 + }, + "scheduler": { + "id": "1de84a4e-3a24-4ec8-862b-16ce49633b9b", + "name": "scheduler", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "SchedulerField" + }, + "value": "unipc" + }, + "unet": { + "id": "ffa6fef4-3ce2-4bdb-9296-9a834849489b", + "name": "unet", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "UNetField" + } + }, + "control": { + "id": "077b64cb-34be-4fcc-83f2-e399807a02bd", + "name": "control", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": true, + "name": "ControlField" + } + }, + "ip_adapter": { + "id": "1d6948f7-3a65-4a65-a20c-768b287251aa", + "name": "ip_adapter", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": true, + "name": "IPAdapterField" + } + }, + "t2i_adapter": { + "id": "75e67b09-952f-4083-aaf4-6b804d690412", + "name": "t2i_adapter", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": true, + "name": "T2IAdapterField" + } + }, + "cfg_rescale_multiplier": { + "id": "9101f0a6-5fe0-4826-b7b3-47e5d506826c", + "name": "cfg_rescale_multiplier", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "FloatField" + }, + "value": 0 + }, + "latents": { + "id": "334d4ba3-5a99-4195-82c5-86fb3f4f7d43", + "name": "latents", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "LatentsField" + } + }, + "denoise_mask": { + "id": "0d3dbdbf-b014-4e95-8b18-ff2ff9cb0bfa", + "name": "denoise_mask", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "DenoiseMaskField" + } + } + }, + "outputs": { + "latents": { + "id": "70fa5bbc-0c38-41bb-861a-74d6d78d2f38", + "name": "latents", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "LatentsField" + } + }, + "width": { + "id": "98ee0e6c-82aa-4e8f-8be5-dc5f00ee47f0", + "name": "width", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + } + }, + "height": { + "id": "e8cb184a-5e1a-47c8-9695-4b8979564f5d", + "name": "height", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + } + } + } + }, + "width": 320, + "height": 703, + "position": { + "x": 1400, + "y": 25 + } + }, + { + "id": "58c957f5-0d01-41fc-a803-b2bbf0413d4f", + "type": "invocation", + "data": { + "id": "58c957f5-0d01-41fc-a803-b2bbf0413d4f", + "type": "l2i", + "label": "", + "isOpen": true, + "notes": "", + "isIntermediate": false, + "useCache": true, + "version": "1.2.0", + "nodePack": "invokeai", + "inputs": { + "metadata": { + "id": "ab375f12-0042-4410-9182-29e30db82c85", + "name": "metadata", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "MetadataField" + } + }, + "latents": { + "id": "3a7e7efd-bff5-47d7-9d48-615127afee78", + "name": "latents", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "LatentsField" + } + }, + "vae": { + "id": "a1f5f7a1-0795-4d58-b036-7820c0b0ef2b", + "name": "vae", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "VaeField" + } + }, + "tiled": { + "id": "da52059a-0cee-4668-942f-519aa794d739", + "name": "tiled", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "BooleanField" + }, + "value": false + }, + "fp32": { + "id": "c4841df3-b24e-4140-be3b-ccd454c2522c", + "name": "fp32", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "BooleanField" + }, + "value": true + } + }, + "outputs": { + "image": { + "id": "72d667d0-cf85-459d-abf2-28bd8b823fe7", + "name": "image", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "ImageField" + } + }, + "width": { + "id": "c8c907d8-1066-49d1-b9a6-83bdcd53addc", + "name": "width", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + } + }, + "height": { + "id": "230f359c-b4ea-436c-b372-332d7dcdca85", + "name": "height", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + } + } + } + }, + "width": 320, + "height": 266, + "position": { + "x": 1800, + "y": 25 + } + } + ], + "edges": [ + { + "id": "reactflow__edge-ea94bc37-d995-4a83-aa99-4af42479f2f2value-55705012-79b9-4aac-9f26-c0b10309785bseed", + "source": "ea94bc37-d995-4a83-aa99-4af42479f2f2", + "target": "55705012-79b9-4aac-9f26-c0b10309785b", + "type": "default", + "sourceHandle": "value", + "targetHandle": "seed" + }, + { + "id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8clip-7d8bf987-284f-413a-b2fd-d825445a5d6cclip", + "source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8", + "target": "7d8bf987-284f-413a-b2fd-d825445a5d6c", + "type": "default", + "sourceHandle": "clip", + "targetHandle": "clip" + }, + { + "id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8clip-93dc02a4-d05b-48ed-b99c-c9b616af3402clip", + "source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8", + "target": "93dc02a4-d05b-48ed-b99c-c9b616af3402", + "type": "default", + "sourceHandle": "clip", + "targetHandle": "clip" + }, + { + "id": "reactflow__edge-55705012-79b9-4aac-9f26-c0b10309785bnoise-eea2702a-19fb-45b5-9d75-56b4211ec03cnoise", + "source": "55705012-79b9-4aac-9f26-c0b10309785b", + "target": "eea2702a-19fb-45b5-9d75-56b4211ec03c", + "type": "default", + "sourceHandle": "noise", + "targetHandle": "noise" + }, + { + "id": "reactflow__edge-7d8bf987-284f-413a-b2fd-d825445a5d6cconditioning-eea2702a-19fb-45b5-9d75-56b4211ec03cpositive_conditioning", + "source": "7d8bf987-284f-413a-b2fd-d825445a5d6c", + "target": "eea2702a-19fb-45b5-9d75-56b4211ec03c", + "type": "default", + "sourceHandle": "conditioning", + "targetHandle": "positive_conditioning" + }, + { + "id": "reactflow__edge-93dc02a4-d05b-48ed-b99c-c9b616af3402conditioning-eea2702a-19fb-45b5-9d75-56b4211ec03cnegative_conditioning", + "source": "93dc02a4-d05b-48ed-b99c-c9b616af3402", + "target": "eea2702a-19fb-45b5-9d75-56b4211ec03c", + "type": "default", + "sourceHandle": "conditioning", + "targetHandle": "negative_conditioning" + }, + { + "id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8unet-eea2702a-19fb-45b5-9d75-56b4211ec03cunet", + "source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8", + "target": "eea2702a-19fb-45b5-9d75-56b4211ec03c", + "type": "default", + "sourceHandle": "unet", + "targetHandle": "unet" + }, + { + "id": "reactflow__edge-eea2702a-19fb-45b5-9d75-56b4211ec03clatents-58c957f5-0d01-41fc-a803-b2bbf0413d4flatents", + "source": "eea2702a-19fb-45b5-9d75-56b4211ec03c", + "target": "58c957f5-0d01-41fc-a803-b2bbf0413d4f", + "type": "default", + "sourceHandle": "latents", + "targetHandle": "latents" + }, + { + "id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8vae-58c957f5-0d01-41fc-a803-b2bbf0413d4fvae", + "source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8", + "target": "58c957f5-0d01-41fc-a803-b2bbf0413d4f", + "type": "default", + "sourceHandle": "vae", + "targetHandle": "vae" + } + ] +} diff --git a/invokeai/app/services/workflow_records/default_workflows/TextToImage_SDXL.json b/invokeai/app/services/workflow_records/default_workflows/TextToImage_SDXL.json new file mode 100644 index 0000000000..ef7810c153 --- /dev/null +++ b/invokeai/app/services/workflow_records/default_workflows/TextToImage_SDXL.json @@ -0,0 +1,1320 @@ +{ + "name": "SDXL Text to Image", + "author": "InvokeAI", + "description": "Sample text to image workflow for SDXL", + "version": "1.0.1", + "contact": "invoke@invoke.ai", + "tags": "text2image, SDXL, default", + "notes": "", + "exposedFields": [ + { + "nodeId": "ade2c0d3-0384-4157-b39b-29ce429cfa15", + "fieldName": "value" + }, + { + "nodeId": "719dabe8-8297-4749-aea1-37be301cd425", + "fieldName": "value" + }, + { + "nodeId": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", + "fieldName": "model" + }, + { + "nodeId": "0093692f-9cf4-454d-a5b8-62f0e3eb3bb8", + "fieldName": "vae_model" + } + ], + "meta": { + "category": "default", + "version": "2.0.0" + }, + "nodes": [ + { + "id": "3774ec24-a69e-4254-864c-097d07a6256f", + "type": "invocation", + "data": { + "id": "3774ec24-a69e-4254-864c-097d07a6256f", + "type": "string_join", + "label": "Positive Style Concat", + "isOpen": false, + "notes": "", + "isIntermediate": true, + "useCache": true, + "version": "1.0.0", + "inputs": { + "string_left": { + "id": "8d84be5c-4a96-46ad-a92c-eaf6fcae4a69", + "name": "string_left", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "StringField" + }, + "value": "" + }, + "string_right": { + "id": "c8e2a881-f675-4c6b-865b-a0892473b750", + "name": "string_right", + "fieldKind": "input", + "label": "Positive Style Concat", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "StringField" + }, + "value": "" + } + }, + "outputs": { + "value": { + "id": "196fad08-73ea-4fe5-8cc3-b55fd3cf40e5", + "name": "value", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "StringField" + } + } + } + }, + "width": 320, + "height": 32, + "position": { + "x": 750, + "y": -225 + } + }, + { + "id": "719dabe8-8297-4749-aea1-37be301cd425", + "type": "invocation", + "data": { + "id": "719dabe8-8297-4749-aea1-37be301cd425", + "type": "string", + "label": "Negative Prompt", + "isOpen": true, + "notes": "", + "isIntermediate": true, + "useCache": true, + "version": "1.0.0", + "inputs": { + "value": { + "id": "744a5f7c-6e3a-4fbc-ac66-ba0cf8559eeb", + "name": "value", + "fieldKind": "input", + "label": "Negative Prompt", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "StringField" + }, + "value": "photograph" + } + }, + "outputs": { + "value": { + "id": "3e0ddf7a-a5de-4dad-b726-5d0cb4e0baa6", + "name": "value", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "StringField" + } + } + } + }, + "width": 320, + "height": 258, + "position": { + "x": 750, + "y": -125 + } + }, + { + "id": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", + "type": "invocation", + "data": { + "id": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", + "type": "sdxl_compel_prompt", + "label": "SDXL Negative Compel Prompt", + "isOpen": false, + "notes": "", + "isIntermediate": true, + "useCache": true, + "version": "1.0.0", + "nodePack": "invokeai", + "inputs": { + "prompt": { + "id": "5a6889e6-95cb-462f-8f4a-6b93ae7afaec", + "name": "prompt", + "fieldKind": "input", + "label": "Negative Prompt", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "StringField" + }, + "value": "" + }, + "style": { + "id": "f240d0e6-3a1c-4320-af23-20ebb707c276", + "name": "style", + "fieldKind": "input", + "label": "Negative Style", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "StringField" + }, + "value": "" + }, + "original_width": { + "id": "05af07b0-99a0-4a68-8ad2-697bbdb7fc7e", + "name": "original_width", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 1024 + }, + "original_height": { + "id": "2c771996-a998-43b7-9dd3-3792664d4e5b", + "name": "original_height", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 1024 + }, + "crop_top": { + "id": "66519dca-a151-4e3e-ae1f-88f1f9877bde", + "name": "crop_top", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 0 + }, + "crop_left": { + "id": "349cf2e9-f3d0-4e16-9ae2-7097d25b6a51", + "name": "crop_left", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 0 + }, + "target_width": { + "id": "44499347-7bd6-4a73-99d6-5a982786db05", + "name": "target_width", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 1024 + }, + "target_height": { + "id": "fda359b0-ab80-4f3c-805b-c9f61319d7d2", + "name": "target_height", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 1024 + }, + "clip": { + "id": "b447adaf-a649-4a76-a827-046a9fc8d89b", + "name": "clip", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "ClipField" + } + }, + "clip2": { + "id": "86ee4e32-08f9-4baa-9163-31d93f5c0187", + "name": "clip2", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "ClipField" + } + } + }, + "outputs": { + "conditioning": { + "id": "7c10118e-7b4e-4911-b98e-d3ba6347dfd0", + "name": "conditioning", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "ConditioningField" + } + } + } + }, + "width": 320, + "height": 32, + "position": { + "x": 750, + "y": 200 + } + }, + { + "id": "55705012-79b9-4aac-9f26-c0b10309785b", + "type": "invocation", + "data": { + "id": "55705012-79b9-4aac-9f26-c0b10309785b", + "type": "noise", + "label": "", + "isOpen": true, + "notes": "", + "isIntermediate": true, + "useCache": true, + "version": "1.0.1", + "nodePack": "invokeai", + "inputs": { + "seed": { + "id": "6431737c-918a-425d-a3b4-5d57e2f35d4d", + "name": "seed", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 0 + }, + "width": { + "id": "38fc5b66-fe6e-47c8-bba9-daf58e454ed7", + "name": "width", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 1024 + }, + "height": { + "id": "16298330-e2bf-4872-a514-d6923df53cbb", + "name": "height", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 1024 + }, + "use_cpu": { + "id": "c7c436d3-7a7a-4e76-91e4-c6deb271623c", + "name": "use_cpu", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "BooleanField" + }, + "value": true + } + }, + "outputs": { + "noise": { + "id": "50f650dc-0184-4e23-a927-0497a96fe954", + "name": "noise", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "LatentsField" + } + }, + "width": { + "id": "bb8a452b-133d-42d1-ae4a-3843d7e4109a", + "name": "width", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + } + }, + "height": { + "id": "35cfaa12-3b8b-4b7a-a884-327ff3abddd9", + "name": "height", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + } + } + } + }, + "width": 320, + "height": 388, + "position": { + "x": 375, + "y": 0 + } + }, + { + "id": "ea94bc37-d995-4a83-aa99-4af42479f2f2", + "type": "invocation", + "data": { + "id": "ea94bc37-d995-4a83-aa99-4af42479f2f2", + "type": "rand_int", + "label": "Random Seed", + "isOpen": false, + "notes": "", + "isIntermediate": true, + "useCache": false, + "version": "1.0.0", + "nodePack": "invokeai", + "inputs": { + "low": { + "id": "3ec65a37-60ba-4b6c-a0b2-553dd7a84b84", + "name": "low", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 0 + }, + "high": { + "id": "085f853a-1a5f-494d-8bec-e4ba29a3f2d1", + "name": "high", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 2147483647 + } + }, + "outputs": { + "value": { + "id": "812ade4d-7699-4261-b9fc-a6c9d2ab55ee", + "name": "value", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + } + } + } + }, + "width": 320, + "height": 32, + "position": { + "x": 375, + "y": -50 + } + }, + { + "id": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", + "type": "invocation", + "data": { + "id": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", + "type": "sdxl_model_loader", + "label": "", + "isOpen": true, + "notes": "", + "isIntermediate": true, + "useCache": true, + "version": "1.0.0", + "nodePack": "invokeai", + "inputs": { + "model": { + "id": "39f9e799-bc95-4318-a200-30eed9e60c42", + "name": "model", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "SDXLMainModelField" + }, + "value": null + } + }, + "outputs": { + "unet": { + "id": "2626a45e-59aa-4609-b131-2d45c5eaed69", + "name": "unet", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "UNetField" + } + }, + "clip": { + "id": "7c9c42fa-93d5-4639-ab8b-c4d9b0559baf", + "name": "clip", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "ClipField" + } + }, + "clip2": { + "id": "0dafddcf-a472-49c1-a47c-7b8fab4c8bc9", + "name": "clip2", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "ClipField" + } + }, + "vae": { + "id": "ee6a6997-1b3c-4ff3-99ce-1e7bfba2750c", + "name": "vae", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "VaeField" + } + } + } + }, + "width": 320, + "height": 257, + "position": { + "x": 375, + "y": -500 + } + }, + { + "id": "faf965a4-7530-427b-b1f3-4ba6505c2a08", + "type": "invocation", + "data": { + "id": "faf965a4-7530-427b-b1f3-4ba6505c2a08", + "type": "sdxl_compel_prompt", + "label": "SDXL Positive Compel Prompt", + "isOpen": false, + "notes": "", + "isIntermediate": true, + "useCache": true, + "version": "1.0.0", + "nodePack": "invokeai", + "inputs": { + "prompt": { + "id": "5a6889e6-95cb-462f-8f4a-6b93ae7afaec", + "name": "prompt", + "fieldKind": "input", + "label": "Positive Prompt", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "StringField" + }, + "value": "" + }, + "style": { + "id": "f240d0e6-3a1c-4320-af23-20ebb707c276", + "name": "style", + "fieldKind": "input", + "label": "Positive Style", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "StringField" + }, + "value": "" + }, + "original_width": { + "id": "05af07b0-99a0-4a68-8ad2-697bbdb7fc7e", + "name": "original_width", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 1024 + }, + "original_height": { + "id": "2c771996-a998-43b7-9dd3-3792664d4e5b", + "name": "original_height", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 1024 + }, + "crop_top": { + "id": "66519dca-a151-4e3e-ae1f-88f1f9877bde", + "name": "crop_top", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 0 + }, + "crop_left": { + "id": "349cf2e9-f3d0-4e16-9ae2-7097d25b6a51", + "name": "crop_left", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 0 + }, + "target_width": { + "id": "44499347-7bd6-4a73-99d6-5a982786db05", + "name": "target_width", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 1024 + }, + "target_height": { + "id": "fda359b0-ab80-4f3c-805b-c9f61319d7d2", + "name": "target_height", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 1024 + }, + "clip": { + "id": "b447adaf-a649-4a76-a827-046a9fc8d89b", + "name": "clip", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "ClipField" + } + }, + "clip2": { + "id": "86ee4e32-08f9-4baa-9163-31d93f5c0187", + "name": "clip2", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "ClipField" + } + } + }, + "outputs": { + "conditioning": { + "id": "7c10118e-7b4e-4911-b98e-d3ba6347dfd0", + "name": "conditioning", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "ConditioningField" + } + } + } + }, + "width": 320, + "height": 32, + "position": { + "x": 750, + "y": -175 + } + }, + { + "id": "63e91020-83b2-4f35-b174-ad9692aabb48", + "type": "invocation", + "data": { + "id": "63e91020-83b2-4f35-b174-ad9692aabb48", + "type": "l2i", + "label": "", + "isOpen": true, + "notes": "", + "isIntermediate": false, + "useCache": false, + "version": "1.2.0", + "nodePack": "invokeai", + "inputs": { + "metadata": { + "id": "88971324-3fdb-442d-b8b7-7612478a8622", + "name": "metadata", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "MetadataField" + } + }, + "latents": { + "id": "da0e40cb-c49f-4fa5-9856-338b91a65f6b", + "name": "latents", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "LatentsField" + } + }, + "vae": { + "id": "ae5164ce-1710-4ec5-a83a-6113a0d1b5c0", + "name": "vae", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "VaeField" + } + }, + "tiled": { + "id": "2ccfd535-1a7b-4ecf-84db-9430a64fb3d7", + "name": "tiled", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "BooleanField" + }, + "value": false + }, + "fp32": { + "id": "64f07d5a-54a2-429c-8c5b-0c2a3a8e5cd5", + "name": "fp32", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "BooleanField" + }, + "value": false + } + }, + "outputs": { + "image": { + "id": "9b281eaa-6504-407d-a5ca-1e5e8020a4bf", + "name": "image", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "ImageField" + } + }, + "width": { + "id": "98e545f3-b53b-490d-b94d-bed9418ccc75", + "name": "width", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + } + }, + "height": { + "id": "4a74bd43-d7f7-4c7f-bb3b-d09bb2992c46", + "name": "height", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + } + } + } + }, + "width": 320, + "height": 266, + "position": { + "x": 1475, + "y": -500 + } + }, + { + "id": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb", + "type": "invocation", + "data": { + "id": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb", + "type": "denoise_latents", + "label": "", + "isOpen": true, + "notes": "", + "isIntermediate": true, + "useCache": true, + "version": "1.5.0", + "nodePack": "invokeai", + "inputs": { + "positive_conditioning": { + "id": "29b73dfa-a06e-4b4a-a844-515b9eb93a81", + "name": "positive_conditioning", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "ConditioningField" + } + }, + "negative_conditioning": { + "id": "a81e6f5b-f4de-4919-b483-b6e2f067465a", + "name": "negative_conditioning", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "ConditioningField" + } + }, + "noise": { + "id": "4ba06bb7-eb45-4fb9-9984-31001b545587", + "name": "noise", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "LatentsField" + } + }, + "steps": { + "id": "36ee8a45-ca69-44bc-9bc3-aa881e6045c0", + "name": "steps", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + }, + "value": 32 + }, + "cfg_scale": { + "id": "2a2024e0-a736-46ec-933c-c1c1ebe96943", + "name": "cfg_scale", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": true, + "name": "FloatField" + }, + "value": 6 + }, + "denoising_start": { + "id": "be219d5e-41b7-430a-8fb5-bc21a31ad219", + "name": "denoising_start", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "FloatField" + }, + "value": 0 + }, + "denoising_end": { + "id": "3adfb7ae-c9f7-4a40-b6e0-4c2050bd1a99", + "name": "denoising_end", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "FloatField" + }, + "value": 1 + }, + "scheduler": { + "id": "14423e0d-7215-4ee0-b065-f9e95eaa8d7d", + "name": "scheduler", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "SchedulerField" + }, + "value": "dpmpp_2m_sde_k" + }, + "unet": { + "id": "e73bbf98-6489-492b-b83c-faed215febac", + "name": "unet", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "UNetField" + } + }, + "control": { + "id": "dab351b3-0c86-4ea5-9782-4e8edbfb0607", + "name": "control", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": true, + "name": "ControlField" + } + }, + "ip_adapter": { + "id": "192daea0-a90a-43cc-a2ee-0114a8e90318", + "name": "ip_adapter", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": true, + "name": "IPAdapterField" + } + }, + "t2i_adapter": { + "id": "ee386a55-d4c7-48c1-ac57-7bc4e3aada7a", + "name": "t2i_adapter", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": true, + "name": "T2IAdapterField" + } + }, + "cfg_rescale_multiplier": { + "id": "106bbe8d-e641-4034-9a39-d4e82c298da1", + "name": "cfg_rescale_multiplier", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "FloatField" + }, + "value": 0 + }, + "latents": { + "id": "3a922c6a-3d8c-4c9e-b3ec-2f4d81cda077", + "name": "latents", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "LatentsField" + } + }, + "denoise_mask": { + "id": "cd7ce032-835f-495f-8b45-d57272f33132", + "name": "denoise_mask", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "DenoiseMaskField" + } + } + }, + "outputs": { + "latents": { + "id": "6260b84f-8361-470a-98d8-5b22a45c2d8c", + "name": "latents", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "LatentsField" + } + }, + "width": { + "id": "aede0ecf-25b6-46be-aa30-b77f79715deb", + "name": "width", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + } + }, + "height": { + "id": "519abf62-d475-48ef-ab8f-66136bc0e499", + "name": "height", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "IntegerField" + } + } + } + }, + "width": 320, + "height": 702, + "position": { + "x": 1125, + "y": -500 + } + }, + { + "id": "0093692f-9cf4-454d-a5b8-62f0e3eb3bb8", + "type": "invocation", + "data": { + "id": "0093692f-9cf4-454d-a5b8-62f0e3eb3bb8", + "type": "vae_loader", + "label": "", + "isOpen": true, + "notes": "", + "isIntermediate": true, + "useCache": true, + "version": "1.0.0", + "inputs": { + "vae_model": { + "id": "28a17000-b629-49c6-b945-77c591cf7440", + "name": "vae_model", + "fieldKind": "input", + "label": "VAE (use the FP16 model)", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "VAEModelField" + }, + "value": null + } + }, + "outputs": { + "vae": { + "id": "a34892b6-ba6d-44eb-8a68-af1f40a84186", + "name": "vae", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "VaeField" + } + } + } + }, + "width": 320, + "height": 161, + "position": { + "x": 375, + "y": -225 + } + }, + { + "id": "ade2c0d3-0384-4157-b39b-29ce429cfa15", + "type": "invocation", + "data": { + "id": "ade2c0d3-0384-4157-b39b-29ce429cfa15", + "type": "string", + "label": "Positive Prompt", + "isOpen": true, + "notes": "", + "isIntermediate": true, + "useCache": true, + "version": "1.0.0", + "inputs": { + "value": { + "id": "744a5f7c-6e3a-4fbc-ac66-ba0cf8559eeb", + "name": "value", + "fieldKind": "input", + "label": "Positive Prompt", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "StringField" + }, + "value": "Super cute tiger cub, fierce, traditional chinese watercolor" + } + }, + "outputs": { + "value": { + "id": "3e0ddf7a-a5de-4dad-b726-5d0cb4e0baa6", + "name": "value", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "StringField" + } + } + } + }, + "width": 320, + "height": 258, + "position": { + "x": 750, + "y": -500 + } + }, + { + "id": "ad8fa655-3a76-43d0-9c02-4d7644dea650", + "type": "invocation", + "data": { + "id": "ad8fa655-3a76-43d0-9c02-4d7644dea650", + "type": "string_join", + "label": "Negative Style Concat", + "isOpen": false, + "notes": "", + "isIntermediate": true, + "useCache": true, + "version": "1.0.0", + "inputs": { + "string_left": { + "id": "8d84be5c-4a96-46ad-a92c-eaf6fcae4a69", + "name": "string_left", + "fieldKind": "input", + "label": "", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "StringField" + }, + "value": "" + }, + "string_right": { + "id": "c8e2a881-f675-4c6b-865b-a0892473b750", + "name": "string_right", + "fieldKind": "input", + "label": "Negative Style Prompt", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "StringField" + }, + "value": "" + } + }, + "outputs": { + "value": { + "id": "196fad08-73ea-4fe5-8cc3-b55fd3cf40e5", + "name": "value", + "fieldKind": "output", + "type": { + "isCollection": false, + "isCollectionOrScalar": false, + "name": "StringField" + } + } + } + }, + "width": 320, + "height": 32, + "position": { + "x": 750, + "y": 150 + } + } + ], + "edges": [ + { + "id": "3774ec24-a69e-4254-864c-097d07a6256f-faf965a4-7530-427b-b1f3-4ba6505c2a08-collapsed", + "source": "3774ec24-a69e-4254-864c-097d07a6256f", + "target": "faf965a4-7530-427b-b1f3-4ba6505c2a08", + "type": "collapsed" + }, + { + "id": "ad8fa655-3a76-43d0-9c02-4d7644dea650-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204-collapsed", + "source": "ad8fa655-3a76-43d0-9c02-4d7644dea650", + "target": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", + "type": "collapsed" + }, + { + "id": "reactflow__edge-ea94bc37-d995-4a83-aa99-4af42479f2f2value-55705012-79b9-4aac-9f26-c0b10309785bseed", + "source": "ea94bc37-d995-4a83-aa99-4af42479f2f2", + "target": "55705012-79b9-4aac-9f26-c0b10309785b", + "type": "default", + "sourceHandle": "value", + "targetHandle": "seed" + }, + { + "id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22clip-faf965a4-7530-427b-b1f3-4ba6505c2a08clip", + "source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", + "target": "faf965a4-7530-427b-b1f3-4ba6505c2a08", + "type": "default", + "sourceHandle": "clip", + "targetHandle": "clip" + }, + { + "id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22clip2-faf965a4-7530-427b-b1f3-4ba6505c2a08clip2", + "source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", + "target": "faf965a4-7530-427b-b1f3-4ba6505c2a08", + "type": "default", + "sourceHandle": "clip2", + "targetHandle": "clip2" + }, + { + "id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22clip-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204clip", + "source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", + "target": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", + "type": "default", + "sourceHandle": "clip", + "targetHandle": "clip" + }, + { + "id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22clip2-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204clip2", + "source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", + "target": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", + "type": "default", + "sourceHandle": "clip2", + "targetHandle": "clip2" + }, + { + "id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22unet-50a36525-3c0a-4cc5-977c-e4bfc3fd6dfbunet", + "source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", + "target": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb", + "type": "default", + "sourceHandle": "unet", + "targetHandle": "unet" + }, + { + "id": "reactflow__edge-faf965a4-7530-427b-b1f3-4ba6505c2a08conditioning-50a36525-3c0a-4cc5-977c-e4bfc3fd6dfbpositive_conditioning", + "source": "faf965a4-7530-427b-b1f3-4ba6505c2a08", + "target": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb", + "type": "default", + "sourceHandle": "conditioning", + "targetHandle": "positive_conditioning" + }, + { + "id": "reactflow__edge-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204conditioning-50a36525-3c0a-4cc5-977c-e4bfc3fd6dfbnegative_conditioning", + "source": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", + "target": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb", + "type": "default", + "sourceHandle": "conditioning", + "targetHandle": "negative_conditioning" + }, + { + "id": "reactflow__edge-55705012-79b9-4aac-9f26-c0b10309785bnoise-50a36525-3c0a-4cc5-977c-e4bfc3fd6dfbnoise", + "source": "55705012-79b9-4aac-9f26-c0b10309785b", + "target": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb", + "type": "default", + "sourceHandle": "noise", + "targetHandle": "noise" + }, + { + "id": "reactflow__edge-50a36525-3c0a-4cc5-977c-e4bfc3fd6dfblatents-63e91020-83b2-4f35-b174-ad9692aabb48latents", + "source": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb", + "target": "63e91020-83b2-4f35-b174-ad9692aabb48", + "type": "default", + "sourceHandle": "latents", + "targetHandle": "latents" + }, + { + "id": "reactflow__edge-0093692f-9cf4-454d-a5b8-62f0e3eb3bb8vae-63e91020-83b2-4f35-b174-ad9692aabb48vae", + "source": "0093692f-9cf4-454d-a5b8-62f0e3eb3bb8", + "target": "63e91020-83b2-4f35-b174-ad9692aabb48", + "type": "default", + "sourceHandle": "vae", + "targetHandle": "vae" + }, + { + "id": "reactflow__edge-ade2c0d3-0384-4157-b39b-29ce429cfa15value-faf965a4-7530-427b-b1f3-4ba6505c2a08prompt", + "source": "ade2c0d3-0384-4157-b39b-29ce429cfa15", + "target": "faf965a4-7530-427b-b1f3-4ba6505c2a08", + "type": "default", + "sourceHandle": "value", + "targetHandle": "prompt" + }, + { + "id": "reactflow__edge-719dabe8-8297-4749-aea1-37be301cd425value-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204prompt", + "source": "719dabe8-8297-4749-aea1-37be301cd425", + "target": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", + "type": "default", + "sourceHandle": "value", + "targetHandle": "prompt" + }, + { + "id": "reactflow__edge-719dabe8-8297-4749-aea1-37be301cd425value-ad8fa655-3a76-43d0-9c02-4d7644dea650string_left", + "source": "719dabe8-8297-4749-aea1-37be301cd425", + "target": "ad8fa655-3a76-43d0-9c02-4d7644dea650", + "type": "default", + "sourceHandle": "value", + "targetHandle": "string_left" + }, + { + "id": "reactflow__edge-ad8fa655-3a76-43d0-9c02-4d7644dea650value-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204style", + "source": "ad8fa655-3a76-43d0-9c02-4d7644dea650", + "target": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", + "type": "default", + "sourceHandle": "value", + "targetHandle": "style" + }, + { + "id": "reactflow__edge-ade2c0d3-0384-4157-b39b-29ce429cfa15value-3774ec24-a69e-4254-864c-097d07a6256fstring_left", + "source": "ade2c0d3-0384-4157-b39b-29ce429cfa15", + "target": "3774ec24-a69e-4254-864c-097d07a6256f", + "type": "default", + "sourceHandle": "value", + "targetHandle": "string_left" + }, + { + "id": "reactflow__edge-3774ec24-a69e-4254-864c-097d07a6256fvalue-faf965a4-7530-427b-b1f3-4ba6505c2a08style", + "source": "3774ec24-a69e-4254-864c-097d07a6256f", + "target": "faf965a4-7530-427b-b1f3-4ba6505c2a08", + "type": "default", + "sourceHandle": "value", + "targetHandle": "style" + } + ] +} diff --git a/invokeai/app/services/workflow_records/workflow_records_base.py b/invokeai/app/services/workflow_records/workflow_records_base.py index d5a4b25ce4..499b0f005d 100644 --- a/invokeai/app/services/workflow_records/workflow_records_base.py +++ b/invokeai/app/services/workflow_records/workflow_records_base.py @@ -1,17 +1,50 @@ from abc import ABC, abstractmethod +from typing import Optional -from invokeai.app.invocations.baseinvocation import WorkflowField +from invokeai.app.services.shared.pagination import PaginatedResults +from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection +from invokeai.app.services.workflow_records.workflow_records_common import ( + Workflow, + WorkflowCategory, + WorkflowRecordDTO, + WorkflowRecordListItemDTO, + WorkflowRecordOrderBy, + WorkflowWithoutID, +) class WorkflowRecordsStorageBase(ABC): """Base class for workflow storage services.""" @abstractmethod - def get(self, workflow_id: str) -> WorkflowField: + def get(self, workflow_id: str) -> WorkflowRecordDTO: """Get workflow by id.""" pass @abstractmethod - def create(self, workflow: WorkflowField) -> WorkflowField: + def create(self, workflow: WorkflowWithoutID) -> WorkflowRecordDTO: """Creates a workflow.""" pass + + @abstractmethod + def update(self, workflow: Workflow) -> WorkflowRecordDTO: + """Updates a workflow.""" + pass + + @abstractmethod + def delete(self, workflow_id: str) -> None: + """Deletes a workflow.""" + pass + + @abstractmethod + def get_many( + self, + page: int, + per_page: int, + order_by: WorkflowRecordOrderBy, + direction: SQLiteDirection, + category: WorkflowCategory, + query: Optional[str], + ) -> PaginatedResults[WorkflowRecordListItemDTO]: + """Gets many workflows.""" + pass diff --git a/invokeai/app/services/workflow_records/workflow_records_common.py b/invokeai/app/services/workflow_records/workflow_records_common.py index 3a2b13f565..599d2750c2 100644 --- a/invokeai/app/services/workflow_records/workflow_records_common.py +++ b/invokeai/app/services/workflow_records/workflow_records_common.py @@ -1,2 +1,105 @@ +import datetime +from enum import Enum +from typing import Any, Union + +import semver +from pydantic import BaseModel, Field, JsonValue, TypeAdapter, field_validator + +from invokeai.app.util.metaenum import MetaEnum +from invokeai.app.util.misc import uuid_string + +__workflow_meta_version__ = semver.Version.parse("1.0.0") + + +class ExposedField(BaseModel): + nodeId: str + fieldName: str + + class WorkflowNotFoundError(Exception): """Raised when a workflow is not found""" + + +class WorkflowRecordOrderBy(str, Enum, metaclass=MetaEnum): + """The order by options for workflow records""" + + CreatedAt = "created_at" + UpdatedAt = "updated_at" + OpenedAt = "opened_at" + Name = "name" + + +class WorkflowCategory(str, Enum, metaclass=MetaEnum): + User = "user" + Default = "default" + + +class WorkflowMeta(BaseModel): + version: str = Field(description="The version of the workflow schema.") + category: WorkflowCategory = Field( + default=WorkflowCategory.User, description="The category of the workflow (user or default)." + ) + + @field_validator("version") + def validate_version(cls, version: str): + try: + semver.Version.parse(version) + return version + except Exception: + raise ValueError(f"Invalid workflow meta version: {version}") + + def to_semver(self) -> semver.Version: + return semver.Version.parse(self.version) + + +class WorkflowWithoutID(BaseModel): + name: str = Field(description="The name of the workflow.") + author: str = Field(description="The author of the workflow.") + description: str = Field(description="The description of the workflow.") + version: str = Field(description="The version of the workflow.") + contact: str = Field(description="The contact of the workflow.") + tags: str = Field(description="The tags of the workflow.") + notes: str = Field(description="The notes of the workflow.") + exposedFields: list[ExposedField] = Field(description="The exposed fields of the workflow.") + meta: WorkflowMeta = Field(description="The meta of the workflow.") + # TODO: nodes and edges are very loosely typed + nodes: list[dict[str, JsonValue]] = Field(description="The nodes of the workflow.") + edges: list[dict[str, JsonValue]] = Field(description="The edges of the workflow.") + + +WorkflowWithoutIDValidator = TypeAdapter(WorkflowWithoutID) + + +class Workflow(WorkflowWithoutID): + id: str = Field(default_factory=uuid_string, description="The id of the workflow.") + + +WorkflowValidator = TypeAdapter(Workflow) + + +class WorkflowRecordDTOBase(BaseModel): + workflow_id: str = Field(description="The id of the workflow.") + name: str = Field(description="The name of the workflow.") + created_at: Union[datetime.datetime, str] = Field(description="The created timestamp of the workflow.") + updated_at: Union[datetime.datetime, str] = Field(description="The updated timestamp of the workflow.") + opened_at: Union[datetime.datetime, str] = Field(description="The opened timestamp of the workflow.") + + +class WorkflowRecordDTO(WorkflowRecordDTOBase): + workflow: Workflow = Field(description="The workflow.") + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "WorkflowRecordDTO": + data["workflow"] = WorkflowValidator.validate_json(data.get("workflow", "")) + return WorkflowRecordDTOValidator.validate_python(data) + + +WorkflowRecordDTOValidator = TypeAdapter(WorkflowRecordDTO) + + +class WorkflowRecordListItemDTO(WorkflowRecordDTOBase): + description: str = Field(description="The description of the workflow.") + category: WorkflowCategory = Field(description="The description of the workflow.") + + +WorkflowRecordListItemDTOValidator = TypeAdapter(WorkflowRecordListItemDTO) diff --git a/invokeai/app/services/workflow_records/workflow_records_sqlite.py b/invokeai/app/services/workflow_records/workflow_records_sqlite.py index b0952e8234..c28a7a2d92 100644 --- a/invokeai/app/services/workflow_records/workflow_records_sqlite.py +++ b/invokeai/app/services/workflow_records/workflow_records_sqlite.py @@ -1,20 +1,25 @@ -import sqlite3 -import threading +from pathlib import Path +from typing import Optional -from invokeai.app.invocations.baseinvocation import WorkflowField, WorkflowFieldValidator from invokeai.app.services.invoker import Invoker -from invokeai.app.services.shared.sqlite import SqliteDatabase +from invokeai.app.services.shared.pagination import PaginatedResults +from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection +from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase from invokeai.app.services.workflow_records.workflow_records_base import WorkflowRecordsStorageBase -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowNotFoundError -from invokeai.app.util.misc import uuid_string +from invokeai.app.services.workflow_records.workflow_records_common import ( + Workflow, + WorkflowCategory, + WorkflowNotFoundError, + WorkflowRecordDTO, + WorkflowRecordListItemDTO, + WorkflowRecordListItemDTOValidator, + WorkflowRecordOrderBy, + WorkflowValidator, + WorkflowWithoutID, +) class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase): - _invoker: Invoker - _conn: sqlite3.Connection - _cursor: sqlite3.Cursor - _lock: threading.RLock - def __init__(self, db: SqliteDatabase) -> None: super().__init__() self._lock = db.lock @@ -24,14 +29,25 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase): def start(self, invoker: Invoker) -> None: self._invoker = invoker + self._sync_default_workflows() - def get(self, workflow_id: str) -> WorkflowField: + def get(self, workflow_id: str) -> WorkflowRecordDTO: + """Gets a workflow by ID. Updates the opened_at column.""" try: self._lock.acquire() self._cursor.execute( """--sql - SELECT workflow - FROM workflows + UPDATE workflow_library + SET opened_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') + WHERE workflow_id = ?; + """, + (workflow_id,), + ) + self._conn.commit() + self._cursor.execute( + """--sql + SELECT workflow_id, workflow, name, created_at, updated_at, opened_at + FROM workflow_library WHERE workflow_id = ?; """, (workflow_id,), @@ -39,25 +55,28 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase): row = self._cursor.fetchone() if row is None: raise WorkflowNotFoundError(f"Workflow with id {workflow_id} not found") - return WorkflowFieldValidator.validate_json(row[0]) + return WorkflowRecordDTO.from_dict(dict(row)) except Exception: self._conn.rollback() raise finally: self._lock.release() - def create(self, workflow: WorkflowField) -> WorkflowField: + def create(self, workflow: WorkflowWithoutID) -> WorkflowRecordDTO: try: - # workflows do not have ids until they are saved - workflow_id = uuid_string() - workflow.root["id"] = workflow_id + # Only user workflows may be created by this method + assert workflow.meta.category is WorkflowCategory.User + workflow_with_id = WorkflowValidator.validate_python(workflow.model_dump()) self._lock.acquire() self._cursor.execute( """--sql - INSERT INTO workflows(workflow) - VALUES (?); + INSERT OR IGNORE INTO workflow_library ( + workflow_id, + workflow + ) + VALUES (?, ?); """, - (workflow.model_dump_json(),), + (workflow_with_id.id, workflow_with_id.model_dump_json()), ) self._conn.commit() except Exception: @@ -65,35 +84,231 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase): raise finally: self._lock.release() - return self.get(workflow_id) + return self.get(workflow_with_id.id) + + def update(self, workflow: Workflow) -> WorkflowRecordDTO: + try: + self._lock.acquire() + self._cursor.execute( + """--sql + UPDATE workflow_library + SET workflow = ? + WHERE workflow_id = ? AND category = 'user'; + """, + (workflow.model_dump_json(), workflow.id), + ) + self._conn.commit() + except Exception: + self._conn.rollback() + raise + finally: + self._lock.release() + return self.get(workflow.id) + + def delete(self, workflow_id: str) -> None: + try: + self._lock.acquire() + self._cursor.execute( + """--sql + DELETE from workflow_library + WHERE workflow_id = ? AND category = 'user'; + """, + (workflow_id,), + ) + self._conn.commit() + except Exception: + self._conn.rollback() + raise + finally: + self._lock.release() + return None + + def get_many( + self, + page: int, + per_page: int, + order_by: WorkflowRecordOrderBy, + direction: SQLiteDirection, + category: WorkflowCategory, + query: Optional[str] = None, + ) -> PaginatedResults[WorkflowRecordListItemDTO]: + try: + self._lock.acquire() + # sanitize! + assert order_by in WorkflowRecordOrderBy + assert direction in SQLiteDirection + assert category in WorkflowCategory + count_query = "SELECT COUNT(*) FROM workflow_library WHERE category = ?" + main_query = """ + SELECT + workflow_id, + category, + name, + description, + created_at, + updated_at, + opened_at + FROM workflow_library + WHERE category = ? + """ + main_params: list[int | str] = [category.value] + count_params: list[int | str] = [category.value] + stripped_query = query.strip() if query else None + if stripped_query: + wildcard_query = "%" + stripped_query + "%" + main_query += " AND name LIKE ? OR description LIKE ? " + count_query += " AND name LIKE ? OR description LIKE ?;" + main_params.extend([wildcard_query, wildcard_query]) + count_params.extend([wildcard_query, wildcard_query]) + + main_query += f" ORDER BY {order_by.value} {direction.value} LIMIT ? OFFSET ?;" + main_params.extend([per_page, page * per_page]) + self._cursor.execute(main_query, main_params) + rows = self._cursor.fetchall() + workflows = [WorkflowRecordListItemDTOValidator.validate_python(dict(row)) for row in rows] + + self._cursor.execute(count_query, count_params) + total = self._cursor.fetchone()[0] + pages = int(total / per_page) + 1 + + return PaginatedResults( + items=workflows, + page=page, + per_page=per_page, + pages=pages, + total=total, + ) + except Exception: + self._conn.rollback() + raise + finally: + self._lock.release() + + def _sync_default_workflows(self) -> None: + """Syncs default workflows to the database. Internal use only.""" + + """ + An enhancement might be to only update workflows that have changed. This would require stable + default workflow IDs, and properly incrementing the workflow version. + + It's much simpler to just replace them all with whichever workflows are in the directory. + + The downside is that the `updated_at` and `opened_at` timestamps for default workflows are + meaningless, as they are overwritten every time the server starts. + """ + + try: + self._lock.acquire() + workflows: list[Workflow] = [] + workflows_dir = Path(__file__).parent / Path("default_workflows") + workflow_paths = workflows_dir.glob("*.json") + for path in workflow_paths: + bytes_ = path.read_bytes() + workflow = WorkflowValidator.validate_json(bytes_) + workflows.append(workflow) + # Only default workflows may be managed by this method + assert all(w.meta.category is WorkflowCategory.Default for w in workflows) + self._cursor.execute( + """--sql + DELETE FROM workflow_library + WHERE category = 'default'; + """ + ) + for w in workflows: + self._cursor.execute( + """--sql + INSERT OR REPLACE INTO workflow_library ( + workflow_id, + workflow + ) + VALUES (?, ?); + """, + (w.id, w.model_dump_json()), + ) + self._conn.commit() + except Exception: + self._conn.rollback() + raise + finally: + self._lock.release() def _create_tables(self) -> None: try: self._lock.acquire() self._cursor.execute( """--sql - CREATE TABLE IF NOT EXISTS workflows ( + CREATE TABLE IF NOT EXISTS workflow_library ( + workflow_id TEXT NOT NULL PRIMARY KEY, workflow TEXT NOT NULL, - workflow_id TEXT GENERATED ALWAYS AS (json_extract(workflow, '$.id')) VIRTUAL NOT NULL UNIQUE, -- gets implicit index created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), - updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) -- updated via trigger + -- updated via trigger + updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), + -- updated manually when retrieving workflow + opened_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), + -- Generated columns, needed for indexing and searching + category TEXT GENERATED ALWAYS as (json_extract(workflow, '$.meta.category')) VIRTUAL NOT NULL, + name TEXT GENERATED ALWAYS as (json_extract(workflow, '$.name')) VIRTUAL NOT NULL, + description TEXT GENERATED ALWAYS as (json_extract(workflow, '$.description')) VIRTUAL NOT NULL ); """ ) self._cursor.execute( """--sql - CREATE TRIGGER IF NOT EXISTS tg_workflows_updated_at + CREATE TRIGGER IF NOT EXISTS tg_workflow_library_updated_at AFTER UPDATE - ON workflows FOR EACH ROW + ON workflow_library FOR EACH ROW BEGIN - UPDATE workflows + UPDATE workflow_library SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') WHERE workflow_id = old.workflow_id; END; """ ) + self._cursor.execute( + """--sql + CREATE INDEX IF NOT EXISTS idx_workflow_library_created_at ON workflow_library(created_at); + """ + ) + self._cursor.execute( + """--sql + CREATE INDEX IF NOT EXISTS idx_workflow_library_updated_at ON workflow_library(updated_at); + """ + ) + self._cursor.execute( + """--sql + CREATE INDEX IF NOT EXISTS idx_workflow_library_opened_at ON workflow_library(opened_at); + """ + ) + self._cursor.execute( + """--sql + CREATE INDEX IF NOT EXISTS idx_workflow_library_category ON workflow_library(category); + """ + ) + self._cursor.execute( + """--sql + CREATE INDEX IF NOT EXISTS idx_workflow_library_name ON workflow_library(name); + """ + ) + self._cursor.execute( + """--sql + CREATE INDEX IF NOT EXISTS idx_workflow_library_description ON workflow_library(description); + """ + ) + + # We do not need the original `workflows` table or `workflow_images` junction table. + self._cursor.execute( + """--sql + DROP TABLE IF EXISTS workflow_images; + """ + ) + self._cursor.execute( + """--sql + DROP TABLE IF EXISTS workflows; + """ + ) + self._conn.commit() except Exception: self._conn.rollback() diff --git a/invokeai/backend/model_manager/migrate_to_db.py b/invokeai/backend/model_manager/migrate_to_db.py index e962a7c28b..eac25a44a0 100644 --- a/invokeai/backend/model_manager/migrate_to_db.py +++ b/invokeai/backend/model_manager/migrate_to_db.py @@ -11,7 +11,7 @@ from invokeai.app.services.model_records import ( DuplicateModelException, ModelRecordServiceSQL, ) -from invokeai.app.services.shared.sqlite import SqliteDatabase +from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase from invokeai.backend.model_manager.config import ( AnyModelConfig, BaseModelType, diff --git a/invokeai/frontend/web/.prettierignore b/invokeai/frontend/web/.prettierignore index 05782f1f53..6a981045b0 100644 --- a/invokeai/frontend/web/.prettierignore +++ b/invokeai/frontend/web/.prettierignore @@ -1,5 +1,6 @@ dist/ public/locales/*.json +!public/locales/en.json .husky/ node_modules/ patches/ diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 948f24093b..f45b9973ce 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -67,7 +67,9 @@ "controlNet": "ControlNet", "controlAdapter": "Control Adapter", "data": "Data", + "delete": "Delete", "details": "Details", + "direction": "Direction", "ipAdapter": "IP Adapter", "t2iAdapter": "T2I Adapter", "darkMode": "Dark Mode", @@ -115,6 +117,7 @@ "nodesDesc": "A node based system for the generation of images is under development currently. Stay tuned for updates about this amazing feature.", "notInstalled": "Not $t(common.installed)", "openInNewTab": "Open in New Tab", + "orderBy": "Order By", "outpaint": "outpaint", "outputs": "Outputs", "postProcessDesc1": "Invoke AI offers a wide variety of post processing features. Image Upscaling and Face Restoration are already available in the WebUI. You can access them from the Advanced Options menu of the Text To Image and Image To Image tabs. You can also process images directly, using the image action buttons above the current image display or in the viewer.", @@ -125,6 +128,8 @@ "random": "Random", "reportBugLabel": "Report Bug", "safetensors": "Safetensors", + "save": "Save", + "saveAs": "Save As", "settingsLabel": "Settings", "simple": "Simple", "somethingWentWrong": "Something went wrong", @@ -161,8 +166,12 @@ "txt2img": "Text To Image", "unifiedCanvas": "Unified Canvas", "unknown": "Unknown", - "unknownError": "Unknown Error", - "upload": "Upload" + "upload": "Upload", + "updated": "Updated", + "created": "Created", + "prevPage": "Previous Page", + "nextPage": "Next Page", + "unknownError": "Unknown Error" }, "controlnet": { "controlAdapter_one": "Control Adapter", @@ -940,9 +949,9 @@ "problemSettingTitle": "Problem Setting Title", "reloadNodeTemplates": "Reload Node Templates", "removeLinearView": "Remove from Linear View", - "resetWorkflow": "Reset Workflow", - "resetWorkflowDesc": "Are you sure you want to reset this workflow?", - "resetWorkflowDesc2": "Resetting the workflow will clear all nodes, edges and workflow details.", + "resetWorkflow": "Reset Workflow Editor", + "resetWorkflowDesc": "Are you sure you want to reset the Workflow Editor?", + "resetWorkflowDesc2": "Resetting the Workflow Editor will clear all nodes, edges and workflow details. Saved workflows will not be affected.", "scheduler": "Scheduler", "schedulerDescription": "TODO", "sDXLMainModelField": "SDXL Model", @@ -1269,7 +1278,6 @@ "modelAddedSimple": "Model Added", "modelAddFailed": "Model Add Failed", "nodesBrokenConnections": "Cannot load. Some connections are broken.", - "nodesCleared": "Nodes Cleared", "nodesCorruptedGraph": "Cannot load. Graph seems to be corrupted.", "nodesLoaded": "Nodes Loaded", "nodesLoadedFailed": "Failed To Load Nodes", @@ -1318,7 +1326,10 @@ "uploadFailedInvalidUploadDesc": "Must be single PNG or JPEG image", "uploadFailedUnableToLoadDesc": "Unable to load file", "upscalingFailed": "Upscaling Failed", - "workflowLoaded": "Workflow Loaded" + "workflowLoaded": "Workflow Loaded", + "problemRetrievingWorkflow": "Problem Retrieving Workflow", + "workflowDeleted": "Workflow Deleted", + "problemDeletingWorkflow": "Problem Deleting Workflow" }, "tooltip": { "feature": { @@ -1613,5 +1624,33 @@ "showIntermediates": "Show Intermediates", "snapToGrid": "Snap to Grid", "undo": "Undo" + }, + "workflows": { + "workflows": "Workflows", + "workflowLibrary": "Workflow Library", + "userWorkflows": "My Workflows", + "defaultWorkflows": "Default Workflows", + "openWorkflow": "Open Workflow", + "uploadWorkflow": "Upload Workflow", + "deleteWorkflow": "Delete Workflow", + "unnamedWorkflow": "Unnamed Workflow", + "downloadWorkflow": "Download Workflow", + "saveWorkflow": "Save Workflow", + "saveWorkflowAs": "Save Workflow As", + "problemSavingWorkflow": "Problem Saving Workflow", + "workflowSaved": "Workflow Saved", + "noRecentWorkflows": "No Recent Workflows", + "noUserWorkflows": "No User Workflows", + "noSystemWorkflows": "No System Workflows", + "problemLoading": "Problem Loading Workflows", + "loading": "Loading Workflows", + "noDescription": "No description", + "searchWorkflows": "Search Workflows", + "clearWorkflowSearchFilter": "Clear Workflow Search Filter", + "workflowName": "Workflow Name", + "workflowEditorReset": "Workflow Editor Reset" + }, + "app": { + "storeNotInitialized": "Store is not initialized" } } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts index b9b1060f18..dce4835418 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts @@ -3,6 +3,7 @@ import { buildNodesGraph } from 'features/nodes/util/graph/buildNodesGraph'; import { queueApi } from 'services/api/endpoints/queue'; import { BatchConfig } from 'services/api/types'; import { startAppListening } from '..'; +import { buildWorkflow } from 'features/nodes/util/workflow/buildWorkflow'; export const addEnqueueRequestedNodes = () => { startAppListening({ @@ -10,10 +11,18 @@ export const addEnqueueRequestedNodes = () => { enqueueRequested.match(action) && action.payload.tabName === 'nodes', effect: async (action, { getState, dispatch }) => { const state = getState(); + const { nodes, edges } = state.nodes; + const workflow = state.workflow; const graph = buildNodesGraph(state.nodes); + const builtWorkflow = buildWorkflow({ + nodes, + edges, + workflow, + }); const batchConfig: BatchConfig = { batch: { graph, + workflow: builtWorkflow, runs: state.generation.iterations, }, prepend: action.payload.prepend, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 584ec18f26..0ea5caf1d6 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -11,13 +11,11 @@ import { TypesafeDroppableData, } from 'features/dnd/types'; import { imageSelected } from 'features/gallery/store/gallerySlice'; -import { - fieldImageValueChanged, - workflowExposedFieldAdded, -} from 'features/nodes/store/nodesSlice'; +import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { initialImageChanged } from 'features/parameters/store/generationSlice'; import { imagesApi } from 'services/api/endpoints/images'; import { startAppListening } from '../'; +import { workflowExposedFieldAdded } from 'features/nodes/store/workflowSlice'; export const dndDropped = createAction<{ overData: TypesafeDroppableData; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts index bd677ca6f1..3dff9a906b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts @@ -1,7 +1,7 @@ import { logger } from 'app/logging/logger'; import { parseify } from 'common/util/serialize'; import { workflowLoadRequested } from 'features/nodes/store/actions'; -import { workflowLoaded } from 'features/nodes/store/nodesSlice'; +import { workflowLoaded } from 'features/nodes/store/actions'; import { $flow } from 'features/nodes/store/reactFlowInstance'; import { WorkflowMigrationError, @@ -21,7 +21,7 @@ export const addWorkflowLoadRequestedListener = () => { actionCreator: workflowLoadRequested, effect: (action, { dispatch, getState }) => { const log = logger('nodes'); - const workflow = action.payload; + const { workflow, asCopy } = action.payload; const nodeTemplates = getState().nodes.nodeTemplates; try { @@ -29,6 +29,12 @@ export const addWorkflowLoadRequestedListener = () => { workflow, nodeTemplates ); + + if (asCopy) { + // If we're loading a copy, we need to remove the ID so that the backend will create a new workflow + delete validatedWorkflow.id; + } + dispatch(workflowLoaded(validatedWorkflow)); if (!warnings.length) { dispatch( @@ -99,7 +105,6 @@ export const addWorkflowLoadRequestedListener = () => { ); } else { // Some other error occurred - console.log(e); log.error( { error: parseify(e) }, t('nodes.unknownErrorValidatingWorkflow') diff --git a/invokeai/frontend/web/src/app/store/nanostores/store.ts b/invokeai/frontend/web/src/app/store/nanostores/store.ts index c9f041fa5d..4e16245c6c 100644 --- a/invokeai/frontend/web/src/app/store/nanostores/store.ts +++ b/invokeai/frontend/web/src/app/store/nanostores/store.ts @@ -1,5 +1,6 @@ -import { Store } from '@reduxjs/toolkit'; +import { createStore } from 'app/store/store'; import { atom } from 'nanostores'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const $store = atom | undefined>(); +export const $store = atom< + Readonly> | undefined +>(); diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 0ea3829c66..3134d914e8 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -14,6 +14,7 @@ import galleryReducer from 'features/gallery/store/gallerySlice'; import loraReducer from 'features/lora/store/loraSlice'; import modelmanagerReducer from 'features/modelManager/store/modelManagerSlice'; import nodesReducer from 'features/nodes/store/nodesSlice'; +import workflowReducer from 'features/nodes/store/workflowSlice'; import generationReducer from 'features/parameters/store/generationSlice'; import postprocessingReducer from 'features/parameters/store/postprocessingSlice'; import queueReducer from 'features/queue/store/queueSlice'; @@ -22,9 +23,11 @@ import configReducer from 'features/system/store/configSlice'; import systemReducer from 'features/system/store/systemSlice'; import hotkeysReducer from 'features/ui/store/hotkeysSlice'; import uiReducer from 'features/ui/store/uiSlice'; +import { createStore as createIDBKeyValStore, get, set } from 'idb-keyval'; import dynamicMiddlewares from 'redux-dynamic-middlewares'; import { Driver, rememberEnhancer, rememberReducer } from 'redux-remember'; import { api } from 'services/api'; +import { authToastMiddleware } from 'services/api/authToastMiddleware'; import { STORAGE_PREFIX } from './constants'; import { serialize } from './enhancers/reduxRemember/serialize'; import { unserialize } from './enhancers/reduxRemember/unserialize'; @@ -32,8 +35,6 @@ import { actionSanitizer } from './middleware/devtools/actionSanitizer'; import { actionsDenylist } from './middleware/devtools/actionsDenylist'; import { stateSanitizer } from './middleware/devtools/stateSanitizer'; import { listenerMiddleware } from './middleware/listenerMiddleware'; -import { createStore as createIDBKeyValStore, get, set } from 'idb-keyval'; -import { authToastMiddleware } from 'services/api/authToastMiddleware'; const allReducers = { canvas: canvasReducer, @@ -53,6 +54,7 @@ const allReducers = { modelmanager: modelmanagerReducer, sdxl: sdxlReducer, queue: queueReducer, + workflow: workflowReducer, [api.reducerPath]: api.reducer, }; @@ -66,6 +68,7 @@ const rememberedKeys: (keyof typeof allReducers)[] = [ 'generation', 'sdxl', 'nodes', + 'workflow', 'postprocessing', 'system', 'ui', diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index 0fe7a36052..7e4cfb39aa 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -23,7 +23,8 @@ export type AppFeature = | 'resumeQueue' | 'prependQueue' | 'invocationCache' - | 'bulkDownload'; + | 'bulkDownload' + | 'workflowLibrary'; /** * A disable-able Stable Diffusion feature diff --git a/invokeai/frontend/web/src/common/components/IAIMantineSelect.tsx b/invokeai/frontend/web/src/common/components/IAIMantineSelect.tsx index 9541015b65..c79a85a0c4 100644 --- a/invokeai/frontend/web/src/common/components/IAIMantineSelect.tsx +++ b/invokeai/frontend/web/src/common/components/IAIMantineSelect.tsx @@ -1,4 +1,10 @@ -import { FormControl, FormLabel, Tooltip, forwardRef } from '@chakra-ui/react'; +import { + FormControl, + FormControlProps, + FormLabel, + Tooltip, + forwardRef, +} from '@chakra-ui/react'; import { Select, SelectProps } from '@mantine/core'; import { useMantineSelectStyles } from 'mantine-theme/hooks/useMantineSelectStyles'; import { RefObject, memo } from 'react'; @@ -13,10 +19,19 @@ export type IAISelectProps = Omit & { tooltip?: string | null; inputRef?: RefObject; label?: string; + formControlProps?: FormControlProps; }; const IAIMantineSelect = forwardRef((props: IAISelectProps, ref) => { - const { tooltip, inputRef, label, disabled, required, ...rest } = props; + const { + tooltip, + formControlProps, + inputRef, + label, + disabled, + required, + ...rest + } = props; const styles = useMantineSelectStyles(); @@ -28,6 +43,7 @@ const IAIMantineSelect = forwardRef((props: IAISelectProps, ref) => { isDisabled={disabled} position="static" data-testid={`select-${label || props.placeholder}`} + {...formControlProps} > {label} + + + + + {t('common.cancel')} + + {t('common.saveAs')} + + + + + + + ); +}; + +export default memo(SaveWorkflowAsButton); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowButton.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowButton.tsx new file mode 100644 index 0000000000..2665b9ff07 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowButton.tsx @@ -0,0 +1,21 @@ +import IAIIconButton from 'common/components/IAIIconButton'; +import { useSaveLibraryWorkflow } from 'features/workflowLibrary/hooks/useSaveWorkflow'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FaSave } from 'react-icons/fa'; + +const SaveLibraryWorkflowButton = () => { + const { t } = useTranslation(); + const { saveWorkflow, isLoading } = useSaveLibraryWorkflow(); + return ( + } + onClick={saveWorkflow} + isLoading={isLoading} + tooltip={t('workflows.saveWorkflow')} + aria-label={t('workflows.saveWorkflow')} + /> + ); +}; + +export default memo(SaveLibraryWorkflowButton); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx new file mode 100644 index 0000000000..ee0a8314a0 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx @@ -0,0 +1,26 @@ +import { useDisclosure } from '@chakra-ui/react'; +import IAIIconButton from 'common/components/IAIIconButton'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FaFolderOpen } from 'react-icons/fa'; +import WorkflowLibraryModal from './WorkflowLibraryModal'; +import { WorkflowLibraryModalContext } from 'features/workflowLibrary/context/WorkflowLibraryModalContext'; + +const WorkflowLibraryButton = () => { + const { t } = useTranslation(); + const disclosure = useDisclosure(); + + return ( + + } + onClick={disclosure.onOpen} + tooltip={t('workflows.workflowLibrary')} + aria-label={t('workflows.workflowLibrary')} + /> + + + ); +}; + +export default memo(WorkflowLibraryButton); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryContent.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryContent.tsx new file mode 100644 index 0000000000..cd3dccc464 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryContent.tsx @@ -0,0 +1,13 @@ +import WorkflowLibraryList from 'features/workflowLibrary/components/WorkflowLibraryList'; +import WorkflowLibraryListWrapper from 'features/workflowLibrary/components/WorkflowLibraryListWrapper'; +import { memo } from 'react'; + +const WorkflowLibraryContent = () => { + return ( + + + + ); +}; + +export default memo(WorkflowLibraryContent); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryList.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryList.tsx new file mode 100644 index 0000000000..02f642a099 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryList.tsx @@ -0,0 +1,242 @@ +import { CloseIcon } from '@chakra-ui/icons'; +import { + ButtonGroup, + Divider, + Flex, + IconButton, + Input, + InputGroup, + InputRightElement, + Spacer, +} from '@chakra-ui/react'; +import { SelectItem } from '@mantine/core'; +import IAIButton from 'common/components/IAIButton'; +import { + IAINoContentFallback, + IAINoContentFallbackWithSpinner, +} from 'common/components/IAIImageFallback'; +import IAIMantineSelect from 'common/components/IAIMantineSelect'; +import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent'; +import { WorkflowCategory } from 'features/nodes/types/workflow'; +import WorkflowLibraryListItem from 'features/workflowLibrary/components/WorkflowLibraryListItem'; +import WorkflowLibraryPagination from 'features/workflowLibrary/components/WorkflowLibraryPagination'; +import { + ChangeEvent, + KeyboardEvent, + memo, + useCallback, + useMemo, + useState, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import { useListWorkflowsQuery } from 'services/api/endpoints/workflows'; +import { SQLiteDirection, WorkflowRecordOrderBy } from 'services/api/types'; +import { useDebounce } from 'use-debounce'; + +const PER_PAGE = 10; + +const ORDER_BY_DATA: SelectItem[] = [ + { value: 'opened_at', label: 'Opened' }, + { value: 'created_at', label: 'Created' }, + { value: 'updated_at', label: 'Updated' }, + { value: 'name', label: 'Name' }, +]; + +const DIRECTION_DATA: SelectItem[] = [ + { value: 'ASC', label: 'Ascending' }, + { value: 'DESC', label: 'Descending' }, +]; + +const WorkflowLibraryList = () => { + const { t } = useTranslation(); + const [category, setCategory] = useState('user'); + const [page, setPage] = useState(0); + const [query, setQuery] = useState(''); + const [order_by, setOrderBy] = useState('opened_at'); + const [direction, setDirection] = useState('ASC'); + const [debouncedQuery] = useDebounce(query, 500); + + const queryArg = useMemo[0]>(() => { + if (category === 'user') { + return { + page, + per_page: PER_PAGE, + order_by, + direction, + category, + query: debouncedQuery, + }; + } + return { + page, + per_page: PER_PAGE, + order_by: 'name' as const, + direction: 'ASC' as const, + category, + query: debouncedQuery, + }; + }, [category, debouncedQuery, direction, order_by, page]); + + const { data, isLoading, isError, isFetching } = + useListWorkflowsQuery(queryArg); + + const handleChangeOrderBy = useCallback( + (value: string | null) => { + if (!value || value === order_by) { + return; + } + setOrderBy(value as WorkflowRecordOrderBy); + setPage(0); + }, + [order_by] + ); + + const handleChangeDirection = useCallback( + (value: string | null) => { + if (!value || value === direction) { + return; + } + setDirection(value as SQLiteDirection); + setPage(0); + }, + [direction] + ); + + const resetFilterText = useCallback(() => { + setQuery(''); + setPage(0); + }, []); + + const handleKeydownFilterText = useCallback( + (e: KeyboardEvent) => { + // exit search mode on escape + if (e.key === 'Escape') { + resetFilterText(); + e.preventDefault(); + setPage(0); + } + }, + [resetFilterText] + ); + + const handleChangeFilterText = useCallback( + (e: ChangeEvent) => { + setQuery(e.target.value); + setPage(0); + }, + [] + ); + + const handleSetUserCategory = useCallback(() => { + setCategory('user'); + setPage(0); + }, []); + + const handleSetDefaultCategory = useCallback(() => { + setCategory('default'); + setPage(0); + }, []); + + return ( + <> + + + + {t('workflows.userWorkflows')} + + + {t('workflows.defaultWorkflows')} + + + + {category === 'user' && ( + <> + + + + )} + + + {query.trim().length && ( + + } + /> + + )} + + + + {isLoading ? ( + + ) : !data || isError ? ( + + ) : data.items.length ? ( + + + {data.items.map((w) => ( + + ))} + + + ) : ( + + )} + + {data && ( + + + + )} + + ); +}; + +export default memo(WorkflowLibraryList); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryListItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryListItem.tsx new file mode 100644 index 0000000000..f8481ded83 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryListItem.tsx @@ -0,0 +1,94 @@ +import { Flex, Heading, Spacer, Text } from '@chakra-ui/react'; +import IAIButton from 'common/components/IAIButton'; +import dateFormat, { masks } from 'dateformat'; +import { useDeleteLibraryWorkflow } from 'features/workflowLibrary/hooks/useDeleteLibraryWorkflow'; +import { useGetAndLoadLibraryWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow'; +import { useWorkflowLibraryModalContext } from 'features/workflowLibrary/context/useWorkflowLibraryModalContext'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { WorkflowRecordListItemDTO } from 'services/api/types'; + +type Props = { + workflowDTO: WorkflowRecordListItemDTO; +}; + +const WorkflowLibraryListItem = ({ workflowDTO }: Props) => { + const { t } = useTranslation(); + const { onClose } = useWorkflowLibraryModalContext(); + const { deleteWorkflow, deleteWorkflowResult } = useDeleteLibraryWorkflow({}); + const { getAndLoadWorkflow, getAndLoadWorkflowResult } = + useGetAndLoadLibraryWorkflow({ onSuccess: onClose }); + + const handleDeleteWorkflow = useCallback(() => { + deleteWorkflow(workflowDTO.workflow_id); + }, [deleteWorkflow, workflowDTO.workflow_id]); + + const handleGetAndLoadWorkflow = useCallback(() => { + getAndLoadWorkflow(workflowDTO.workflow_id); + }, [getAndLoadWorkflow, workflowDTO.workflow_id]); + + return ( + + + + + + {workflowDTO.name || t('workflows.unnamedWorkflow')} + + + {workflowDTO.category === 'user' && ( + + {t('common.updated')}:{' '} + {dateFormat(workflowDTO.updated_at, masks.shortDate)}{' '} + {dateFormat(workflowDTO.updated_at, masks.shortTime)} + + )} + + + {workflowDTO.description ? ( + + {workflowDTO.description} + + ) : ( + + {t('workflows.noDescription')} + + )} + + {workflowDTO.category === 'user' && ( + + {t('common.created')}:{' '} + {dateFormat(workflowDTO.created_at, masks.shortDate)}{' '} + {dateFormat(workflowDTO.created_at, masks.shortTime)} + + )} + + + + {t('common.load')} + + {workflowDTO.category === 'user' && ( + + {t('common.delete')} + + )} + + + ); +}; + +export default memo(WorkflowLibraryListItem); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryListWrapper.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryListWrapper.tsx new file mode 100644 index 0000000000..7dfdac9c4c --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryListWrapper.tsx @@ -0,0 +1,21 @@ +import { Flex } from '@chakra-ui/react'; +import { PropsWithChildren, memo } from 'react'; + +const WorkflowLibraryListWrapper = (props: PropsWithChildren) => { + return ( + + {props.children} + + ); +}; + +export default memo(WorkflowLibraryListWrapper); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryModal.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryModal.tsx new file mode 100644 index 0000000000..cd0c905788 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryModal.tsx @@ -0,0 +1,40 @@ +import { + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, +} from '@chakra-ui/react'; +import WorkflowLibraryContent from 'features/workflowLibrary/components/WorkflowLibraryContent'; +import { useWorkflowLibraryModalContext } from 'features/workflowLibrary/context/useWorkflowLibraryModalContext'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +const WorkflowLibraryModal = () => { + const { t } = useTranslation(); + const { isOpen, onClose } = useWorkflowLibraryModalContext(); + return ( + + + + {t('workflows.workflowLibrary')} + + + + + + + + ); +}; + +export default memo(WorkflowLibraryModal); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryPagination.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryPagination.tsx new file mode 100644 index 0000000000..57ad04bb36 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryPagination.tsx @@ -0,0 +1,87 @@ +import { ButtonGroup } from '@chakra-ui/react'; +import IAIButton from 'common/components/IAIButton'; +import IAIIconButton from 'common/components/IAIIconButton'; +import { Dispatch, SetStateAction, memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FaChevronLeft, FaChevronRight } from 'react-icons/fa'; +import { paths } from 'services/api/schema'; + +const PAGES_TO_DISPLAY = 7; + +type PageData = { + page: number; + onClick: () => void; +}; + +type Props = { + page: number; + setPage: Dispatch>; + data: paths['/api/v1/workflows/']['get']['responses']['200']['content']['application/json']; +}; + +const WorkflowLibraryPagination = ({ page, setPage, data }: Props) => { + const { t } = useTranslation(); + + const handlePrevPage = useCallback(() => { + setPage((p) => Math.max(p - 1, 0)); + }, [setPage]); + + const handleNextPage = useCallback(() => { + setPage((p) => Math.min(p + 1, data.pages - 1)); + }, [data.pages, setPage]); + + const pages: PageData[] = useMemo(() => { + const pages = []; + let first = + data.pages > PAGES_TO_DISPLAY + ? Math.max(0, page - Math.floor(PAGES_TO_DISPLAY / 2)) + : 0; + const last = + data.pages > PAGES_TO_DISPLAY + ? Math.min(data.pages, first + PAGES_TO_DISPLAY) + : data.pages; + if (last - first < PAGES_TO_DISPLAY && data.pages > PAGES_TO_DISPLAY) { + first = last - PAGES_TO_DISPLAY; + } + for (let i = first; i < last; i++) { + pages.push({ + page: i, + onClick: () => setPage(i), + }); + } + return pages; + }, [data.pages, page, setPage]); + + return ( + + } + /> + {pages.map((p) => ( + + {p.page + 1} + + ))} + } + /> + + ); +}; + +export default memo(WorkflowLibraryPagination); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/context/WorkflowLibraryModalContext.ts b/invokeai/frontend/web/src/features/workflowLibrary/context/WorkflowLibraryModalContext.ts new file mode 100644 index 0000000000..88c1f565ee --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/context/WorkflowLibraryModalContext.ts @@ -0,0 +1,5 @@ +import { UseDisclosureReturn } from '@chakra-ui/react'; +import { createContext } from 'react'; + +export const WorkflowLibraryModalContext = + createContext(null); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/context/useWorkflowLibraryModalContext.ts b/invokeai/frontend/web/src/features/workflowLibrary/context/useWorkflowLibraryModalContext.ts new file mode 100644 index 0000000000..11b0771643 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/context/useWorkflowLibraryModalContext.ts @@ -0,0 +1,12 @@ +import { WorkflowLibraryModalContext } from 'features/workflowLibrary/context/WorkflowLibraryModalContext'; +import { useContext } from 'react'; + +export const useWorkflowLibraryModalContext = () => { + const context = useContext(WorkflowLibraryModalContext); + if (!context) { + throw new Error( + 'useWorkflowLibraryContext must be used within a WorkflowLibraryContext.Provider' + ); + } + return context; +}; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useDeleteLibraryWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useDeleteLibraryWorkflow.ts new file mode 100644 index 0000000000..bf518fe409 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useDeleteLibraryWorkflow.ts @@ -0,0 +1,48 @@ +import { useAppToaster } from 'app/components/Toaster'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDeleteWorkflowMutation } from 'services/api/endpoints/workflows'; + +type UseDeleteLibraryWorkflowOptions = { + onSuccess?: () => void; + onError?: () => void; +}; + +type UseDeleteLibraryWorkflowReturn = { + deleteWorkflow: (workflow_id: string) => Promise; + deleteWorkflowResult: ReturnType[1]; +}; + +type UseDeleteLibraryWorkflow = ( + arg: UseDeleteLibraryWorkflowOptions +) => UseDeleteLibraryWorkflowReturn; + +export const useDeleteLibraryWorkflow: UseDeleteLibraryWorkflow = ({ + onSuccess, + onError, +}) => { + const toaster = useAppToaster(); + const { t } = useTranslation(); + const [_deleteWorkflow, deleteWorkflowResult] = useDeleteWorkflowMutation(); + + const deleteWorkflow = useCallback( + async (workflow_id: string) => { + try { + await _deleteWorkflow(workflow_id).unwrap(); + toaster({ + title: t('toast.workflowDeleted'), + }); + onSuccess && onSuccess(); + } catch { + toaster({ + title: t('toast.problemDeletingWorkflow'), + status: 'error', + }); + onError && onError(); + } + }, + [_deleteWorkflow, toaster, t, onSuccess, onError] + ); + + return { deleteWorkflow, deleteWorkflowResult }; +}; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow.ts new file mode 100644 index 0000000000..a1292fce55 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow.ts @@ -0,0 +1,54 @@ +import { useAppToaster } from 'app/components/Toaster'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { workflowLoadRequested } from 'features/nodes/store/actions'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLazyGetImageWorkflowQuery } from 'services/api/endpoints/images'; + +type UseGetAndLoadEmbeddedWorkflowOptions = { + onSuccess?: () => void; + onError?: () => void; +}; + +type UseGetAndLoadEmbeddedWorkflowReturn = { + getAndLoadEmbeddedWorkflow: (imageName: string) => Promise; + getAndLoadEmbeddedWorkflowResult: ReturnType< + typeof useLazyGetImageWorkflowQuery + >[1]; +}; + +type UseGetAndLoadEmbeddedWorkflow = ( + options: UseGetAndLoadEmbeddedWorkflowOptions +) => UseGetAndLoadEmbeddedWorkflowReturn; + +export const useGetAndLoadEmbeddedWorkflow: UseGetAndLoadEmbeddedWorkflow = ({ + onSuccess, + onError, +}) => { + const dispatch = useAppDispatch(); + const toaster = useAppToaster(); + const { t } = useTranslation(); + const [_getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult] = + useLazyGetImageWorkflowQuery(); + const getAndLoadEmbeddedWorkflow = useCallback( + async (imageName: string) => { + try { + const workflow = await _getAndLoadEmbeddedWorkflow(imageName); + dispatch( + workflowLoadRequested({ workflow: workflow.data, asCopy: true }) + ); + // No toast - the listener for this action does that after the workflow is loaded + onSuccess && onSuccess(); + } catch { + toaster({ + title: t('toast.problemRetrievingWorkflow'), + status: 'error', + }); + onError && onError(); + } + }, + [_getAndLoadEmbeddedWorkflow, dispatch, onSuccess, toaster, t, onError] + ); + + return { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult }; +}; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow.ts new file mode 100644 index 0000000000..27de12789a --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow.ts @@ -0,0 +1,52 @@ +import { useAppToaster } from 'app/components/Toaster'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { workflowLoadRequested } from 'features/nodes/store/actions'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLazyGetWorkflowQuery } from 'services/api/endpoints/workflows'; + +type UseGetAndLoadLibraryWorkflowOptions = { + onSuccess?: () => void; + onError?: () => void; +}; + +type UseGetAndLoadLibraryWorkflowReturn = { + getAndLoadWorkflow: (workflow_id: string) => Promise; + getAndLoadWorkflowResult: ReturnType[1]; +}; + +type UseGetAndLoadLibraryWorkflow = ( + arg: UseGetAndLoadLibraryWorkflowOptions +) => UseGetAndLoadLibraryWorkflowReturn; + +export const useGetAndLoadLibraryWorkflow: UseGetAndLoadLibraryWorkflow = ({ + onSuccess, + onError, +}) => { + const dispatch = useAppDispatch(); + const toaster = useAppToaster(); + const { t } = useTranslation(); + const [_getAndLoadWorkflow, getAndLoadWorkflowResult] = + useLazyGetWorkflowQuery(); + const getAndLoadWorkflow = useCallback( + async (workflow_id: string) => { + try { + const data = await _getAndLoadWorkflow(workflow_id).unwrap(); + dispatch( + workflowLoadRequested({ workflow: data.workflow, asCopy: false }) + ); + // No toast - the listener for this action does that after the workflow is loaded + onSuccess && onSuccess(); + } catch { + toaster({ + title: t('toast.problemRetrievingWorkflow'), + status: 'error', + }); + onError && onError(); + } + }, + [_getAndLoadWorkflow, dispatch, onSuccess, toaster, t, onError] + ); + + return { getAndLoadWorkflow, getAndLoadWorkflowResult }; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useLoadWorkflowFromFile.tsx b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx similarity index 60% rename from invokeai/frontend/web/src/features/nodes/hooks/useLoadWorkflowFromFile.tsx rename to invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx index 03a7f5e824..43369d9b4f 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useLoadWorkflowFromFile.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx @@ -1,15 +1,22 @@ -import { ListItem, Text, UnorderedList } from '@chakra-ui/react'; import { useLogger } from 'app/logging/useLogger'; import { useAppDispatch } from 'app/store/storeHooks'; +import { workflowLoadRequested } from 'features/nodes/store/actions'; import { addToast } from 'features/system/store/systemSlice'; import { makeToast } from 'features/system/util/makeToast'; -import { RefObject, memo, useCallback } from 'react'; +import { RefObject, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { ZodError } from 'zod'; -import { fromZodIssue } from 'zod-validation-error'; -import { workflowLoadRequested } from 'features/nodes/store/actions'; -export const useLoadWorkflowFromFile = (resetRef: RefObject<() => void>) => { +type useLoadWorkflowFromFileOptions = { + resetRef: RefObject<() => void>; +}; + +type UseLoadWorkflowFromFile = ( + options: useLoadWorkflowFromFileOptions +) => (file: File | null) => void; + +export const useLoadWorkflowFromFile: UseLoadWorkflowFromFile = ({ + resetRef, +}) => { const dispatch = useAppDispatch(); const logger = useLogger('nodes'); const { t } = useTranslation(); @@ -24,7 +31,9 @@ export const useLoadWorkflowFromFile = (resetRef: RefObject<() => void>) => { try { const parsedJSON = JSON.parse(String(rawJSON)); - dispatch(workflowLoadRequested(parsedJSON)); + dispatch( + workflowLoadRequested({ workflow: parsedJSON, asCopy: true }) + ); } catch (e) { // There was a problem reading the file logger.error(t('nodes.unableToLoadWorkflow')); @@ -41,6 +50,7 @@ export const useLoadWorkflowFromFile = (resetRef: RefObject<() => void>) => { }; reader.readAsText(file); + // Reset the file picker internal state so that the same file can be loaded again resetRef.current?.(); }, @@ -49,24 +59,3 @@ export const useLoadWorkflowFromFile = (resetRef: RefObject<() => void>) => { return loadWorkflowFromFile; }; - -const WorkflowValidationErrorContent = memo((props: { error: ZodError }) => { - if (props.error.issues[0]) { - return ( - - {fromZodIssue(props.error.issues[0], { prefix: null }).toString()} - - ); - } - return ( - - {props.error.issues.map((issue, i) => ( - - {fromZodIssue(issue, { prefix: null }).toString()} - - ))} - - ); -}); - -WorkflowValidationErrorContent.displayName = 'WorkflowValidationErrorContent'; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts new file mode 100644 index 0000000000..a177706f0d --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts @@ -0,0 +1,61 @@ +import { useAppToaster } from 'app/components/Toaster'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { useWorkflow } from 'features/nodes/hooks/useWorkflow'; +import { workflowLoaded } from 'features/nodes/store/actions'; +import { zWorkflowV2 } from 'features/nodes/types/workflow'; +import { getWorkflowCopyName } from 'features/workflowLibrary/util/getWorkflowCopyName'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + useCreateWorkflowMutation, + useUpdateWorkflowMutation, +} from 'services/api/endpoints/workflows'; + +type UseSaveLibraryWorkflowReturn = { + saveWorkflow: () => Promise; + isLoading: boolean; + isError: boolean; +}; + +type UseSaveLibraryWorkflow = () => UseSaveLibraryWorkflowReturn; + +export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const workflow = useWorkflow(); + const [updateWorkflow, updateWorkflowResult] = useUpdateWorkflowMutation(); + const [createWorkflow, createWorkflowResult] = useCreateWorkflowMutation(); + const toaster = useAppToaster(); + const saveWorkflow = useCallback(async () => { + try { + if (workflow.id) { + const data = await updateWorkflow(workflow).unwrap(); + const updatedWorkflow = zWorkflowV2.parse(data.workflow); + dispatch(workflowLoaded(updatedWorkflow)); + toaster({ + title: t('workflows.workflowSaved'), + status: 'success', + }); + } else { + const data = await createWorkflow(workflow).unwrap(); + const createdWorkflow = zWorkflowV2.parse(data.workflow); + createdWorkflow.name = getWorkflowCopyName(createdWorkflow.name); + dispatch(workflowLoaded(createdWorkflow)); + toaster({ + title: t('workflows.workflowSaved'), + status: 'success', + }); + } + } catch (e) { + toaster({ + title: t('workflows.problemSavingWorkflow'), + status: 'error', + }); + } + }, [workflow, updateWorkflow, dispatch, toaster, t, createWorkflow]); + return { + saveWorkflow, + isLoading: updateWorkflowResult.isLoading || createWorkflowResult.isLoading, + isError: updateWorkflowResult.isError || createWorkflowResult.isError, + }; +}; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts new file mode 100644 index 0000000000..e0b08ed985 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts @@ -0,0 +1,58 @@ +import { useAppToaster } from 'app/components/Toaster'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { useWorkflow } from 'features/nodes/hooks/useWorkflow'; +import { workflowLoaded } from 'features/nodes/store/actions'; +import { zWorkflowV2 } from 'features/nodes/types/workflow'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useCreateWorkflowMutation } from 'services/api/endpoints/workflows'; + +type SaveWorkflowAsArg = { + name: string; + onSuccess?: () => void; + onError?: () => void; +}; + +type UseSaveWorkflowAsReturn = { + saveWorkflowAs: (arg: SaveWorkflowAsArg) => Promise; + isLoading: boolean; + isError: boolean; +}; + +type UseSaveWorkflowAs = () => UseSaveWorkflowAsReturn; + +export const useSaveWorkflowAs: UseSaveWorkflowAs = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const workflow = useWorkflow(); + const [createWorkflow, createWorkflowResult] = useCreateWorkflowMutation(); + const toaster = useAppToaster(); + const saveWorkflowAs = useCallback( + async ({ name: newName, onSuccess, onError }: SaveWorkflowAsArg) => { + try { + workflow.id = undefined; + workflow.name = newName; + const data = await createWorkflow(workflow).unwrap(); + const createdWorkflow = zWorkflowV2.parse(data.workflow); + dispatch(workflowLoaded(createdWorkflow)); + onSuccess && onSuccess(); + toaster({ + title: t('workflows.workflowSaved'), + status: 'success', + }); + } catch (e) { + onError && onError(); + toaster({ + title: t('workflows.problemSavingWorkflow'), + status: 'error', + }); + } + }, + [workflow, dispatch, toaster, t, createWorkflow] + ); + return { + saveWorkflowAs, + isLoading: createWorkflowResult.isLoading, + isError: createWorkflowResult.isError, + }; +}; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/util/getWorkflowCopyName.ts b/invokeai/frontend/web/src/features/workflowLibrary/util/getWorkflowCopyName.ts new file mode 100644 index 0000000000..b5d990bb49 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/util/getWorkflowCopyName.ts @@ -0,0 +1,2 @@ +export const getWorkflowCopyName = (name: string): string => + `${name.trim()} (copy)`; diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts index e261402837..f98bfd2c4a 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/images.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts @@ -9,7 +9,6 @@ import { } from 'features/gallery/store/types'; import { CoreMetadata, zCoreMetadata } from 'features/nodes/types/metadata'; import { keyBy } from 'lodash-es'; -import { ApiTagDescription, LIST_TAG, api } from '..'; import { components, paths } from 'services/api/schema'; import { DeleteBoardResult, @@ -26,6 +25,7 @@ import { imagesAdapter, imagesSelectors, } from 'services/api/util'; +import { ApiTagDescription, LIST_TAG, api } from '..'; import { boardsApi } from './boards'; import { addToast } from 'features/system/store/systemSlice'; import { t } from 'i18next'; @@ -130,6 +130,16 @@ export const imagesApi = api.injectEndpoints({ }, keepUnusedDataFor: 86400, // 24 hours }), + getImageWorkflow: build.query< + paths['/api/v1/images/i/{image_name}/workflow']['get']['responses']['200']['content']['application/json'], + string + >({ + query: (image_name) => ({ url: `images/i/${image_name}/workflow` }), + providesTags: (result, error, image_name) => [ + { type: 'ImageWorkflow', id: image_name }, + ], + keepUnusedDataFor: 86400, // 24 hours + }), deleteImage: build.mutation({ query: ({ image_name }) => ({ url: `images/i/${image_name}`, @@ -1572,6 +1582,8 @@ export const { useLazyListImagesQuery, useGetImageDTOQuery, useGetImageMetadataQuery, + useGetImageWorkflowQuery, + useLazyGetImageWorkflowQuery, useDeleteImageMutation, useDeleteImagesMutation, useUploadImageMutation, diff --git a/invokeai/frontend/web/src/services/api/endpoints/workflows.ts b/invokeai/frontend/web/src/services/api/endpoints/workflows.ts index e056d63119..53552b78b1 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/workflows.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/workflows.ts @@ -1,30 +1,87 @@ -import { logger } from 'app/logging/logger'; -import { WorkflowV2, zWorkflowV2 } from 'features/nodes/types/workflow'; -import { api } from '..'; import { paths } from 'services/api/schema'; +import { LIST_TAG, api } from '..'; export const workflowsApi = api.injectEndpoints({ endpoints: (build) => ({ - getWorkflow: build.query({ + getWorkflow: build.query< + paths['/api/v1/workflows/i/{workflow_id}']['get']['responses']['200']['content']['application/json'], + string + >({ query: (workflow_id) => `workflows/i/${workflow_id}`, providesTags: (result, error, workflow_id) => [ { type: 'Workflow', id: workflow_id }, ], - transformResponse: ( - response: paths['/api/v1/workflows/i/{workflow_id}']['get']['responses']['200']['content']['application/json'] - ) => { - if (response) { - const result = zWorkflowV2.safeParse(response); - if (result.success) { - return result.data; - } else { - logger('images').warn('Problem parsing workflow'); - } + onQueryStarted: async (arg, api) => { + const { dispatch, queryFulfilled } = api; + try { + await queryFulfilled; + dispatch( + workflowsApi.util.invalidateTags([ + { type: 'WorkflowsRecent', id: LIST_TAG }, + ]) + ); + } catch { + // no-op } - return; }, }), + deleteWorkflow: build.mutation({ + query: (workflow_id) => ({ + url: `workflows/i/${workflow_id}`, + method: 'DELETE', + }), + invalidatesTags: (result, error, workflow_id) => [ + { type: 'Workflow', id: LIST_TAG }, + { type: 'Workflow', id: workflow_id }, + { type: 'WorkflowsRecent', id: LIST_TAG }, + ], + }), + createWorkflow: build.mutation< + paths['/api/v1/workflows/']['post']['responses']['200']['content']['application/json'], + paths['/api/v1/workflows/']['post']['requestBody']['content']['application/json']['workflow'] + >({ + query: (workflow) => ({ + url: 'workflows/', + method: 'POST', + body: { workflow }, + }), + invalidatesTags: [ + { type: 'Workflow', id: LIST_TAG }, + { type: 'WorkflowsRecent', id: LIST_TAG }, + ], + }), + updateWorkflow: build.mutation< + paths['/api/v1/workflows/i/{workflow_id}']['patch']['responses']['200']['content']['application/json'], + paths['/api/v1/workflows/i/{workflow_id}']['patch']['requestBody']['content']['application/json']['workflow'] + >({ + query: (workflow) => ({ + url: `workflows/i/${workflow.id}`, + method: 'PATCH', + body: { workflow }, + }), + invalidatesTags: (response, error, workflow) => [ + { type: 'WorkflowsRecent', id: LIST_TAG }, + { type: 'Workflow', id: LIST_TAG }, + { type: 'Workflow', id: workflow.id }, + ], + }), + listWorkflows: build.query< + paths['/api/v1/workflows/']['get']['responses']['200']['content']['application/json'], + NonNullable + >({ + query: (params) => ({ + url: 'workflows/', + params, + }), + providesTags: [{ type: 'Workflow', id: LIST_TAG }], + }), }), }); -export const { useGetWorkflowQuery } = workflowsApi; +export const { + useLazyGetWorkflowQuery, + useCreateWorkflowMutation, + useDeleteWorkflowMutation, + useUpdateWorkflowMutation, + useListWorkflowsQuery, +} = workflowsApi; diff --git a/invokeai/frontend/web/src/services/api/hooks/useDebouncedWorkflow.ts b/invokeai/frontend/web/src/services/api/hooks/useDebouncedWorkflow.ts deleted file mode 100644 index 20c6d2d6c6..0000000000 --- a/invokeai/frontend/web/src/services/api/hooks/useDebouncedWorkflow.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { skipToken } from '@reduxjs/toolkit/query'; -import { useAppSelector } from 'app/store/storeHooks'; -import { useDebounce } from 'use-debounce'; -import { useGetWorkflowQuery } from 'services/api/endpoints/workflows'; - -export const useDebouncedWorkflow = (workflowId?: string | null) => { - const workflowFetchDebounce = useAppSelector( - (state) => state.config.workflowFetchDebounce - ); - - const [debouncedWorkflowID] = useDebounce( - workflowId, - workflowFetchDebounce ?? 0 - ); - - const { data: workflow, isLoading } = useGetWorkflowQuery( - debouncedWorkflowID ?? skipToken - ); - - return { workflow, isLoading }; -}; diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index f824afed1f..83d5232963 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -20,6 +20,7 @@ export const tagTypes = [ 'ImageNameList', 'ImageList', 'ImageMetadata', + 'ImageWorkflow', 'ImageMetadataFromFile', 'IntermediatesCount', 'SessionQueueItem', @@ -40,6 +41,7 @@ export const tagTypes = [ 'LoRAModel', 'SDXLRefinerModel', 'Workflow', + 'WorkflowsRecent', ] as const; export type ApiTagDescription = TagDescription<(typeof tagTypes)[number]>; export const LIST_TAG = 'LIST'; diff --git a/invokeai/frontend/web/src/services/api/schema.d.ts b/invokeai/frontend/web/src/services/api/schema.d.ts index b6b6a0e8be..a0af5e7d1e 100644 --- a/invokeai/frontend/web/src/services/api/schema.d.ts +++ b/invokeai/frontend/web/src/services/api/schema.d.ts @@ -159,6 +159,10 @@ export type paths = { */ get: operations["get_image_metadata"]; }; + "/api/v1/images/i/{image_name}/workflow": { + /** Get Image Workflow */ + get: operations["get_image_workflow"]; + }; "/api/v1/images/i/{image_name}/full": { /** * Get Image Full @@ -415,6 +419,28 @@ export type paths = { * @description Gets a workflow */ get: operations["get_workflow"]; + /** + * Delete Workflow + * @description Deletes a workflow + */ + delete: operations["delete_workflow"]; + /** + * Update Workflow + * @description Updates a workflow + */ + patch: operations["update_workflow"]; + }; + "/api/v1/workflows/": { + /** + * List Workflows + * @description Gets a page of workflows + */ + get: operations["list_workflows"]; + /** + * Create Workflow + * @description Creates a workflow + */ + post: operations["create_workflow"]; }; }; @@ -527,6 +553,8 @@ export type components = { data?: components["schemas"]["BatchDatum"][][] | null; /** @description The graph to initialize the session with */ graph: components["schemas"]["Graph"]; + /** @description The workflow to initialize the session with */ + workflow?: components["schemas"]["WorkflowWithoutID"] | null; /** * Runs * @description Int stating how many times to iterate through all possible batch indices @@ -600,8 +628,6 @@ export type components = { * @description Creates a blank image and forwards it to the pipeline */ BlankImageInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -795,6 +821,11 @@ export type components = { */ batch_ids: string[]; }; + /** Body_create_workflow */ + Body_create_workflow: { + /** @description The workflow to create */ + workflow: components["schemas"]["WorkflowWithoutID"]; + }; /** Body_delete_images_from_list */ Body_delete_images_from_list: { /** @@ -897,6 +928,11 @@ export type components = { */ image_names: string[]; }; + /** Body_update_workflow */ + Body_update_workflow: { + /** @description The updated workflow */ + workflow: components["schemas"]["Workflow"]; + }; /** Body_upload_image */ Body_upload_image: { /** @@ -1110,8 +1146,6 @@ export type components = { CV2InfillInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -1138,6 +1172,79 @@ export type components = { */ type: "infill_cv2"; }; + /** + * Calculate Image Tiles + * @description Calculate the coordinates and overlaps of tiles that cover a target image shape. + */ + CalculateImageTilesInvocation: { + /** + * Id + * @description The id of this instance of an invocation. Must be unique among all instances of invocations. + */ + id: string; + /** + * Is Intermediate + * @description Whether or not this is an intermediate invocation. + * @default false + */ + is_intermediate?: boolean; + /** + * Use Cache + * @description Whether or not to use the cache + * @default true + */ + use_cache?: boolean; + /** + * Image Width + * @description The image width, in pixels, to calculate tiles for. + * @default 1024 + */ + image_width?: number; + /** + * Image Height + * @description The image height, in pixels, to calculate tiles for. + * @default 1024 + */ + image_height?: number; + /** + * Tile Width + * @description The tile width, in pixels. + * @default 576 + */ + tile_width?: number; + /** + * Tile Height + * @description The tile height, in pixels. + * @default 576 + */ + tile_height?: number; + /** + * Overlap + * @description The target overlap, in pixels, between adjacent tiles. Adjacent tiles will overlap by at least this amount + * @default 128 + */ + overlap?: number; + /** + * type + * @default calculate_image_tiles + * @constant + */ + type: "calculate_image_tiles"; + }; + /** CalculateImageTilesOutput */ + CalculateImageTilesOutput: { + /** + * Tiles + * @description The tiles coordinates that cover a particular image shape. + */ + tiles: components["schemas"]["Tile"][]; + /** + * type + * @default calculate_image_tiles_output + * @constant + */ + type: "calculate_image_tiles_output"; + }; /** * CancelByBatchIDsResult * @description Result of canceling by list of batch ids @@ -1154,8 +1261,6 @@ export type components = { * @description Canny edge detection for ControlNet */ CannyImageProcessorInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -1197,7 +1302,7 @@ export type components = { type: "canny_image_processor"; }; /** - * Center Pad or Crop Image + * Paste Image * @description Pad or crop an image's sides from the center by specified pixels. Positive values are outside of the image. */ CenterPadCropInvocation: { @@ -1246,10 +1351,10 @@ export type components = { bottom?: number; /** * type - * @default img_pad_crop + * @default img_paste * @constant */ - type: "img_pad_crop"; + type: "img_paste"; }; /** * ClearResult @@ -1415,8 +1520,6 @@ export type components = { ColorCorrectInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -1523,8 +1626,6 @@ export type components = { * @description Generates a color map from the provided image */ ColorMapImageProcessorInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -1726,8 +1827,6 @@ export type components = { * @description Applies content shuffle processing to image */ ContentShuffleImageProcessorInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -2298,6 +2397,58 @@ export type components = { */ type: "create_denoise_mask"; }; + /** + * Crop Latents + * @description Crops a latent-space tensor to a box specified in image-space. The box dimensions and coordinates must be + * divisible by the latent scale factor of 8. + */ + CropLatentsCoreInvocation: { + /** + * Id + * @description The id of this instance of an invocation. Must be unique among all instances of invocations. + */ + id: string; + /** + * Is Intermediate + * @description Whether or not this is an intermediate invocation. + * @default false + */ + is_intermediate?: boolean; + /** + * Use Cache + * @description Whether or not to use the cache + * @default true + */ + use_cache?: boolean; + /** @description Latents tensor */ + latents?: components["schemas"]["LatentsField"]; + /** + * X + * @description The left x coordinate (in px) of the crop rectangle in image space. This value will be converted to a dimension in latent space. + */ + x?: number; + /** + * Y + * @description The top y coordinate (in px) of the crop rectangle in image space. This value will be converted to a dimension in latent space. + */ + y?: number; + /** + * Width + * @description The width (in px) of the crop rectangle in image space. This value will be converted to a dimension in latent space. + */ + width?: number; + /** + * Height + * @description The height (in px) of the crop rectangle in image space. This value will be converted to a dimension in latent space. + */ + height?: number; + /** + * type + * @default crop_latents + * @constant + */ + type: "crop_latents"; + }; /** CursorPaginatedResults[SessionQueueItemDTO] */ CursorPaginatedResults_SessionQueueItemDTO_: { /** @@ -2321,8 +2472,6 @@ export type components = { * @description Simple inpaint using opencv. */ CvInpaintInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -2600,8 +2749,6 @@ export type components = { ESRGANInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -2686,6 +2833,13 @@ export type components = { */ priority: number; }; + /** ExposedField */ + ExposedField: { + /** Nodeid */ + nodeId: string; + /** Fieldname */ + fieldName: string; + }; /** * FaceIdentifier * @description Outputs an image with detected face IDs printed on each face. For use with other FaceTools. @@ -2693,8 +2847,6 @@ export type components = { FaceIdentifierInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -2740,8 +2892,6 @@ export type components = { FaceMaskInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -2837,8 +2987,6 @@ export type components = { FaceOffInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -3286,7 +3434,7 @@ export type components = { * @description The nodes in this graph */ nodes?: { - [key: string]: components["schemas"]["BlankImageInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["LinearUIOutputInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["ONNXLatentsToImageInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["ONNXTextToLatentsInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["OnnxModelLoaderInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ONNXPromptInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["LineartImageProcessorInvocation"]; + [key: string]: components["schemas"]["SaveImageInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["OnnxModelLoaderInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ONNXLatentsToImageInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ONNXTextToLatentsInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["LinearUIOutputInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ONNXPromptInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["CannyImageProcessorInvocation"]; }; /** * Edges @@ -3323,7 +3471,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["ONNXModelLoaderOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["String2Output"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["T2IAdapterOutput"]; + [key: string]: components["schemas"]["IntegerOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["String2Output"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["ONNXModelLoaderOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["CalculateImageTilesOutput"]; }; /** * Errors @@ -3397,8 +3545,6 @@ export type components = { * @description Applies HED edge detection to image */ HedImageProcessorInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -3655,8 +3801,6 @@ export type components = { ImageBlurInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -3715,8 +3859,6 @@ export type components = { ImageChannelInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -3757,8 +3899,6 @@ export type components = { ImageChannelMultiplyInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -3810,8 +3950,6 @@ export type components = { ImageChannelOffsetInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -3908,8 +4046,6 @@ export type components = { ImageConvertInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -3950,8 +4086,6 @@ export type components = { ImageCropInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -4071,16 +4205,16 @@ export type components = { * @description Whether this image is starred. */ starred: boolean; + /** + * Has Workflow + * @description Whether this image has a workflow. + */ + has_workflow: boolean; /** * Board Id * @description The id of the board the image belongs to, if one exists. */ board_id?: string | null; - /** - * Workflow Id - * @description The workflow that generated this image. - */ - workflow_id?: string | null; }; /** * ImageField @@ -4100,8 +4234,6 @@ export type components = { ImageHueAdjustmentInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -4141,8 +4273,6 @@ export type components = { ImageInverseLerpInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -4219,8 +4349,6 @@ export type components = { ImageLerpInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -4266,8 +4394,6 @@ export type components = { ImageMultiplyInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -4301,8 +4427,6 @@ export type components = { * @description Add blur to NSFW-flagged images */ ImageNSFWBlurInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -4356,14 +4480,12 @@ export type components = { type: "image_output"; }; /** - * Paste Image + * Center Pad or Crop Image * @description Pastes an image into another image. */ ImagePasteInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -4407,10 +4529,10 @@ export type components = { crop?: boolean; /** * type - * @default img_paste + * @default img_pad_crop * @constant */ - type: "img_paste"; + type: "img_pad_crop"; }; /** * ImageRecordChanges @@ -4447,8 +4569,6 @@ export type components = { * @description Resizes an image to specific dimensions */ ImageResizeInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -4501,8 +4621,6 @@ export type components = { * @description Scales an image by a factor */ ImageScaleInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -4615,8 +4733,6 @@ export type components = { * @description Add an invisible watermark to an image */ ImageWatermarkInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -4674,8 +4790,6 @@ export type components = { InfillColorInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -4719,8 +4833,6 @@ export type components = { InfillPatchMatchInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -4767,8 +4879,6 @@ export type components = { InfillTileInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -5045,6 +5155,7 @@ export type components = { */ type: "iterate_output"; }; + JsonValue: unknown; /** * LaMa Infill * @description Infills transparent areas of an image using the LaMa model @@ -5052,8 +5163,6 @@ export type components = { LaMaInfillInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -5207,8 +5316,6 @@ export type components = { * @description Generates an image from latents. */ LatentsToImageInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -5256,8 +5363,6 @@ export type components = { * @description Applies leres processing to image */ LeresImageProcessorInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -5323,8 +5428,6 @@ export type components = { LinearUIOutputInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -5358,8 +5461,6 @@ export type components = { * @description Applies line art anime processing to image */ LineartAnimeImageProcessorInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -5405,8 +5506,6 @@ export type components = { * @description Applies line art processing to image */ LineartImageProcessorInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -5818,8 +5917,6 @@ export type components = { MaskCombineInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -5855,8 +5952,6 @@ export type components = { MaskEdgeInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -5910,8 +6005,6 @@ export type components = { MaskFromAlphaInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -5949,8 +6042,6 @@ export type components = { * @description Applies mediapipe face processing to image */ MediapipeFaceProcessorInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -6062,6 +6153,47 @@ export type components = { */ merge_dest_directory?: string | null; }; + /** + * Merge Tiles to Image + * @description Merge multiple tile images into a single image. + */ + MergeTilesToImageInvocation: { + /** @description Optional metadata to be saved with the image */ + metadata?: components["schemas"]["MetadataField"] | null; + /** + * Id + * @description The id of this instance of an invocation. Must be unique among all instances of invocations. + */ + id: string; + /** + * Is Intermediate + * @description Whether or not this is an intermediate invocation. + * @default false + */ + is_intermediate?: boolean; + /** + * Use Cache + * @description Whether or not to use the cache + * @default true + */ + use_cache?: boolean; + /** + * Tiles With Images + * @description A list of tile images with tile properties. + */ + tiles_with_images?: components["schemas"]["TileWithImage"][]; + /** + * Blend Amount + * @description The amount to blend adjacent tiles in pixels. Must be <= the amount of overlap between adjacent tiles. + */ + blend_amount?: number; + /** + * type + * @default merge_tiles_to_image + * @constant + */ + type: "merge_tiles_to_image"; + }; /** * MetadataField * @description Pydantic model for metadata with custom root of type dict[str, Any]. @@ -6184,8 +6316,6 @@ export type components = { * @description Applies Midas depth processing to image */ MidasDepthImageProcessorInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -6231,8 +6361,6 @@ export type components = { * @description Applies MLSD processing to image */ MlsdImageProcessorInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -6472,8 +6600,6 @@ export type components = { * @description Applies NormalBae processing to image */ NormalbaeImageProcessorInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -6519,8 +6645,6 @@ export type components = { * @description Generates an image from latents. */ ONNXLatentsToImageInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -6963,8 +7087,6 @@ export type components = { * @description Applies Openpose processing to image */ OpenposeImageProcessorInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -7011,13 +7133,83 @@ export type components = { */ type: "openpose_image_processor"; }; + /** PaginatedResults[WorkflowRecordListItemDTO] */ + PaginatedResults_WorkflowRecordListItemDTO_: { + /** + * Page + * @description Current Page + */ + page: number; + /** + * Pages + * @description Total number of pages + */ + pages: number; + /** + * Per Page + * @description Number of items per page + */ + per_page: number; + /** + * Total + * @description Total number of items in result + */ + total: number; + /** + * Items + * @description Items + */ + items: components["schemas"]["WorkflowRecordListItemDTO"][]; + }; + /** + * Pair Tile with Image + * @description Pair an image with its tile properties. + */ + PairTileImageInvocation: { + /** + * Id + * @description The id of this instance of an invocation. Must be unique among all instances of invocations. + */ + id: string; + /** + * Is Intermediate + * @description Whether or not this is an intermediate invocation. + * @default false + */ + is_intermediate?: boolean; + /** + * Use Cache + * @description Whether or not to use the cache + * @default true + */ + use_cache?: boolean; + /** @description The tile image. */ + image?: components["schemas"]["ImageField"]; + /** @description The tile properties. */ + tile?: components["schemas"]["Tile"]; + /** + * type + * @default pair_tile_image + * @constant + */ + type: "pair_tile_image"; + }; + /** PairTileImageOutput */ + PairTileImageOutput: { + /** @description A tile description with its corresponding image. */ + tile_with_image: components["schemas"]["TileWithImage"]; + /** + * type + * @default pair_tile_image_output + * @constant + */ + type: "pair_tile_image_output"; + }; /** * PIDI Processor * @description Applies PIDI processing to image */ PidiImageProcessorInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -7832,6 +8024,11 @@ export type components = { */ type: "sdxl_refiner_model_loader_output"; }; + /** + * SQLiteDirection + * @enum {string} + */ + SQLiteDirection: "ASC" | "DESC"; /** * Save Image * @description Saves an image. Unlike an image primitive, this invocation stores a copy of the image. @@ -7839,8 +8036,6 @@ export type components = { SaveImageInvocation: { /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -8047,8 +8242,6 @@ export type components = { * @description Applies segment anything processing to image */ SegmentAnythingProcessorInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -8165,6 +8358,8 @@ export type components = { field_values?: components["schemas"]["NodeFieldValue"][] | null; /** @description The fully-populated session to be executed */ session: components["schemas"]["GraphExecutionState"]; + /** @description The workflow associated with this queue item */ + workflow?: components["schemas"]["WorkflowWithoutID"] | null; }; /** SessionQueueItemDTO */ SessionQueueItemDTO: { @@ -9155,6 +9350,17 @@ export type components = { */ source?: string | null; }; + /** TBLR */ + TBLR: { + /** Top */ + top: number; + /** Bottom */ + bottom: number; + /** Left */ + left: number; + /** Right */ + right: number; + }; /** * TextualInversionConfig * @description Model config for textual inversion embeddings. @@ -9219,13 +9425,18 @@ export type components = { model_format: null; error?: components["schemas"]["ModelError"] | null; }; + /** Tile */ + Tile: { + /** @description The coordinates of this tile relative to its parent image. */ + coords: components["schemas"]["TBLR"]; + /** @description The amount of overlap with adjacent tiles on each side of this tile. */ + overlap: components["schemas"]["TBLR"]; + }; /** * Tile Resample Processor * @description Tile resampler processor */ TileResamplerProcessorInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -9260,6 +9471,101 @@ export type components = { */ type: "tile_image_processor"; }; + /** + * Tile to Properties + * @description Split a Tile into its individual properties. + */ + TileToPropertiesInvocation: { + /** + * Id + * @description The id of this instance of an invocation. Must be unique among all instances of invocations. + */ + id: string; + /** + * Is Intermediate + * @description Whether or not this is an intermediate invocation. + * @default false + */ + is_intermediate?: boolean; + /** + * Use Cache + * @description Whether or not to use the cache + * @default true + */ + use_cache?: boolean; + /** @description The tile to split into properties. */ + tile?: components["schemas"]["Tile"]; + /** + * type + * @default tile_to_properties + * @constant + */ + type: "tile_to_properties"; + }; + /** TileToPropertiesOutput */ + TileToPropertiesOutput: { + /** + * Coords Left + * @description Left coordinate of the tile relative to its parent image. + */ + coords_left: number; + /** + * Coords Right + * @description Right coordinate of the tile relative to its parent image. + */ + coords_right: number; + /** + * Coords Top + * @description Top coordinate of the tile relative to its parent image. + */ + coords_top: number; + /** + * Coords Bottom + * @description Bottom coordinate of the tile relative to its parent image. + */ + coords_bottom: number; + /** + * Width + * @description The width of the tile. Equal to coords_right - coords_left. + */ + width: number; + /** + * Height + * @description The height of the tile. Equal to coords_bottom - coords_top. + */ + height: number; + /** + * Overlap Top + * @description Overlap between this tile and its top neighbor. + */ + overlap_top: number; + /** + * Overlap Bottom + * @description Overlap between this tile and its bottom neighbor. + */ + overlap_bottom: number; + /** + * Overlap Left + * @description Overlap between this tile and its left neighbor. + */ + overlap_left: number; + /** + * Overlap Right + * @description Overlap between this tile and its right neighbor. + */ + overlap_right: number; + /** + * type + * @default tile_to_properties_output + * @constant + */ + type: "tile_to_properties_output"; + }; + /** TileWithImage */ + TileWithImage: { + tile: components["schemas"]["Tile"]; + image: components["schemas"]["ImageField"]; + }; /** UNetField */ UNetField: { /** @description Info to load unet submodel */ @@ -9507,19 +9813,220 @@ export type components = { /** Error Type */ type: string; }; + /** Workflow */ + Workflow: { + /** + * Name + * @description The name of the workflow. + */ + name: string; + /** + * Author + * @description The author of the workflow. + */ + author: string; + /** + * Description + * @description The description of the workflow. + */ + description: string; + /** + * Version + * @description The version of the workflow. + */ + version: string; + /** + * Contact + * @description The contact of the workflow. + */ + contact: string; + /** + * Tags + * @description The tags of the workflow. + */ + tags: string; + /** + * Notes + * @description The notes of the workflow. + */ + notes: string; + /** + * Exposedfields + * @description The exposed fields of the workflow. + */ + exposedFields: components["schemas"]["ExposedField"][]; + /** @description The meta of the workflow. */ + meta: components["schemas"]["WorkflowMeta"]; + /** + * Nodes + * @description The nodes of the workflow. + */ + nodes: { + [key: string]: components["schemas"]["JsonValue"]; + }[]; + /** + * Edges + * @description The edges of the workflow. + */ + edges: { + [key: string]: components["schemas"]["JsonValue"]; + }[]; + /** + * Id + * @description The id of the workflow. + */ + id?: string; + }; /** - * WorkflowField - * @description Pydantic model for workflows with custom root of type dict[str, Any]. - * Workflows are stored without a strict schema. + * WorkflowCategory + * @enum {string} */ - WorkflowField: Record; + WorkflowCategory: "user" | "default"; + /** WorkflowMeta */ + WorkflowMeta: { + /** + * Version + * @description The version of the workflow schema. + */ + version: string; + /** @description The category of the workflow (user or default). */ + category: components["schemas"]["WorkflowCategory"]; + }; + /** WorkflowRecordDTO */ + WorkflowRecordDTO: { + /** + * Workflow Id + * @description The id of the workflow. + */ + workflow_id: string; + /** + * Name + * @description The name of the workflow. + */ + name: string; + /** + * Created At + * @description The created timestamp of the workflow. + */ + created_at: string; + /** + * Updated At + * @description The updated timestamp of the workflow. + */ + updated_at: string; + /** + * Opened At + * @description The opened timestamp of the workflow. + */ + opened_at: string; + /** @description The workflow. */ + workflow: components["schemas"]["Workflow"]; + }; + /** WorkflowRecordListItemDTO */ + WorkflowRecordListItemDTO: { + /** + * Workflow Id + * @description The id of the workflow. + */ + workflow_id: string; + /** + * Name + * @description The name of the workflow. + */ + name: string; + /** + * Created At + * @description The created timestamp of the workflow. + */ + created_at: string; + /** + * Updated At + * @description The updated timestamp of the workflow. + */ + updated_at: string; + /** + * Opened At + * @description The opened timestamp of the workflow. + */ + opened_at: string; + /** + * Description + * @description The description of the workflow. + */ + description: string; + /** @description The description of the workflow. */ + category: components["schemas"]["WorkflowCategory"]; + }; + /** + * WorkflowRecordOrderBy + * @description The order by options for workflow records + * @enum {string} + */ + WorkflowRecordOrderBy: "created_at" | "updated_at" | "opened_at" | "name"; + /** WorkflowWithoutID */ + WorkflowWithoutID: { + /** + * Name + * @description The name of the workflow. + */ + name: string; + /** + * Author + * @description The author of the workflow. + */ + author: string; + /** + * Description + * @description The description of the workflow. + */ + description: string; + /** + * Version + * @description The version of the workflow. + */ + version: string; + /** + * Contact + * @description The contact of the workflow. + */ + contact: string; + /** + * Tags + * @description The tags of the workflow. + */ + tags: string; + /** + * Notes + * @description The notes of the workflow. + */ + notes: string; + /** + * Exposedfields + * @description The exposed fields of the workflow. + */ + exposedFields: components["schemas"]["ExposedField"][]; + /** @description The meta of the workflow. */ + meta: components["schemas"]["WorkflowMeta"]; + /** + * Nodes + * @description The nodes of the workflow. + */ + nodes: { + [key: string]: components["schemas"]["JsonValue"]; + }[]; + /** + * Edges + * @description The edges of the workflow. + */ + edges: { + [key: string]: components["schemas"]["JsonValue"]; + }[]; + }; /** * Zoe (Depth) Processor * @description Applies Zoe depth processing to image */ ZoeDepthImageProcessorInvocation: { - /** @description Optional workflow to be saved with the image */ - workflow?: components["schemas"]["WorkflowField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -9757,54 +10264,54 @@ export type components = { * @enum {string} */ UIType: "SDXLMainModelField" | "SDXLRefinerModelField" | "ONNXModelField" | "VAEModelField" | "LoRAModelField" | "ControlNetModelField" | "IPAdapterModelField" | "SchedulerField" | "AnyField" | "CollectionField" | "CollectionItemField" | "DEPRECATED_Boolean" | "DEPRECATED_Color" | "DEPRECATED_Conditioning" | "DEPRECATED_Control" | "DEPRECATED_Float" | "DEPRECATED_Image" | "DEPRECATED_Integer" | "DEPRECATED_Latents" | "DEPRECATED_String" | "DEPRECATED_BooleanCollection" | "DEPRECATED_ColorCollection" | "DEPRECATED_ConditioningCollection" | "DEPRECATED_ControlCollection" | "DEPRECATED_FloatCollection" | "DEPRECATED_ImageCollection" | "DEPRECATED_IntegerCollection" | "DEPRECATED_LatentsCollection" | "DEPRECATED_StringCollection" | "DEPRECATED_BooleanPolymorphic" | "DEPRECATED_ColorPolymorphic" | "DEPRECATED_ConditioningPolymorphic" | "DEPRECATED_ControlPolymorphic" | "DEPRECATED_FloatPolymorphic" | "DEPRECATED_ImagePolymorphic" | "DEPRECATED_IntegerPolymorphic" | "DEPRECATED_LatentsPolymorphic" | "DEPRECATED_StringPolymorphic" | "DEPRECATED_MainModel" | "DEPRECATED_UNet" | "DEPRECATED_Vae" | "DEPRECATED_CLIP" | "DEPRECATED_Collection" | "DEPRECATED_CollectionItem" | "DEPRECATED_Enum" | "DEPRECATED_WorkflowField" | "DEPRECATED_IsIntermediate" | "DEPRECATED_BoardField" | "DEPRECATED_MetadataItem" | "DEPRECATED_MetadataItemCollection" | "DEPRECATED_MetadataItemPolymorphic" | "DEPRECATED_MetadataDict"; - /** - * StableDiffusionOnnxModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusionOnnxModelFormat: "olive" | "onnx"; - /** - * CLIPVisionModelFormat - * @description An enumeration. - * @enum {string} - */ - CLIPVisionModelFormat: "diffusers"; /** * StableDiffusion1ModelFormat * @description An enumeration. * @enum {string} */ StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; - /** - * StableDiffusionXLModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; - /** - * StableDiffusion2ModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; - /** - * T2IAdapterModelFormat - * @description An enumeration. - * @enum {string} - */ - T2IAdapterModelFormat: "diffusers"; /** * IPAdapterModelFormat * @description An enumeration. * @enum {string} */ IPAdapterModelFormat: "invokeai"; + /** + * T2IAdapterModelFormat + * @description An enumeration. + * @enum {string} + */ + T2IAdapterModelFormat: "diffusers"; + /** + * StableDiffusionOnnxModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusionOnnxModelFormat: "olive" | "onnx"; /** * ControlNetModelFormat * @description An enumeration. * @enum {string} */ ControlNetModelFormat: "checkpoint" | "diffusers"; + /** + * StableDiffusion2ModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; + /** + * StableDiffusionXLModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; + /** + * CLIPVisionModelFormat + * @description An enumeration. + * @enum {string} + */ + CLIPVisionModelFormat: "diffusers"; }; responses: never; parameters: never; @@ -10542,6 +11049,29 @@ export type operations = { }; }; }; + /** Get Image Workflow */ + get_image_workflow: { + parameters: { + path: { + /** @description The name of image whose workflow to get */ + image_name: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["WorkflowWithoutID"] | null; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; /** * Get Image Full * @description Gets a full-resolution image file @@ -11523,7 +12053,119 @@ export type operations = { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["WorkflowField"]; + "application/json": components["schemas"]["WorkflowRecordDTO"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** + * Delete Workflow + * @description Deletes a workflow + */ + delete_workflow: { + parameters: { + path: { + /** @description The workflow to delete */ + workflow_id: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** + * Update Workflow + * @description Updates a workflow + */ + update_workflow: { + requestBody: { + content: { + "application/json": components["schemas"]["Body_update_workflow"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["WorkflowRecordDTO"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** + * List Workflows + * @description Gets a page of workflows + */ + list_workflows: { + parameters: { + query?: { + /** @description The page to get */ + page?: number; + /** @description The number of workflows per page */ + per_page?: number; + /** @description The order by */ + order_by?: components["schemas"]["WorkflowRecordOrderBy"]; + /** @description The order by */ + direction?: components["schemas"]["SQLiteDirection"]; + /** @description The category to get */ + category?: components["schemas"]["WorkflowCategory"]; + /** @description The text to filter by, matches name and description */ + query?: string | null; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["PaginatedResults_WorkflowRecordListItemDTO_"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** + * Create Workflow + * @description Creates a workflow + */ + create_workflow: { + requestBody: { + content: { + "application/json": components["schemas"]["Body_create_workflow"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["WorkflowRecordDTO"]; }; }; /** @description Validation Error */ diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 9b6e531d93..91da86d58c 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -114,6 +114,10 @@ export type GraphExecutionState = s['GraphExecutionState']; export type Batch = s['Batch']; export type SessionQueueItemDTO = s['SessionQueueItemDTO']; export type SessionQueueItem = s['SessionQueueItem']; +export type WorkflowRecordOrderBy = s['WorkflowRecordOrderBy']; +export type SQLiteDirection = s['SQLiteDirection']; +export type WorkflowDTO = s['WorkflowRecordDTO']; +export type WorkflowRecordListItemDTO = s['WorkflowRecordListItemDTO']; // General nodes export type CollectInvocation = s['CollectInvocation']; diff --git a/pyproject.toml b/pyproject.toml index 961a8335e8..bb8b7a361b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,10 +36,10 @@ dependencies = [ "albumentations", "basicsr", "click", - "clip_anytorch", # replacing "clip @ https://github.com/openai/CLIP/archive/eaa22acb90a5876642d0507623e859909230a52d.zip", + "clip_anytorch", # replacing "clip @ https://github.com/openai/CLIP/archive/eaa22acb90a5876642d0507623e859909230a52d.zip", "compel~=2.0.2", "controlnet-aux>=0.0.6", - "timm==0.6.13", # needed to override timm latest in controlnet_aux, see https://github.com/isl-org/ZoeDepth/issues/26 + "timm==0.6.13", # needed to override timm latest in controlnet_aux, see https://github.com/isl-org/ZoeDepth/issues/26 "datasets", "diffusers[torch]~=0.23.0", "dnspython~=2.4.0", @@ -51,9 +51,9 @@ dependencies = [ "fastapi-events~=0.9.1", "huggingface-hub~=0.16.4", "imohash", - "invisible-watermark~=0.2.0", # needed to install SDXL base and refiner using their repo_ids - "matplotlib", # needed for plotting of Penner easing functions - "mediapipe", # needed for "mediapipeface" controlnet model + "invisible-watermark~=0.2.0", # needed to install SDXL base and refiner using their repo_ids + "matplotlib", # needed for plotting of Penner easing functions + "mediapipe", # needed for "mediapipeface" controlnet model # Minimum numpy version of 1.24.0 is needed to use the 'strict' argument to np.testing.assert_array_equal(). "numpy>=1.24.0", "npyscreen", @@ -61,7 +61,7 @@ dependencies = [ "onnx", "onnxruntime", "opencv-python~=4.8.1.1", - "pydantic~=2.5.0", + "pydantic~=2.5.2", "pydantic-settings~=2.0.3", "picklescan", "pillow", @@ -98,7 +98,7 @@ dependencies = [ ] "dev" = ["jurigged", "pudb"] "test" = [ - "ruff", + "ruff==0.1.7", "ruff-lsp", "mypy", "pre-commit", @@ -166,6 +166,7 @@ version = { attr = "invokeai.version.__version__" } [tool.setuptools.package-data] "invokeai.app.assets" = ["**/*.png"] +"invokeai.app.services.workflow_records.default_workflows" = ["*.json"] "invokeai.assets.fonts" = ["**/*.ttf"] "invokeai.backend" = ["**.png"] "invokeai.configs" = ["*.example", "**/*.yaml", "*.txt"] diff --git a/tests/app/services/model_records/test_model_records_sql.py b/tests/app/services/model_records/test_model_records_sql.py index 002c3e2c07..5c8bbb4048 100644 --- a/tests/app/services/model_records/test_model_records_sql.py +++ b/tests/app/services/model_records/test_model_records_sql.py @@ -13,7 +13,7 @@ from invokeai.app.services.model_records import ( ModelRecordServiceSQL, UnknownModelException, ) -from invokeai.app.services.shared.sqlite import SqliteDatabase +from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase from invokeai.backend.model_manager.config import ( BaseModelType, MainCheckpointConfig, diff --git a/tests/nodes/test_graph_execution_state.py b/tests/nodes/test_graph_execution_state.py index cc40970ace..203c470469 100644 --- a/tests/nodes/test_graph_execution_state.py +++ b/tests/nodes/test_graph_execution_state.py @@ -28,7 +28,7 @@ from invokeai.app.services.shared.graph import ( IterateInvocation, LibraryGraph, ) -from invokeai.app.services.shared.sqlite import SqliteDatabase +from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase from invokeai.backend.util.logging import InvokeAILogger from .test_invoker import create_edge @@ -77,7 +77,6 @@ def mock_services() -> InvocationServices: session_queue=None, # type: ignore urls=None, # type: ignore workflow_records=None, # type: ignore - workflow_image_records=None, # type: ignore ) @@ -94,6 +93,7 @@ def invoke_next(g: GraphExecutionState, services: InvocationServices) -> tuple[B queue_id=DEFAULT_QUEUE_ID, services=services, graph_execution_state_id="1", + workflow=None, ) ) g.complete(n.id, o) diff --git a/tests/nodes/test_invoker.py b/tests/nodes/test_invoker.py index c775fb93f2..88186d5448 100644 --- a/tests/nodes/test_invoker.py +++ b/tests/nodes/test_invoker.py @@ -24,7 +24,7 @@ from invokeai.app.services.invoker import Invoker from invokeai.app.services.item_storage.item_storage_sqlite import SqliteItemStorage from invokeai.app.services.session_queue.session_queue_common import DEFAULT_QUEUE_ID from invokeai.app.services.shared.graph import Graph, GraphExecutionState, GraphInvocation, LibraryGraph -from invokeai.app.services.shared.sqlite import SqliteDatabase +from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase @pytest.fixture @@ -82,7 +82,6 @@ def mock_services() -> InvocationServices: session_queue=None, # type: ignore urls=None, # type: ignore workflow_records=None, # type: ignore - workflow_image_records=None, # type: ignore ) diff --git a/tests/nodes/test_sqlite.py b/tests/nodes/test_sqlite.py index 818f9d048f..9d2d6d5cd3 100644 --- a/tests/nodes/test_sqlite.py +++ b/tests/nodes/test_sqlite.py @@ -3,7 +3,7 @@ from pydantic import BaseModel, Field from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.app.services.item_storage.item_storage_sqlite import SqliteItemStorage -from invokeai.app.services.shared.sqlite import SqliteDatabase +from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase from invokeai.backend.util.logging import InvokeAILogger diff --git a/tests/test_config.py b/tests/test_config.py index 6d76872a0d..740a4866dd 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -206,9 +206,9 @@ def test_deny_nodes(patch_rootdir): # confirm invocations union will not have denied nodes all_invocations = BaseInvocation.get_invocations() - has_integer = len([i for i in all_invocations if i.__fields__.get("type").default == "integer"]) == 1 - has_string = len([i for i in all_invocations if i.__fields__.get("type").default == "string"]) == 1 - has_float = len([i for i in all_invocations if i.__fields__.get("type").default == "float"]) == 1 + has_integer = len([i for i in all_invocations if i.model_fields.get("type").default == "integer"]) == 1 + has_string = len([i for i in all_invocations if i.model_fields.get("type").default == "string"]) == 1 + has_float = len([i for i in all_invocations if i.model_fields.get("type").default == "float"]) == 1 assert has_integer assert has_string