From c2da74c5871e5992eed0fedc088d4b46e5731464 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 17 Oct 2023 17:02:15 +1100 Subject: [PATCH 01/27] feat: add workflows table & service --- invokeai/app/api/dependencies.py | 3 + invokeai/app/api/routers/workflows.py | 20 +++ invokeai/app/api_app.py | 20 ++- invokeai/app/invocations/baseinvocation.py | 32 +--- invokeai/app/services/invocation_services.py | 4 + .../app/services/workflow_records/__init__.py | 0 .../workflow_records/workflow_records_base.py | 17 ++ .../workflow_records_common.py | 22 +++ .../workflow_records_sqlite.py | 148 ++++++++++++++++++ tests/nodes/test_graph_execution_state.py | 1 + tests/nodes/test_invoker.py | 1 + 11 files changed, 235 insertions(+), 33 deletions(-) create mode 100644 invokeai/app/api/routers/workflows.py create mode 100644 invokeai/app/services/workflow_records/__init__.py create mode 100644 invokeai/app/services/workflow_records/workflow_records_base.py create mode 100644 invokeai/app/services/workflow_records/workflow_records_common.py create mode 100644 invokeai/app/services/workflow_records/workflow_records_sqlite.py diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index c9a2f0a843..ae4882c0d0 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -30,6 +30,7 @@ from ..services.shared.default_graphs import create_system_graphs from ..services.shared.graph import GraphExecutionState, LibraryGraph from ..services.shared.sqlite import SqliteDatabase from ..services.urls.urls_default import LocalUrlService +from ..services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage from .events import FastAPIEventService @@ -90,6 +91,7 @@ class ApiDependencies: session_processor = DefaultSessionProcessor() session_queue = SqliteSessionQueue(db=db) urls = LocalUrlService() + workflow_records = SqliteWorkflowRecordsStorage(db=db) services = InvocationServices( board_image_records=board_image_records, @@ -114,6 +116,7 @@ class ApiDependencies: session_processor=session_processor, session_queue=session_queue, urls=urls, + workflow_records=workflow_records, ) create_system_graphs(services.graph_library) diff --git a/invokeai/app/api/routers/workflows.py b/invokeai/app/api/routers/workflows.py new file mode 100644 index 0000000000..814123fc81 --- /dev/null +++ b/invokeai/app/api/routers/workflows.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter, Body, Path + +from invokeai.app.api.dependencies import ApiDependencies +from invokeai.app.services.workflow_records.workflow_records_common import WorkflowField + +workflows_router = APIRouter(prefix="/v1/workflows", tags=["workflows"]) + + +@workflows_router.get( + "/i/{workflow_id}", + operation_id="get_workflow", + responses={ + 200: {"model": WorkflowField}, + }, +) +async def get_workflow( + workflow_id: str = Path(description="The workflow to get"), +) -> WorkflowField: + """Gets a workflow""" + return ApiDependencies.invoker.services.workflow_records.get(workflow_id) diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index 866a6665c8..e04cf564ab 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -38,7 +38,17 @@ if True: # hack to make flake8 happy with imports coming after setting up the c from ..backend.util.logging import InvokeAILogger from .api.dependencies import ApiDependencies - from .api.routers import app_info, board_images, boards, images, models, session_queue, sessions, utilities + from .api.routers import ( + app_info, + board_images, + boards, + images, + models, + sessions, + session_queue, + utilities, + workflows, + ) from .api.sockets import SocketIO from .invocations.baseinvocation import BaseInvocation, UIConfigBase, _InputField, _OutputField @@ -95,18 +105,13 @@ async def shutdown_event() -> None: app.include_router(sessions.session_router, prefix="/api") app.include_router(utilities.utilities_router, prefix="/api") - app.include_router(models.models_router, prefix="/api") - app.include_router(images.images_router, prefix="/api") - app.include_router(boards.boards_router, prefix="/api") - app.include_router(board_images.board_images_router, prefix="/api") - app.include_router(app_info.app_router, prefix="/api") - app.include_router(session_queue.session_queue_router, prefix="/api") +app.include_router(workflows.workflows_router, prefix="/api") # Build a custom OpenAPI to include all outputs @@ -166,7 +171,6 @@ def custom_openapi() -> dict[str, Any]: # print(f"Config with name {name} already defined") continue - # "BaseModelType":{"title":"BaseModelType","description":"An enumeration.","enum":["sd-1","sd-2"],"type":"string"} openapi_schema["components"]["schemas"][name] = dict( title=name, description="An enumeration.", diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index ba94e7c440..f2e5f33e6e 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -2,7 +2,6 @@ from __future__ import annotations -import json import re from abc import ABC, abstractmethod from enum import Enum @@ -11,12 +10,13 @@ from types import UnionType from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterable, Literal, Optional, Type, TypeVar, Union import semver -from pydantic import BaseModel, ConfigDict, Field, create_model, field_validator +from pydantic import BaseModel, ConfigDict, Field, RootModel, TypeAdapter, create_model from pydantic.fields import _Unset from pydantic_core import PydanticUndefined from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.app.util.misc import uuid_string +from invokeai.app.services.workflow_records.workflow_records_common import WorkflowField if TYPE_CHECKING: from ..services.invocation_services import InvocationServices @@ -60,7 +60,7 @@ class FieldDescriptions: denoised_latents = "Denoised latents tensor" latents = "Latents tensor" strength = "Strength of denoising (proportional to steps)" - core_metadata = "Optional core metadata to be written to image" + workflow = "Optional workflow to be saved with the image" interp_mode = "Interpolation mode" torch_antialias = "Whether or not to apply antialiasing (bilinear or bicubic only)" fp32 = "Whether or not to use full float32 precision" @@ -665,27 +665,7 @@ class BaseInvocation(ABC, BaseModel): description="Whether or not this is an intermediate invocation.", json_schema_extra=dict(ui_type=UIType.IsIntermediate), ) - workflow: Optional[str] = Field( - default=None, - description="The workflow to save with the image", - json_schema_extra=dict(ui_type=UIType.WorkflowField), - ) - use_cache: Optional[bool] = Field( - default=True, - description="Whether or not to use the cache", - ) - - @field_validator("workflow", mode="before") - @classmethod - def validate_workflow_is_json(cls, v): - """We don't have a workflow schema in the backend, so we just check that it's valid JSON""" - if v is None: - return None - try: - json.loads(v) - except json.decoder.JSONDecodeError: - raise ValueError("Workflow must be valid JSON") - return v + use_cache: bool = InputField(default=True, description="Whether or not to use the cache") UIConfig: ClassVar[Type[UIConfigBase]] @@ -824,4 +804,6 @@ def invocation_output( return wrapper -GenericBaseModel = TypeVar("GenericBaseModel", bound=BaseModel) +class WithWorkflow(BaseModel): + workflow: Optional[WorkflowField] = InputField(default=None, description=FieldDescriptions.workflow) + diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index ba53ea50cf..94db75d810 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -27,6 +27,7 @@ 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_records.workflow_records_base import WorkflowRecordsStorageBase class InvocationServices: @@ -55,6 +56,7 @@ class InvocationServices: invocation_cache: "InvocationCacheBase" names: "NameServiceBase" urls: "UrlServiceBase" + workflow_records: "WorkflowRecordsStorageBase" def __init__( self, @@ -80,6 +82,7 @@ class InvocationServices: invocation_cache: "InvocationCacheBase", names: "NameServiceBase", urls: "UrlServiceBase", + workflow_records: "WorkflowRecordsStorageBase", ): self.board_images = board_images self.board_image_records = board_image_records @@ -103,3 +106,4 @@ class InvocationServices: self.invocation_cache = invocation_cache self.names = names self.urls = urls + self.workflow_records = workflow_records diff --git a/invokeai/app/services/workflow_records/__init__.py b/invokeai/app/services/workflow_records/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/invokeai/app/services/workflow_records/workflow_records_base.py b/invokeai/app/services/workflow_records/workflow_records_base.py new file mode 100644 index 0000000000..97f7cfe3c0 --- /dev/null +++ b/invokeai/app/services/workflow_records/workflow_records_base.py @@ -0,0 +1,17 @@ +from abc import ABC, abstractmethod + +from invokeai.app.services.workflow_records.workflow_records_common import WorkflowField + + +class WorkflowRecordsStorageBase(ABC): + """Base class for workflow storage services.""" + + @abstractmethod + def get(self, workflow_id: str) -> WorkflowField: + """Get workflow by id.""" + pass + + @abstractmethod + def create(self, workflow: WorkflowField) -> WorkflowField: + """Creates a workflow.""" + pass diff --git a/invokeai/app/services/workflow_records/workflow_records_common.py b/invokeai/app/services/workflow_records/workflow_records_common.py new file mode 100644 index 0000000000..d548656dab --- /dev/null +++ b/invokeai/app/services/workflow_records/workflow_records_common.py @@ -0,0 +1,22 @@ +from typing import Any + +from pydantic import Field, RootModel, TypeAdapter + + +class WorkflowNotFoundError(Exception): + """Raised when a workflow is not found""" + + +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="Workflow dict") + + def model_dump(self, *args, **kwargs) -> dict[str, Any]: + return super().model_dump(*args, **kwargs)["root"] + + +type_adapter_WorkflowField = TypeAdapter(WorkflowField) diff --git a/invokeai/app/services/workflow_records/workflow_records_sqlite.py b/invokeai/app/services/workflow_records/workflow_records_sqlite.py new file mode 100644 index 0000000000..2b284ac03f --- /dev/null +++ b/invokeai/app/services/workflow_records/workflow_records_sqlite.py @@ -0,0 +1,148 @@ +import sqlite3 +import threading + +from invokeai.app.services.invoker import Invoker +from invokeai.app.services.shared.sqlite import SqliteDatabase +from invokeai.app.services.workflow_records.workflow_records_base import WorkflowRecordsStorageBase +from invokeai.app.services.workflow_records.workflow_records_common import ( + WorkflowField, + WorkflowNotFoundError, + type_adapter_WorkflowField, +) +from invokeai.app.util.misc import uuid_string + + +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 + self._conn = db.conn + self._cursor = self._conn.cursor() + self._create_tables() + + def start(self, invoker: Invoker) -> None: + self._invoker = invoker + + def get(self, workflow_id: str) -> WorkflowField: + try: + self._lock.acquire() + self._cursor.execute( + """--sql + SELECT workflow + FROM workflows + WHERE workflow_id = ?; + """, + (workflow_id,), + ) + row = self._cursor.fetchone() + if row is None: + raise WorkflowNotFoundError(f"Workflow with id {workflow_id} not found") + return type_adapter_WorkflowField.validate_json(row[0]) + except Exception: + self._conn.rollback() + raise + finally: + self._lock.release() + + def create(self, workflow: WorkflowField) -> WorkflowField: + try: + # workflows do not have ids until they are saved + workflow_id = uuid_string() + workflow.root["id"] = workflow_id + self._lock.acquire() + self._cursor.execute( + """--sql + INSERT INTO workflows(workflow) + VALUES (?); + """, + (workflow.json(),), + ) + self._conn.commit() + except Exception: + self._conn.rollback() + raise + finally: + self._lock.release() + return self.get(workflow_id) + + def _create_tables(self) -> None: + try: + self._lock.acquire() + self._cursor.execute( + """--sql + CREATE TABLE IF NOT EXISTS workflows ( + 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 + ); + """ + ) + + self._cursor.execute( + """--sql + CREATE TRIGGER IF NOT EXISTS tg_workflows_updated_at + AFTER UPDATE + ON workflows FOR EACH ROW + BEGIN + UPDATE workflows + SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') + WHERE workflow_id = old.workflow_id; + END; + """ + ) + + self._conn.commit() + except Exception: + self._conn.rollback() + raise + finally: + self._lock.release() + + # def update(self, workflow_id: str, workflow: Workflow) -> Workflow: + # """Updates a workflow record.""" + # try: + # workflow_id = workflow.get("id", None) + # if type(workflow_id) is not str: + # raise WorkflowNotFoundError(f"Workflow does not have a valid id, got {workflow_id}") + # self._lock.acquire() + # self._cursor.execute( + # """--sql + # UPDATE workflows + # SET workflow = ? + # WHERE workflow_id = ? + # """, + # (workflow, 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) -> Workflow: + # """Updates a workflow record.""" + # workflow = self.get(workflow_id) + # try: + # self._lock.acquire() + # self._cursor.execute( + # """--sql + # DELETE FROM workflows + # WHERE workflow_id = ? + # """, + # (workflow_id,), + # ) + # self._conn.commit() + # except Exception: + # self._conn.rollback() + # raise + # finally: + # self._lock.release() + # return workflow diff --git a/tests/nodes/test_graph_execution_state.py b/tests/nodes/test_graph_execution_state.py index 27b8a58bea..e2d435e621 100644 --- a/tests/nodes/test_graph_execution_state.py +++ b/tests/nodes/test_graph_execution_state.py @@ -75,6 +75,7 @@ def mock_services() -> InvocationServices: session_processor=None, # type: ignore session_queue=None, # type: ignore urls=None, # type: ignore + workflow_records=None, # type: ignore ) diff --git a/tests/nodes/test_invoker.py b/tests/nodes/test_invoker.py index 105f7417cd..9774f07fdd 100644 --- a/tests/nodes/test_invoker.py +++ b/tests/nodes/test_invoker.py @@ -80,6 +80,7 @@ def mock_services() -> InvocationServices: session_processor=None, # type: ignore session_queue=None, # type: ignore urls=None, # type: ignore + workflow_records=None, # type: ignore ) From f0db4d36e459a802abe9843ce619cc9e9bd3e3e0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 17 Oct 2023 17:23:10 +1100 Subject: [PATCH 02/27] feat: metadata refactor - Refactor how metadata is handled to support a user-defined metadata in graphs - Update workflow embed handling - Update UI to work with these changes - Update tests to support metadata/workflow changes --- invokeai/app/api/routers/images.py | 33 +- invokeai/app/api/routers/workflows.py | 2 +- invokeai/app/api_app.py | 2 +- invokeai/app/invocations/baseinvocation.py | 36 +- .../controlnet_image_processors.py | 7 +- invokeai/app/invocations/cv.py | 4 +- invokeai/app/invocations/facetools.py | 8 +- invokeai/app/invocations/image.py | 223 +-- invokeai/app/invocations/infill.py | 21 +- invokeai/app/invocations/latent.py | 12 +- invokeai/app/invocations/metadata.py | 174 +- invokeai/app/invocations/onnx.py | 14 +- invokeai/app/invocations/primitives.py | 4 +- invokeai/app/invocations/upscale.py | 5 +- .../services/image_files/image_files_base.py | 7 +- .../services/image_files/image_files_disk.py | 11 +- .../image_records/image_records_base.py | 6 +- .../image_records/image_records_common.py | 8 + .../image_records/image_records_sqlite.py | 35 +- invokeai/app/services/images/images_base.py | 9 +- invokeai/app/services/images/images_common.py | 2 - .../app/services/images/images_default.py | 35 +- invokeai/app/services/shared/graph.py | 32 +- .../CurrentImage/CurrentImageButtons.tsx | 41 +- .../SingleSelectionMenuItems.tsx | 42 +- .../ImageMetadataViewer.tsx | 28 +- .../Invocation/EmbedWorkflowCheckbox.tsx | 6 +- .../nodes/Invocation/InvocationNodeFooter.tsx | 4 +- .../features/nodes/hooks/useWithWorkflow.ts | 31 + .../util/validateSourceAndTargetTypes.ts | 5 +- .../web/src/features/nodes/types/constants.ts | 38 + .../web/src/features/nodes/types/types.ts | 120 +- .../nodes/util/fieldTemplateBuilders.ts | 144 +- .../features/nodes/util/fieldValueBuilders.ts | 6 + .../addControlNetToLinearGraph.ts | 25 +- .../nodes/util/graphBuilders/addHrfToGraph.ts | 42 +- .../addIPAdapterToLinearGraph.ts | 33 +- .../util/graphBuilders/addLoRAsToGraph.ts | 53 +- .../util/graphBuilders/addSDXLLoRAstoGraph.ts | 63 +- .../graphBuilders/addSDXLRefinerToGraph.ts | 27 +- .../util/graphBuilders/addSaveImageNode.ts | 27 +- .../graphBuilders/addSeamlessToLinearGraph.ts | 12 + .../addT2IAdapterToLinearGraph.ts | 23 +- .../nodes/util/graphBuilders/addVAEToGraph.ts | 10 +- .../graphBuilders/addWatermarkerToGraph.ts | 25 +- .../graphBuilders/buildAdHocUpscaleGraph.ts | 9 +- .../buildCanvasImageToImageGraph.ts | 14 +- .../buildCanvasSDXLImageToImageGraph.ts | 23 +- .../buildCanvasSDXLTextToImageGraph.ts | 23 +- .../buildCanvasTextToImageGraph.ts | 23 +- .../graphBuilders/buildLinearBatchConfig.ts | 75 +- .../buildLinearImageToImageGraph.ts | 23 +- .../buildLinearSDXLImageToImageGraph.ts | 25 +- .../buildLinearSDXLTextToImageGraph.ts | 23 +- .../buildLinearTextToImageGraph.ts | 30 +- .../util/graphBuilders/buildNodesGraph.ts | 5 +- .../nodes/util/graphBuilders/constants.ts | 8 + .../nodes/util/graphBuilders/metadata.ts | 58 + .../src/features/nodes/util/parseSchema.ts | 44 +- .../web/src/services/api/endpoints/images.ts | 18 +- .../src/services/api/endpoints/workflows.ts | 31 + .../frontend/web/src/services/api/index.ts | 1 + .../frontend/web/src/services/api/schema.d.ts | 1489 +++++++---------- .../frontend/web/src/services/api/types.ts | 17 +- tests/nodes/test_node_graph.py | 148 +- tests/nodes/test_nodes.py | 23 + 66 files changed, 1807 insertions(+), 1798 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useWithWorkflow.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/graphBuilders/metadata.ts create mode 100644 invokeai/frontend/web/src/services/api/endpoints/workflows.ts diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 84d8e8eea4..f462437700 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -5,12 +5,13 @@ from fastapi import Body, HTTPException, Path, Query, Request, Response, UploadF from fastapi.responses import FileResponse from fastapi.routing import APIRouter from PIL import Image -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ValidationError -from invokeai.app.invocations.metadata import ImageMetadata +from invokeai.app.invocations.baseinvocation import MetadataField, type_adapter_MetadataField 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 type_adapter_WorkflowField from ..dependencies import ApiDependencies @@ -45,8 +46,10 @@ async def upload_image( if not file.content_type or not file.content_type.startswith("image"): raise HTTPException(status_code=415, detail="Not an image") - contents = await file.read() + metadata = None + workflow = None + contents = await file.read() try: pil_image = Image.open(io.BytesIO(contents)) if crop_visible: @@ -56,6 +59,24 @@ async def upload_image( # Error opening the image raise HTTPException(status_code=415, detail="Failed to read image") + # attempt to parse metadata from image + metadata_raw = pil_image.info.get("invokeai_metadata", None) + if metadata_raw: + try: + metadata = type_adapter_MetadataField.validate_json(metadata_raw) + except ValidationError: + ApiDependencies.invoker.services.logger.warn("Failed to parse metadata for uploaded image") + pass + + # attempt to parse workflow from image + workflow_raw = pil_image.info.get("invokeai_workflow", None) + if workflow_raw is not None: + try: + workflow = type_adapter_WorkflowField.validate_json(workflow_raw) + except ValidationError: + ApiDependencies.invoker.services.logger.warn("Failed to parse metadata for uploaded image") + pass + try: image_dto = ApiDependencies.invoker.services.images.create( image=pil_image, @@ -63,6 +84,8 @@ async def upload_image( image_category=image_category, session_id=session_id, board_id=board_id, + metadata=metadata, + workflow=workflow, is_intermediate=is_intermediate, ) @@ -146,11 +169,11 @@ async def get_image_dto( @images_router.get( "/i/{image_name}/metadata", operation_id="get_image_metadata", - response_model=ImageMetadata, + response_model=Optional[MetadataField], ) async def get_image_metadata( image_name: str = Path(description="The name of image to get"), -) -> ImageMetadata: +) -> Optional[MetadataField]: """Gets an image's metadata""" try: diff --git a/invokeai/app/api/routers/workflows.py b/invokeai/app/api/routers/workflows.py index 814123fc81..57a33fe73f 100644 --- a/invokeai/app/api/routers/workflows.py +++ b/invokeai/app/api/routers/workflows.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Body, Path +from fastapi import APIRouter, Path from invokeai.app.api.dependencies import ApiDependencies from invokeai.app.services.workflow_records.workflow_records_common import WorkflowField diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index e04cf564ab..51aa14c75b 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -44,8 +44,8 @@ if True: # hack to make flake8 happy with imports coming after setting up the c boards, images, models, - sessions, session_queue, + sessions, utilities, workflows, ) diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index f2e5f33e6e..39df4971a6 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -15,8 +15,8 @@ from pydantic.fields import _Unset from pydantic_core import PydanticUndefined from invokeai.app.services.config.config_default import InvokeAIAppConfig -from invokeai.app.util.misc import uuid_string from invokeai.app.services.workflow_records.workflow_records_common import WorkflowField +from invokeai.app.util.misc import uuid_string if TYPE_CHECKING: from ..services.invocation_services import InvocationServices @@ -60,6 +60,11 @@ class FieldDescriptions: denoised_latents = "Denoised latents tensor" latents = "Latents tensor" strength = "Strength of denoising (proportional to steps)" + metadata = "Optional metadata to be saved with the image" + metadata_collection = "Collection of Metadata" + metadata_item_polymorphic = "A single metadata item or collection of metadata items" + metadata_item_label = "Label for this metadata item" + metadata_item_value = "The value for this metadata item (may be any type)" workflow = "Optional workflow to be saved with the image" interp_mode = "Interpolation mode" torch_antialias = "Whether or not to apply antialiasing (bilinear or bicubic only)" @@ -167,8 +172,12 @@ class UIType(str, Enum): Scheduler = "Scheduler" WorkflowField = "WorkflowField" IsIntermediate = "IsIntermediate" - MetadataField = "MetadataField" BoardField = "BoardField" + Any = "Any" + MetadataItem = "MetadataItem" + MetadataItemCollection = "MetadataItemCollection" + MetadataItemPolymorphic = "MetadataItemPolymorphic" + MetadataDict = "MetadataDict" # endregion @@ -807,3 +816,26 @@ def invocation_output( class WithWorkflow(BaseModel): workflow: Optional[WorkflowField] = InputField(default=None, description=FieldDescriptions.workflow) + +class MetadataItemField(BaseModel): + label: str = Field(description=FieldDescriptions.metadata_item_label) + value: Any = Field(description=FieldDescriptions.metadata_item_value) + + +class MetadataField(RootModel): + """ + Pydantic model for metadata with custom root of type dict[str, Any]. + Metadata is stored without a strict schema. + """ + + root: dict[str, Any] = Field(description="A dictionary of metadata, shape of which is arbitrary") + + def model_dump(self, *args, **kwargs) -> dict[str, Any]: + return super().model_dump(*args, **kwargs)["root"] + + +type_adapter_MetadataField = TypeAdapter(MetadataField) + + +class WithMetadata(BaseModel): + metadata: Optional[MetadataField] = InputField(default=None, description=FieldDescriptions.metadata) diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index 200c37d851..7c76b70e7f 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -38,6 +38,8 @@ from .baseinvocation import ( InputField, InvocationContext, OutputField, + WithMetadata, + WithWorkflow, invocation, invocation_output, ) @@ -127,12 +129,12 @@ class ControlNetInvocation(BaseInvocation): # This invocation exists for other invocations to subclass it - do not register with @invocation! -class ImageProcessorInvocation(BaseInvocation): +class ImageProcessorInvocation(BaseInvocation, WithMetadata, WithWorkflow): """Base class for invocations that preprocess images for ControlNet""" image: ImageField = InputField(description="The image to process") - def run_processor(self, image): + def run_processor(self, image: Image.Image) -> Image.Image: # superclass just passes through image without processing return image @@ -150,6 +152,7 @@ class ImageProcessorInvocation(BaseInvocation): session_id=context.graph_execution_state_id, node_id=self.id, is_intermediate=self.is_intermediate, + metadata=self.metadata, workflow=self.workflow, ) diff --git a/invokeai/app/invocations/cv.py b/invokeai/app/invocations/cv.py index 3b85955d74..e5cfd327c1 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, invocation +from .baseinvocation import BaseInvocation, InputField, InvocationContext, WithMetadata, WithWorkflow, invocation @invocation("cv_inpaint", title="OpenCV Inpaint", tags=["opencv", "inpaint"], category="inpaint", version="1.0.0") -class CvInpaintInvocation(BaseInvocation): +class CvInpaintInvocation(BaseInvocation, WithMetadata, WithWorkflow): """Simple inpaint using opencv.""" image: ImageField = InputField(description="The image to inpaint") diff --git a/invokeai/app/invocations/facetools.py b/invokeai/app/invocations/facetools.py index 40e15e9476..0bb24ef69d 100644 --- a/invokeai/app/invocations/facetools.py +++ b/invokeai/app/invocations/facetools.py @@ -16,6 +16,8 @@ from invokeai.app.invocations.baseinvocation import ( InputField, InvocationContext, OutputField, + WithMetadata, + WithWorkflow, invocation, invocation_output, ) @@ -437,7 +439,7 @@ def get_faces_list( @invocation("face_off", title="FaceOff", tags=["image", "faceoff", "face", "mask"], category="image", version="1.0.2") -class FaceOffInvocation(BaseInvocation): +class FaceOffInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Bound, extract, and mask a face from an image using MediaPipe detection""" image: ImageField = InputField(description="Image for face detection") @@ -531,7 +533,7 @@ class FaceOffInvocation(BaseInvocation): @invocation("face_mask_detection", title="FaceMask", tags=["image", "face", "mask"], category="image", version="1.0.2") -class FaceMaskInvocation(BaseInvocation): +class FaceMaskInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Face mask creation using mediapipe face detection""" image: ImageField = InputField(description="Image to face detect") @@ -650,7 +652,7 @@ class FaceMaskInvocation(BaseInvocation): @invocation( "face_identifier", title="FaceIdentifier", tags=["image", "face", "identifier"], category="image", version="1.0.2" ) -class FaceIdentifierInvocation(BaseInvocation): +class FaceIdentifierInvocation(BaseInvocation, WithWorkflow, 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") diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 3a4f4eadac..9a4e9da954 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -7,13 +7,21 @@ import cv2 import numpy from PIL import Image, ImageChops, ImageFilter, ImageOps -from invokeai.app.invocations.metadata import CoreMetadata from invokeai.app.invocations.primitives import BoardField, ColorField, ImageField, ImageOutput from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark from invokeai.backend.image_util.safety_checker import SafetyChecker -from .baseinvocation import BaseInvocation, FieldDescriptions, Input, InputField, InvocationContext, invocation +from .baseinvocation import ( + BaseInvocation, + FieldDescriptions, + Input, + InputField, + InvocationContext, + WithMetadata, + WithWorkflow, + invocation, +) @invocation("show_image", title="Show Image", tags=["image"], category="image", version="1.0.0") @@ -36,14 +44,8 @@ class ShowImageInvocation(BaseInvocation): ) -@invocation( - "blank_image", - title="Blank Image", - tags=["image"], - category="image", - version="1.0.0", -) -class BlankImageInvocation(BaseInvocation): +@invocation("blank_image", title="Blank Image", tags=["image"], category="image", version="1.0.0") +class BlankImageInvocation(BaseInvocation, WithMetadata, WithWorkflow): """Creates a blank image and forwards it to the pipeline""" width: int = InputField(default=512, description="The width of the image") @@ -61,6 +63,7 @@ class BlankImageInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, workflow=self.workflow, ) @@ -71,14 +74,8 @@ class BlankImageInvocation(BaseInvocation): ) -@invocation( - "img_crop", - title="Crop Image", - tags=["image", "crop"], - category="image", - version="1.0.0", -) -class ImageCropInvocation(BaseInvocation): +@invocation("img_crop", title="Crop Image", tags=["image", "crop"], category="image", version="1.0.0") +class ImageCropInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Crops an image to a specified box. The box can be outside of the image.""" image: ImageField = InputField(description="The image to crop") @@ -100,6 +97,7 @@ class ImageCropInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, workflow=self.workflow, ) @@ -110,14 +108,8 @@ class ImageCropInvocation(BaseInvocation): ) -@invocation( - "img_paste", - title="Paste Image", - tags=["image", "paste"], - category="image", - version="1.0.1", -) -class ImagePasteInvocation(BaseInvocation): +@invocation("img_paste", title="Paste Image", tags=["image", "paste"], category="image", version="1.0.1") +class ImagePasteInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Pastes an image into another image.""" base_image: ImageField = InputField(description="The base image") @@ -159,6 +151,7 @@ class ImagePasteInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, workflow=self.workflow, ) @@ -169,14 +162,8 @@ class ImagePasteInvocation(BaseInvocation): ) -@invocation( - "tomask", - title="Mask from Alpha", - tags=["image", "mask"], - category="image", - version="1.0.0", -) -class MaskFromAlphaInvocation(BaseInvocation): +@invocation("tomask", title="Mask from Alpha", tags=["image", "mask"], category="image", version="1.0.0") +class MaskFromAlphaInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Extracts the alpha channel of an image as a mask.""" image: ImageField = InputField(description="The image to create the mask from") @@ -196,6 +183,7 @@ class MaskFromAlphaInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, workflow=self.workflow, ) @@ -206,14 +194,8 @@ class MaskFromAlphaInvocation(BaseInvocation): ) -@invocation( - "img_mul", - title="Multiply Images", - tags=["image", "multiply"], - category="image", - version="1.0.0", -) -class ImageMultiplyInvocation(BaseInvocation): +@invocation("img_mul", title="Multiply Images", tags=["image", "multiply"], category="image", version="1.0.0") +class ImageMultiplyInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Multiplies two images together using `PIL.ImageChops.multiply()`.""" image1: ImageField = InputField(description="The first image to multiply") @@ -232,6 +214,7 @@ class ImageMultiplyInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, workflow=self.workflow, ) @@ -245,14 +228,8 @@ class ImageMultiplyInvocation(BaseInvocation): IMAGE_CHANNELS = Literal["A", "R", "G", "B"] -@invocation( - "img_chan", - title="Extract Image Channel", - tags=["image", "channel"], - category="image", - version="1.0.0", -) -class ImageChannelInvocation(BaseInvocation): +@invocation("img_chan", title="Extract Image Channel", tags=["image", "channel"], category="image", version="1.0.0") +class ImageChannelInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Gets a channel from an image.""" image: ImageField = InputField(description="The image to get the channel from") @@ -270,6 +247,7 @@ class ImageChannelInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, workflow=self.workflow, ) @@ -283,14 +261,8 @@ class ImageChannelInvocation(BaseInvocation): 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.0.0", -) -class ImageConvertInvocation(BaseInvocation): +@invocation("img_conv", title="Convert Image Mode", tags=["image", "convert"], category="image", version="1.0.0") +class ImageConvertInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Converts an image to a different mode.""" image: ImageField = InputField(description="The image to convert") @@ -308,6 +280,7 @@ class ImageConvertInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, workflow=self.workflow, ) @@ -318,14 +291,8 @@ class ImageConvertInvocation(BaseInvocation): ) -@invocation( - "img_blur", - title="Blur Image", - tags=["image", "blur"], - category="image", - version="1.0.0", -) -class ImageBlurInvocation(BaseInvocation): +@invocation("img_blur", title="Blur Image", tags=["image", "blur"], category="image", version="1.0.0") +class ImageBlurInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Blurs an image""" image: ImageField = InputField(description="The image to blur") @@ -348,6 +315,7 @@ class ImageBlurInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, workflow=self.workflow, ) @@ -378,23 +346,14 @@ PIL_RESAMPLING_MAP = { } -@invocation( - "img_resize", - title="Resize Image", - tags=["image", "resize"], - category="image", - version="1.0.0", -) -class ImageResizeInvocation(BaseInvocation): +@invocation("img_resize", title="Resize Image", tags=["image", "resize"], category="image", version="1.0.0") +class ImageResizeInvocation(BaseInvocation, WithMetadata, WithWorkflow): """Resizes an image to specific dimensions""" image: ImageField = InputField(description="The image to resize") width: int = InputField(default=512, gt=0, description="The width to resize to (px)") height: int = InputField(default=512, gt=0, description="The height to resize to (px)") resample_mode: PIL_RESAMPLING_MODES = InputField(default="bicubic", description="The resampling mode") - metadata: Optional[CoreMetadata] = InputField( - default=None, description=FieldDescriptions.core_metadata, ui_hidden=True - ) def invoke(self, context: InvocationContext) -> ImageOutput: image = context.services.images.get_pil_image(self.image.image_name) @@ -413,7 +372,7 @@ class ImageResizeInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, - metadata=self.metadata.model_dump() if self.metadata else None, + metadata=self.metadata, workflow=self.workflow, ) @@ -424,14 +383,8 @@ class ImageResizeInvocation(BaseInvocation): ) -@invocation( - "img_scale", - title="Scale Image", - tags=["image", "scale"], - category="image", - version="1.0.0", -) -class ImageScaleInvocation(BaseInvocation): +@invocation("img_scale", title="Scale Image", tags=["image", "scale"], category="image", version="1.0.0") +class ImageScaleInvocation(BaseInvocation, WithMetadata, WithWorkflow): """Scales an image by a factor""" image: ImageField = InputField(description="The image to scale") @@ -461,6 +414,7 @@ class ImageScaleInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, workflow=self.workflow, ) @@ -471,14 +425,8 @@ class ImageScaleInvocation(BaseInvocation): ) -@invocation( - "img_lerp", - title="Lerp Image", - tags=["image", "lerp"], - category="image", - version="1.0.0", -) -class ImageLerpInvocation(BaseInvocation): +@invocation("img_lerp", title="Lerp Image", tags=["image", "lerp"], category="image", version="1.0.0") +class ImageLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Linear interpolation of all pixels of an image""" image: ImageField = InputField(description="The image to lerp") @@ -500,6 +448,7 @@ class ImageLerpInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, workflow=self.workflow, ) @@ -510,14 +459,8 @@ class ImageLerpInvocation(BaseInvocation): ) -@invocation( - "img_ilerp", - title="Inverse Lerp Image", - tags=["image", "ilerp"], - category="image", - version="1.0.0", -) -class ImageInverseLerpInvocation(BaseInvocation): +@invocation("img_ilerp", title="Inverse Lerp Image", tags=["image", "ilerp"], category="image", version="1.0.0") +class ImageInverseLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Inverse linear interpolation of all pixels of an image""" image: ImageField = InputField(description="The image to lerp") @@ -539,6 +482,7 @@ class ImageInverseLerpInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, workflow=self.workflow, ) @@ -549,20 +493,11 @@ class ImageInverseLerpInvocation(BaseInvocation): ) -@invocation( - "img_nsfw", - title="Blur NSFW Image", - tags=["image", "nsfw"], - category="image", - version="1.0.0", -) -class ImageNSFWBlurInvocation(BaseInvocation): +@invocation("img_nsfw", title="Blur NSFW Image", tags=["image", "nsfw"], category="image", version="1.0.0") +class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithWorkflow): """Add blur to NSFW-flagged images""" image: ImageField = InputField(description="The image to check") - metadata: Optional[CoreMetadata] = InputField( - default=None, description=FieldDescriptions.core_metadata, ui_hidden=True - ) def invoke(self, context: InvocationContext) -> ImageOutput: image = context.services.images.get_pil_image(self.image.image_name) @@ -583,7 +518,7 @@ class ImageNSFWBlurInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, - metadata=self.metadata.model_dump() if self.metadata else None, + metadata=self.metadata, workflow=self.workflow, ) @@ -607,14 +542,11 @@ class ImageNSFWBlurInvocation(BaseInvocation): category="image", version="1.0.0", ) -class ImageWatermarkInvocation(BaseInvocation): +class ImageWatermarkInvocation(BaseInvocation, WithMetadata, WithWorkflow): """Add an invisible watermark to an image""" image: ImageField = InputField(description="The image to check") text: str = InputField(default="InvokeAI", description="Watermark text") - metadata: Optional[CoreMetadata] = InputField( - default=None, description=FieldDescriptions.core_metadata, ui_hidden=True - ) def invoke(self, context: InvocationContext) -> ImageOutput: image = context.services.images.get_pil_image(self.image.image_name) @@ -626,7 +558,7 @@ class ImageWatermarkInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, - metadata=self.metadata.model_dump() if self.metadata else None, + metadata=self.metadata, workflow=self.workflow, ) @@ -637,14 +569,8 @@ class ImageWatermarkInvocation(BaseInvocation): ) -@invocation( - "mask_edge", - title="Mask Edge", - tags=["image", "mask", "inpaint"], - category="image", - version="1.0.0", -) -class MaskEdgeInvocation(BaseInvocation): +@invocation("mask_edge", title="Mask Edge", tags=["image", "mask", "inpaint"], category="image", version="1.0.0") +class MaskEdgeInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Applies an edge mask to an image""" image: ImageField = InputField(description="The image to apply the mask to") @@ -678,6 +604,7 @@ class MaskEdgeInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, workflow=self.workflow, ) @@ -695,7 +622,7 @@ class MaskEdgeInvocation(BaseInvocation): category="image", version="1.0.0", ) -class MaskCombineInvocation(BaseInvocation): +class MaskCombineInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Combine two masks together by multiplying them using `PIL.ImageChops.multiply()`.""" mask1: ImageField = InputField(description="The first mask to combine") @@ -714,6 +641,7 @@ class MaskCombineInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, workflow=self.workflow, ) @@ -724,14 +652,8 @@ class MaskCombineInvocation(BaseInvocation): ) -@invocation( - "color_correct", - title="Color Correct", - tags=["image", "color"], - category="image", - version="1.0.0", -) -class ColorCorrectInvocation(BaseInvocation): +@invocation("color_correct", title="Color Correct", tags=["image", "color"], category="image", version="1.0.0") +class ColorCorrectInvocation(BaseInvocation, WithWorkflow, 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. @@ -830,6 +752,7 @@ class ColorCorrectInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, workflow=self.workflow, ) @@ -840,14 +763,8 @@ class ColorCorrectInvocation(BaseInvocation): ) -@invocation( - "img_hue_adjust", - title="Adjust Image Hue", - tags=["image", "hue"], - category="image", - version="1.0.0", -) -class ImageHueAdjustmentInvocation(BaseInvocation): +@invocation("img_hue_adjust", title="Adjust Image Hue", tags=["image", "hue"], category="image", version="1.0.0") +class ImageHueAdjustmentInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Adjusts the Hue of an image.""" image: ImageField = InputField(description="The image to adjust") @@ -875,6 +792,7 @@ class ImageHueAdjustmentInvocation(BaseInvocation): node_id=self.id, is_intermediate=self.is_intermediate, session_id=context.graph_execution_state_id, + metadata=self.metadata, workflow=self.workflow, ) @@ -950,7 +868,7 @@ CHANNEL_FORMATS = { category="image", version="1.0.0", ) -class ImageChannelOffsetInvocation(BaseInvocation): +class ImageChannelOffsetInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Add or subtract a value from a specific color channel of an image.""" image: ImageField = InputField(description="The image to adjust") @@ -984,6 +902,7 @@ class ImageChannelOffsetInvocation(BaseInvocation): node_id=self.id, is_intermediate=self.is_intermediate, session_id=context.graph_execution_state_id, + metadata=self.metadata, workflow=self.workflow, ) @@ -1020,7 +939,7 @@ class ImageChannelOffsetInvocation(BaseInvocation): category="image", version="1.0.0", ) -class ImageChannelMultiplyInvocation(BaseInvocation): +class ImageChannelMultiplyInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Scale a specific color channel of an image.""" image: ImageField = InputField(description="The image to adjust") @@ -1060,6 +979,7 @@ class ImageChannelMultiplyInvocation(BaseInvocation): is_intermediate=self.is_intermediate, session_id=context.graph_execution_state_id, workflow=self.workflow, + metadata=self.metadata, ) return ImageOutput( @@ -1079,16 +999,11 @@ class ImageChannelMultiplyInvocation(BaseInvocation): version="1.0.1", use_cache=False, ) -class SaveImageInvocation(BaseInvocation): +class SaveImageInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Saves an image. Unlike an image primitive, this invocation stores a copy of the image.""" image: ImageField = InputField(description=FieldDescriptions.image) - board: Optional[BoardField] = InputField(default=None, description=FieldDescriptions.board, input=Input.Direct) - metadata: Optional[CoreMetadata] = InputField( - default=None, - description=FieldDescriptions.core_metadata, - ui_hidden=True, - ) + board: BoardField = InputField(default=None, description=FieldDescriptions.board, input=Input.Direct) def invoke(self, context: InvocationContext) -> ImageOutput: image = context.services.images.get_pil_image(self.image.image_name) @@ -1101,7 +1016,7 @@ class SaveImageInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, - metadata=self.metadata.model_dump() if self.metadata else None, + metadata=self.metadata, workflow=self.workflow, ) diff --git a/invokeai/app/invocations/infill.py b/invokeai/app/invocations/infill.py index d8384290f3..b100fe7c4e 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, invocation +from .baseinvocation import BaseInvocation, InputField, InvocationContext, WithMetadata, WithWorkflow, invocation from .image import PIL_RESAMPLING_MAP, PIL_RESAMPLING_MODES @@ -119,7 +119,7 @@ def tile_fill_missing(im: Image.Image, tile_size: int = 16, seed: Optional[int] @invocation("infill_rgba", title="Solid Color Infill", tags=["image", "inpaint"], category="inpaint", version="1.0.0") -class InfillColorInvocation(BaseInvocation): +class InfillColorInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Infills transparent areas of an image with a solid color""" image: ImageField = InputField(description="The image to infill") @@ -143,6 +143,7 @@ class InfillColorInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, workflow=self.workflow, ) @@ -154,7 +155,7 @@ class InfillColorInvocation(BaseInvocation): @invocation("infill_tile", title="Tile Infill", tags=["image", "inpaint"], category="inpaint", version="1.0.0") -class InfillTileInvocation(BaseInvocation): +class InfillTileInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Infills transparent areas of an image with tiles of the image""" image: ImageField = InputField(description="The image to infill") @@ -179,6 +180,7 @@ class InfillTileInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, workflow=self.workflow, ) @@ -192,7 +194,7 @@ class InfillTileInvocation(BaseInvocation): @invocation( "infill_patchmatch", title="PatchMatch Infill", tags=["image", "inpaint"], category="inpaint", version="1.0.0" ) -class InfillPatchMatchInvocation(BaseInvocation): +class InfillPatchMatchInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Infills transparent areas of an image using the PatchMatch algorithm""" image: ImageField = InputField(description="The image to infill") @@ -232,6 +234,7 @@ class InfillPatchMatchInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, workflow=self.workflow, ) @@ -243,7 +246,7 @@ class InfillPatchMatchInvocation(BaseInvocation): @invocation("infill_lama", title="LaMa Infill", tags=["image", "inpaint"], category="inpaint", version="1.0.0") -class LaMaInfillInvocation(BaseInvocation): +class LaMaInfillInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Infills transparent areas of an image using the LaMa model""" image: ImageField = InputField(description="The image to infill") @@ -260,6 +263,8 @@ class LaMaInfillInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, + workflow=self.workflow, ) return ImageOutput( @@ -269,8 +274,8 @@ class LaMaInfillInvocation(BaseInvocation): ) -@invocation("infill_cv2", title="CV2 Infill", tags=["image", "inpaint"], category="inpaint", version="1.0.0") -class CV2InfillInvocation(BaseInvocation): +@invocation("infill_cv2", title="CV2 Infill", tags=["image", "inpaint"], category="inpaint") +class CV2InfillInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Infills transparent areas of an image using OpenCV Inpainting""" image: ImageField = InputField(description="The image to infill") @@ -287,6 +292,8 @@ class CV2InfillInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, + workflow=self.workflow, ) return ImageOutput( diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index c28c87395d..a537972c0b 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -23,7 +23,6 @@ from pydantic import field_validator from torchvision.transforms.functional import resize as tv_resize from invokeai.app.invocations.ip_adapter import IPAdapterField -from invokeai.app.invocations.metadata import CoreMetadata from invokeai.app.invocations.primitives import ( DenoiseMaskField, DenoiseMaskOutput, @@ -64,6 +63,8 @@ from .baseinvocation import ( InvocationContext, OutputField, UIType, + WithMetadata, + WithWorkflow, invocation, invocation_output, ) @@ -792,7 +793,7 @@ class DenoiseLatentsInvocation(BaseInvocation): category="latents", version="1.0.0", ) -class LatentsToImageInvocation(BaseInvocation): +class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow): """Generates an image from latents.""" latents: LatentsField = InputField( @@ -805,11 +806,6 @@ class LatentsToImageInvocation(BaseInvocation): ) tiled: bool = InputField(default=False, description=FieldDescriptions.tiled) fp32: bool = InputField(default=DEFAULT_PRECISION == "float32", description=FieldDescriptions.fp32) - metadata: Optional[CoreMetadata] = InputField( - default=None, - description=FieldDescriptions.core_metadata, - ui_hidden=True, - ) @torch.no_grad() def invoke(self, context: InvocationContext) -> ImageOutput: @@ -878,7 +874,7 @@ class LatentsToImageInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, - metadata=self.metadata.model_dump() if self.metadata else None, + metadata=self.metadata, workflow=self.workflow, ) diff --git a/invokeai/app/invocations/metadata.py b/invokeai/app/invocations/metadata.py index 4d76926aaa..205dbef814 100644 --- a/invokeai/app/invocations/metadata.py +++ b/invokeai/app/invocations/metadata.py @@ -1,13 +1,17 @@ -from typing import Optional +from typing import Any, Literal, Optional, Union -from pydantic import Field +from pydantic import BaseModel, ConfigDict, Field from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, + FieldDescriptions, InputField, InvocationContext, + MetadataField, + MetadataItemField, OutputField, + UIType, invocation, invocation_output, ) @@ -16,116 +20,100 @@ from invokeai.app.invocations.ip_adapter import IPAdapterModelField from invokeai.app.invocations.model import LoRAModelField, MainModelField, VAEModelField from invokeai.app.invocations.primitives import ImageField from invokeai.app.invocations.t2i_adapter import T2IAdapterField -from invokeai.app.util.model_exclude_null import BaseModelExcludeNull from ...version import __version__ -class LoRAMetadataField(BaseModelExcludeNull): - """LoRA metadata for an image generated in InvokeAI.""" +class LoRAMetadataField(BaseModel): + """LoRA Metadata Field""" - lora: LoRAModelField = Field(description="The LoRA model") - weight: float = Field(description="The weight of the LoRA model") + lora: LoRAModelField = Field(description=FieldDescriptions.lora_model) + weight: float = Field(description=FieldDescriptions.lora_weight) -class IPAdapterMetadataField(BaseModelExcludeNull): +class IPAdapterMetadataField(BaseModel): + """IP Adapter Field, minus the CLIP Vision Encoder model""" + image: ImageField = Field(description="The IP-Adapter image prompt.") - ip_adapter_model: IPAdapterModelField = Field(description="The IP-Adapter model to use.") - weight: float = Field(description="The weight of the IP-Adapter model") + ip_adapter_model: IPAdapterModelField = Field( + description="The IP-Adapter model.", + ) + weight: Union[float, list[float]] = Field( + default=1, + ge=0, + description="The weight given to the IP-Adapter", + ) begin_step_percent: float = Field( - default=0, ge=0, le=1, description="When the IP-Adapter is first applied (% of total steps)" + default=0, ge=-1, le=2, description="When the IP-Adapter is first applied (% of total steps)" ) end_step_percent: float = Field( default=1, ge=0, le=1, description="When the IP-Adapter is last applied (% of total steps)" ) -class CoreMetadata(BaseModelExcludeNull): - """Core generation metadata for an image generated in InvokeAI.""" +@invocation_output("metadata_item_output") +class MetadataItemOutput(BaseInvocationOutput): + """Metadata Item Output""" - app_version: str = Field(default=__version__, description="The version of InvokeAI used to generate this image") - generation_mode: Optional[str] = Field( - default=None, - description="The generation mode that output this image", - ) - created_by: Optional[str] = Field(default=None, description="The name of the creator of the image") - positive_prompt: Optional[str] = Field(default=None, description="The positive prompt parameter") - negative_prompt: Optional[str] = Field(default=None, description="The negative prompt parameter") - width: Optional[int] = Field(default=None, description="The width parameter") - height: Optional[int] = Field(default=None, description="The height parameter") - seed: Optional[int] = Field(default=None, description="The seed used for noise generation") - rand_device: Optional[str] = Field(default=None, description="The device used for random number generation") - cfg_scale: Optional[float] = Field(default=None, description="The classifier-free guidance scale parameter") - steps: Optional[int] = Field(default=None, description="The number of steps used for inference") - scheduler: Optional[str] = Field(default=None, description="The scheduler used for inference") - clip_skip: Optional[int] = Field( - default=None, - description="The number of skipped CLIP layers", - ) - model: Optional[MainModelField] = Field(default=None, description="The main model used for inference") - controlnets: Optional[list[ControlField]] = Field(default=None, description="The ControlNets used for inference") - ipAdapters: Optional[list[IPAdapterMetadataField]] = Field( - default=None, description="The IP Adapters used for inference" - ) - t2iAdapters: Optional[list[T2IAdapterField]] = Field(default=None, description="The IP Adapters used for inference") - loras: Optional[list[LoRAMetadataField]] = Field(default=None, description="The LoRAs used for inference") - vae: Optional[VAEModelField] = Field( - default=None, - description="The VAE used for decoding, if the main model's default was not used", + item: MetadataItemField = OutputField(description="Metadata Item") + + +@invocation("metadata_item", title="Metadata Item", tags=["metadata"], category="metadata", version="1.0.0") +class MetadataItemInvocation(BaseInvocation): + """Used to create an arbitrary metadata item. Provide "label" and make a connection to "value" to store that data as the value.""" + + label: str = InputField(description=FieldDescriptions.metadata_item_label) + value: Any = InputField(description=FieldDescriptions.metadata_item_value, ui_type=UIType.Any) + + def invoke(self, context: InvocationContext) -> MetadataItemOutput: + return MetadataItemOutput(item=MetadataItemField(label=self.label, value=self.value)) + + +@invocation_output("metadata_output") +class MetadataOutput(BaseInvocationOutput): + metadata: MetadataField = OutputField(description="Metadata Dict") + + +@invocation("metadata", title="Metadata", tags=["metadata"], category="metadata", version="1.0.0") +class MetadataInvocation(BaseInvocation): + """Takes a MetadataItem or collection of MetadataItems and outputs a MetadataDict.""" + + items: Union[list[MetadataItemField], MetadataItemField] = InputField( + description=FieldDescriptions.metadata_item_polymorphic ) - # Latents-to-Latents - strength: Optional[float] = Field( - default=None, - description="The strength used for latents-to-latents", - ) - init_image: Optional[str] = Field(default=None, description="The name of the initial image") + def invoke(self, context: InvocationContext) -> MetadataOutput: + if isinstance(self.items, MetadataItemField): + # single metadata item + data = {self.items.label: self.items.value} + else: + # collection of metadata items + data = {item.label: item.value for item in self.items} - # SDXL - positive_style_prompt: Optional[str] = Field(default=None, description="The positive style prompt parameter") - negative_style_prompt: Optional[str] = Field(default=None, description="The negative style prompt parameter") - - # SDXL Refiner - refiner_model: Optional[MainModelField] = Field(default=None, description="The SDXL Refiner model used") - refiner_cfg_scale: Optional[float] = Field( - default=None, - description="The classifier-free guidance scale parameter used for the refiner", - ) - refiner_steps: Optional[int] = Field(default=None, description="The number of steps used for the refiner") - refiner_scheduler: Optional[str] = Field(default=None, description="The scheduler used for the refiner") - refiner_positive_aesthetic_score: Optional[float] = Field( - default=None, description="The aesthetic score used for the refiner" - ) - refiner_negative_aesthetic_score: Optional[float] = Field( - default=None, description="The aesthetic score used for the refiner" - ) - refiner_start: Optional[float] = Field(default=None, description="The start value used for refiner denoising") + # add app version + data.update({"app_version": __version__}) + return MetadataOutput(metadata=MetadataField.model_validate(data)) -class ImageMetadata(BaseModelExcludeNull): - """An image's generation metadata""" +@invocation("merge_metadata", title="Metadata Merge", tags=["metadata"], category="metadata", version="1.0.0") +class MergeMetadataInvocation(BaseInvocation): + """Merged a collection of MetadataDict into a single MetadataDict.""" - metadata: Optional[dict] = Field( - default=None, - description="The image's core metadata, if it was created in the Linear or Canvas UI", - ) - graph: Optional[dict] = Field(default=None, description="The graph that created the image") + collection: list[MetadataField] = InputField(description=FieldDescriptions.metadata_collection) + + def invoke(self, context: InvocationContext) -> MetadataOutput: + data = {} + for item in self.collection: + data.update(item.model_dump()) + + return MetadataOutput(metadata=MetadataField.model_validate(data)) -@invocation_output("metadata_accumulator_output") -class MetadataAccumulatorOutput(BaseInvocationOutput): - """The output of the MetadataAccumulator node""" +@invocation("core_metadata", title="Core Metadata", tags=["metadata"], category="metadata", version="1.0.0") +class CoreMetadataInvocation(BaseInvocation): + """Collects core generation metadata into a MetadataField""" - metadata: CoreMetadata = OutputField(description="The core metadata for the image") - - -@invocation( - "metadata_accumulator", title="Metadata Accumulator", tags=["metadata"], category="metadata", version="1.0.0" -) -class MetadataAccumulatorInvocation(BaseInvocation): - """Outputs a Core Metadata Object""" - - generation_mode: Optional[str] = InputField( + generation_mode: Literal["txt2img", "img2img", "inpaint", "outpaint"] = InputField( default=None, description="The generation mode that output this image", ) @@ -138,6 +126,8 @@ class MetadataAccumulatorInvocation(BaseInvocation): cfg_scale: Optional[float] = InputField(default=None, description="The classifier-free guidance scale parameter") steps: Optional[int] = InputField(default=None, description="The number of steps used for inference") scheduler: Optional[str] = InputField(default=None, description="The scheduler used for inference") + seamless_x: Optional[bool] = InputField(default=None, description="Whether seamless tiling was used on the X axis") + seamless_y: Optional[bool] = InputField(default=None, description="Whether seamless tiling was used on the Y axis") clip_skip: Optional[int] = InputField( default=None, description="The number of skipped CLIP layers", @@ -220,7 +210,13 @@ class MetadataAccumulatorInvocation(BaseInvocation): description="The start value used for refiner denoising", ) - def invoke(self, context: InvocationContext) -> MetadataAccumulatorOutput: + def invoke(self, context: InvocationContext) -> MetadataOutput: """Collects and outputs a CoreMetadata object""" - return MetadataAccumulatorOutput(metadata=CoreMetadata(**self.model_dump())) + return MetadataOutput( + metadata=MetadataField.model_validate( + self.model_dump(exclude_none=True, exclude={"id", "type", "is_intermediate", "use_cache"}) + ) + ) + + model_config = ConfigDict(extra="allow") diff --git a/invokeai/app/invocations/onnx.py b/invokeai/app/invocations/onnx.py index 3f4f688cf4..140505f736 100644 --- a/invokeai/app/invocations/onnx.py +++ b/invokeai/app/invocations/onnx.py @@ -4,7 +4,7 @@ import inspect import re # from contextlib import ExitStack -from typing import List, Literal, Optional, Union +from typing import List, Literal, Union import numpy as np import torch @@ -12,7 +12,6 @@ from diffusers.image_processor import VaeImageProcessor from pydantic import BaseModel, ConfigDict, Field, field_validator from tqdm import tqdm -from invokeai.app.invocations.metadata import CoreMetadata from invokeai.app.invocations.primitives import ConditioningField, ConditioningOutput, ImageField, ImageOutput from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin from invokeai.app.util.step_callback import stable_diffusion_step_callback @@ -31,6 +30,8 @@ from .baseinvocation import ( OutputField, UIComponent, UIType, + WithMetadata, + WithWorkflow, invocation, invocation_output, ) @@ -327,7 +328,7 @@ class ONNXTextToLatentsInvocation(BaseInvocation): category="image", version="1.0.0", ) -class ONNXLatentsToImageInvocation(BaseInvocation): +class ONNXLatentsToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow): """Generates an image from latents.""" latents: LatentsField = InputField( @@ -338,11 +339,6 @@ class ONNXLatentsToImageInvocation(BaseInvocation): description=FieldDescriptions.vae, input=Input.Connection, ) - metadata: Optional[CoreMetadata] = InputField( - default=None, - description=FieldDescriptions.core_metadata, - ui_hidden=True, - ) # tiled: bool = InputField(default=False, description="Decode latents by overlaping tiles(less memory consumption)") def invoke(self, context: InvocationContext) -> ImageOutput: @@ -381,7 +377,7 @@ class ONNXLatentsToImageInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, - metadata=self.metadata.model_dump() if self.metadata else None, + metadata=self.metadata, workflow=self.workflow, ) diff --git a/invokeai/app/invocations/primitives.py b/invokeai/app/invocations/primitives.py index c314edfd15..88ede88cde 100644 --- a/invokeai/app/invocations/primitives.py +++ b/invokeai/app/invocations/primitives.py @@ -251,7 +251,9 @@ class ImageCollectionOutput(BaseInvocationOutput): @invocation("image", title="Image Primitive", tags=["primitives", "image"], category="primitives", version="1.0.0") -class ImageInvocation(BaseInvocation): +class ImageInvocation( + BaseInvocation, +): """An image primitive value""" image: ImageField = InputField(description="The image to load") diff --git a/invokeai/app/invocations/upscale.py b/invokeai/app/invocations/upscale.py index d30bb71d95..1167914aca 100644 --- a/invokeai/app/invocations/upscale.py +++ b/invokeai/app/invocations/upscale.py @@ -14,7 +14,7 @@ from invokeai.app.invocations.primitives import ImageField, ImageOutput from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin from invokeai.backend.util.devices import choose_torch_device -from .baseinvocation import BaseInvocation, InputField, InvocationContext, invocation +from .baseinvocation import BaseInvocation, InputField, InvocationContext, WithMetadata, WithWorkflow, invocation # TODO: Populate this from disk? # TODO: Use model manager to load? @@ -30,7 +30,7 @@ if choose_torch_device() == torch.device("mps"): @invocation("esrgan", title="Upscale (RealESRGAN)", tags=["esrgan", "upscale"], category="esrgan", version="1.1.0") -class ESRGANInvocation(BaseInvocation): +class ESRGANInvocation(BaseInvocation, WithWorkflow, WithMetadata): """Upscales an image using RealESRGAN.""" image: ImageField = InputField(description="The input image") @@ -123,6 +123,7 @@ class ESRGANInvocation(BaseInvocation): node_id=self.id, session_id=context.graph_execution_state_id, is_intermediate=self.is_intermediate, + metadata=self.metadata, workflow=self.workflow, ) diff --git a/invokeai/app/services/image_files/image_files_base.py b/invokeai/app/services/image_files/image_files_base.py index 5dde7b05d6..3f6e797225 100644 --- a/invokeai/app/services/image_files/image_files_base.py +++ b/invokeai/app/services/image_files/image_files_base.py @@ -4,6 +4,9 @@ from typing import Optional from PIL.Image import Image as PILImageType +from invokeai.app.invocations.metadata import MetadataField +from invokeai.app.services.workflow_records.workflow_records_common import WorkflowField + class ImageFileStorageBase(ABC): """Low-level service responsible for storing and retrieving image files.""" @@ -30,8 +33,8 @@ class ImageFileStorageBase(ABC): self, image: PILImageType, image_name: str, - metadata: Optional[dict] = None, - workflow: Optional[str] = None, + metadata: Optional[MetadataField] = None, + workflow: Optional[WorkflowField] = 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.""" diff --git a/invokeai/app/services/image_files/image_files_disk.py b/invokeai/app/services/image_files/image_files_disk.py index 9111a71605..57c05562d5 100644 --- a/invokeai/app/services/image_files/image_files_disk.py +++ b/invokeai/app/services/image_files/image_files_disk.py @@ -1,5 +1,4 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI Team -import json from pathlib import Path from queue import Queue from typing import Dict, Optional, Union @@ -8,7 +7,9 @@ from PIL import Image, PngImagePlugin from PIL.Image import Image as PILImageType from send2trash import send2trash +from invokeai.app.invocations.metadata import MetadataField from invokeai.app.services.invoker import Invoker +from invokeai.app.services.workflow_records.workflow_records_common import WorkflowField from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail from .image_files_base import ImageFileStorageBase @@ -55,8 +56,8 @@ class DiskImageFileStorage(ImageFileStorageBase): self, image: PILImageType, image_name: str, - metadata: Optional[dict] = None, - workflow: Optional[str] = None, + metadata: Optional[MetadataField] = None, + workflow: Optional[WorkflowField] = None, thumbnail_size: int = 256, ) -> None: try: @@ -67,9 +68,9 @@ class DiskImageFileStorage(ImageFileStorageBase): if metadata is not None or workflow is not None: if metadata is not None: - pnginfo.add_text("invokeai_metadata", json.dumps(metadata)) + pnginfo.add_text("invokeai_metadata", metadata.model_dump_json()) if workflow is not None: - pnginfo.add_text("invokeai_workflow", workflow) + pnginfo.add_text("invokeai_workflow", workflow.model_dump_json()) else: # For uploaded images, we want to retain metadata. PIL strips it on save; manually add it back # TODO: retain non-invokeai metadata on save... diff --git a/invokeai/app/services/image_records/image_records_base.py b/invokeai/app/services/image_records/image_records_base.py index 7e74b06e9e..cd1db81857 100644 --- a/invokeai/app/services/image_records/image_records_base.py +++ b/invokeai/app/services/image_records/image_records_base.py @@ -2,6 +2,7 @@ from abc import ABC, abstractmethod from datetime import datetime from typing import Optional +from invokeai.app.invocations.metadata import MetadataField from invokeai.app.services.shared.pagination import OffsetPaginatedResults from .image_records_common import ImageCategory, ImageRecord, ImageRecordChanges, ResourceOrigin @@ -18,7 +19,7 @@ class ImageRecordStorageBase(ABC): pass @abstractmethod - def get_metadata(self, image_name: str) -> Optional[dict]: + def get_metadata(self, image_name: str) -> Optional[MetadataField]: """Gets an image's metadata'.""" pass @@ -78,7 +79,8 @@ class ImageRecordStorageBase(ABC): starred: Optional[bool] = False, session_id: Optional[str] = None, node_id: Optional[str] = None, - metadata: Optional[dict] = None, + metadata: Optional[MetadataField] = None, + workflow_id: Optional[str] = None, ) -> datetime: """Saves an image record.""" pass diff --git a/invokeai/app/services/image_records/image_records_common.py b/invokeai/app/services/image_records/image_records_common.py index 5a6e5652c9..6576fb9647 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( "width", "height", "session_id", + "workflow_id", "node_id", "is_intermediate", "created_at", @@ -140,6 +141,11 @@ class ImageRecord(BaseModelExcludeNull): description="The session ID that generated this image, if it is a generated image.", ) """The session ID that generated this image, if it is a generated image.""" + workflow_id: Optional[str] = Field( + default=None, + description="The workflow that generated this image.", + ) + """The workflow that generated this image.""" node_id: Optional[str] = Field( default=None, description="The node ID that generated this image, if it is a generated image.", @@ -184,6 +190,7 @@ def deserialize_image_record(image_dict: dict) -> ImageRecord: width = image_dict.get("width", 0) height = image_dict.get("height", 0) session_id = image_dict.get("session_id", None) + workflow_id = image_dict.get("workflow_id", None) node_id = image_dict.get("node_id", None) created_at = image_dict.get("created_at", get_iso_timestamp()) updated_at = image_dict.get("updated_at", get_iso_timestamp()) @@ -198,6 +205,7 @@ def deserialize_image_record(image_dict: dict) -> ImageRecord: width=width, height=height, session_id=session_id, + workflow_id=workflow_id, node_id=node_id, created_at=created_at, updated_at=updated_at, diff --git a/invokeai/app/services/image_records/image_records_sqlite.py b/invokeai/app/services/image_records/image_records_sqlite.py index 33bf373a7d..7b60ec3d5b 100644 --- a/invokeai/app/services/image_records/image_records_sqlite.py +++ b/invokeai/app/services/image_records/image_records_sqlite.py @@ -1,9 +1,9 @@ -import json import sqlite3 import threading from datetime import datetime from typing import Optional, Union, cast +from invokeai.app.invocations.baseinvocation import MetadataField, type_adapter_MetadataField from invokeai.app.services.shared.pagination import OffsetPaginatedResults from invokeai.app.services.shared.sqlite import SqliteDatabase @@ -76,6 +76,16 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): """ ) + if "workflow_id" not in columns: + self._cursor.execute( + """--sql + ALTER TABLE images + ADD COLUMN workflow_id TEXT; + -- TODO: This requires a migration: + -- FOREIGN KEY (workflow_id) REFERENCES workflows (workflow_id) ON DELETE SET NULL; + """ + ) + # Create the `images` table indices. self._cursor.execute( """--sql @@ -141,22 +151,26 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): return deserialize_image_record(dict(result)) - def get_metadata(self, image_name: str) -> Optional[dict]: + def get_metadata(self, image_name: str) -> Optional[MetadataField]: try: self._lock.acquire() self._cursor.execute( """--sql - SELECT images.metadata FROM images + SELECT metadata FROM images WHERE image_name = ?; """, (image_name,), ) result = cast(Optional[sqlite3.Row], self._cursor.fetchone()) - if not result or not result[0]: - return None - return json.loads(result[0]) + + if not result: + raise ImageRecordNotFoundException + + as_dict = dict(result) + metadata_raw = cast(Optional[str], as_dict.get("metadata", None)) + return type_adapter_MetadataField.validate_json(metadata_raw) if metadata_raw is not None else None except sqlite3.Error as e: self._conn.rollback() raise ImageRecordNotFoundException from e @@ -408,10 +422,11 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): starred: Optional[bool] = False, session_id: Optional[str] = None, node_id: Optional[str] = None, - metadata: Optional[dict] = None, + metadata: Optional[MetadataField] = None, + workflow_id: Optional[str] = None, ) -> datetime: try: - metadata_json = None if metadata is None else json.dumps(metadata) + metadata_json = metadata.model_dump_json() if metadata is not None else None self._lock.acquire() self._cursor.execute( """--sql @@ -424,10 +439,11 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): node_id, session_id, metadata, + workflow_id, is_intermediate, starred ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); """, ( image_name, @@ -438,6 +454,7 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): node_id, session_id, metadata_json, + workflow_id, is_intermediate, starred, ), diff --git a/invokeai/app/services/images/images_base.py b/invokeai/app/services/images/images_base.py index ac7a4a2152..ebb40424bc 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.metadata import ImageMetadata +from invokeai.app.invocations.metadata 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 WorkflowField class ImageServiceABC(ABC): @@ -50,8 +51,8 @@ class ImageServiceABC(ABC): session_id: Optional[str] = None, board_id: Optional[str] = None, is_intermediate: Optional[bool] = False, - metadata: Optional[dict] = None, - workflow: Optional[str] = None, + metadata: Optional[MetadataField] = None, + workflow: Optional[WorkflowField] = None, ) -> ImageDTO: """Creates an image, storing the file and its metadata.""" pass @@ -81,7 +82,7 @@ class ImageServiceABC(ABC): pass @abstractmethod - def get_metadata(self, image_name: str) -> ImageMetadata: + def get_metadata(self, image_name: str) -> Optional[MetadataField]: """Gets an image's metadata.""" pass diff --git a/invokeai/app/services/images/images_common.py b/invokeai/app/services/images/images_common.py index 325cecdd26..0464244b94 100644 --- a/invokeai/app/services/images/images_common.py +++ b/invokeai/app/services/images/images_common.py @@ -25,8 +25,6 @@ class ImageDTO(ImageRecord, ImageUrlsDTO): ) """The id of the board the image belongs to, if one exists.""" - pass - def image_record_to_dto( image_record: ImageRecord, diff --git a/invokeai/app/services/images/images_default.py b/invokeai/app/services/images/images_default.py index 3c78c4f29a..e466e809b1 100644 --- a/invokeai/app/services/images/images_default.py +++ b/invokeai/app/services/images/images_default.py @@ -2,10 +2,10 @@ from typing import Optional from PIL.Image import Image as PILImageType -from invokeai.app.invocations.metadata import ImageMetadata +from invokeai.app.invocations.metadata import MetadataField from invokeai.app.services.invoker import Invoker from invokeai.app.services.shared.pagination import OffsetPaginatedResults -from invokeai.app.util.metadata import get_metadata_graph_from_raw_session +from invokeai.app.services.workflow_records.workflow_records_common import WorkflowField from ..image_files.image_files_common import ( ImageFileDeleteException, @@ -42,8 +42,8 @@ class ImageService(ImageServiceABC): session_id: Optional[str] = None, board_id: Optional[str] = None, is_intermediate: Optional[bool] = False, - metadata: Optional[dict] = None, - workflow: Optional[str] = None, + metadata: Optional[MetadataField] = None, + workflow: Optional[WorkflowField] = None, ) -> ImageDTO: if image_origin not in ResourceOrigin: raise InvalidOriginException @@ -56,6 +56,12 @@ 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 +75,7 @@ class ImageService(ImageServiceABC): # Nullable fields node_id=node_id, metadata=metadata, + workflow_id=workflow_id, session_id=session_id, ) if board_id is not None: @@ -146,25 +153,9 @@ class ImageService(ImageServiceABC): self.__invoker.services.logger.error("Problem getting image DTO") raise e - def get_metadata(self, image_name: str) -> ImageMetadata: + def get_metadata(self, image_name: str) -> Optional[MetadataField]: try: - image_record = self.__invoker.services.image_records.get(image_name) - metadata = self.__invoker.services.image_records.get_metadata(image_name) - - if not image_record.session_id: - return ImageMetadata(metadata=metadata) - - session_raw = self.__invoker.services.graph_execution_manager.get_raw(image_record.session_id) - graph = None - - if session_raw: - try: - graph = get_metadata_graph_from_raw_session(session_raw) - except Exception as e: - self.__invoker.services.logger.warn(f"Failed to parse session graph: {e}") - graph = None - - return ImageMetadata(graph=graph, metadata=metadata) + return self.__invoker.services.image_records.get_metadata(image_name) except ImageRecordNotFoundException: self.__invoker.services.logger.error("Image record not found") raise diff --git a/invokeai/app/services/shared/graph.py b/invokeai/app/services/shared/graph.py index 8f974f7c6b..0f703db749 100644 --- a/invokeai/app/services/shared/graph.py +++ b/invokeai/app/services/shared/graph.py @@ -439,6 +439,14 @@ class Graph(BaseModel): except Exception as e: raise UnknownGraphValidationError(f"Problem validating graph {e}") from e + def _is_destination_field_Any(self, edge: Edge) -> bool: + """Checks if the destination field for an edge is of type typing.Any""" + return get_input_field(self.get_node(edge.destination.node_id), edge.destination.field) == Any + + def _is_destination_field_list_of_Any(self, edge: Edge) -> bool: + """Checks if the destination field for an edge is of type typing.Any""" + return get_input_field(self.get_node(edge.destination.node_id), edge.destination.field) == list[Any] + def _validate_edge(self, edge: Edge): """Validates that a new edge doesn't create a cycle in the graph""" @@ -491,8 +499,19 @@ class Graph(BaseModel): f"Collector output type does not match collector input type: {edge.source.node_id}.{edge.source.field} to {edge.destination.node_id}.{edge.destination.field}" ) - # Validate if collector output type matches input type (if this edge results in both being set) - if isinstance(from_node, CollectInvocation) and edge.source.field == "collection": + # Validate that we are not connecting collector to iterator (currently unsupported) + if isinstance(from_node, CollectInvocation) and isinstance(to_node, IterateInvocation): + raise InvalidEdgeError( + f"Cannot connect collector to iterator: {edge.source.node_id}.{edge.source.field} to {edge.destination.node_id}.{edge.destination.field}" + ) + + # Validate if collector output type matches input type (if this edge results in both being set) - skip if the destination field is not Any or list[Any] + if ( + isinstance(from_node, CollectInvocation) + and edge.source.field == "collection" + and not self._is_destination_field_list_of_Any(edge) + and not self._is_destination_field_Any(edge) + ): if not self._is_collector_connection_valid(edge.source.node_id, new_output=edge.destination): raise InvalidEdgeError( f"Collector input type does not match collector output type: {edge.source.node_id}.{edge.source.field} to {edge.destination.node_id}.{edge.destination.field}" @@ -725,16 +744,15 @@ class Graph(BaseModel): # Get the input root type input_root_type = next(t[0] for t in type_degrees if t[1] == 0) # type: ignore - # Verify that all outputs are lists - # if not all((get_origin(f) == list for f in output_fields)): - # return False - # Verify that all outputs are lists if not all(is_list_or_contains_list(f) for f in output_fields): return False # Verify that all outputs match the input type (are a base class or the same class) - if not all((issubclass(input_root_type, get_args(f)[0]) for f in output_fields)): + if not all( + is_union_subtype(input_root_type, get_args(f)[0]) or issubclass(input_root_type, get_args(f)[0]) + for f in output_fields + ): return False return True diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx index 57f06a0cea..4c0aa5e0e8 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx @@ -27,7 +27,7 @@ import { setShouldShowImageDetails, setShouldShowProgressInViewer, } from 'features/ui/store/uiSlice'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { @@ -40,11 +40,13 @@ import { import { FaCircleNodes, FaEllipsis } from 'react-icons/fa6'; import { useGetImageDTOQuery, - useGetImageMetadataFromFileQuery, + useGetImageMetadataQuery, } from 'services/api/endpoints/images'; import { menuListMotionProps } from 'theme/components/menu'; +import { useDebounce } from 'use-debounce'; import { sentImageToImg2Img } from '../../store/actions'; import SingleSelectionMenuItems from '../ImageContextMenu/SingleSelectionMenuItems'; +import { useGetWorkflowQuery } from 'services/api/endpoints/workflows'; const currentImageButtonsSelector = createSelector( [stateSelector, activeTabNameSelector], @@ -89,7 +91,6 @@ const CurrentImageButtons = () => { shouldShowImageDetails, lastSelectedImage, shouldShowProgressInViewer, - shouldFetchMetadataFromApi, } = useAppSelector(currentImageButtonsSelector); const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled; @@ -104,23 +105,17 @@ const CurrentImageButtons = () => { lastSelectedImage?.image_name ?? skipToken ); - const getMetadataArg = useMemo(() => { - if (lastSelectedImage) { - return { image: lastSelectedImage, shouldFetchMetadataFromApi }; - } else { - return skipToken; - } - }, [lastSelectedImage, shouldFetchMetadataFromApi]); + const [debouncedImageName] = useDebounce(lastSelectedImage?.image_name, 300); + const [debouncedWorkflowId] = useDebounce( + lastSelectedImage?.workflow_id, + 300 + ); - const { metadata, workflow, isLoading } = useGetImageMetadataFromFileQuery( - getMetadataArg, - { - selectFromResult: (res) => ({ - isLoading: res.isFetching, - metadata: res?.currentData?.metadata, - workflow: res?.currentData?.workflow, - }), - } + const { data: metadata, isLoading: isLoadingMetadata } = + useGetImageMetadataQuery(debouncedImageName ?? skipToken); + + const { data: workflow, isLoading: isLoadingWorkflow } = useGetWorkflowQuery( + debouncedWorkflowId ?? skipToken ); const handleLoadWorkflow = useCallback(() => { @@ -257,7 +252,7 @@ const CurrentImageButtons = () => { } tooltip={`${t('nodes.loadWorkflow')} (W)`} aria-label={`${t('nodes.loadWorkflow')} (W)`} @@ -265,7 +260,7 @@ const CurrentImageButtons = () => { onClick={handleLoadWorkflow} /> } tooltip={`${t('parameters.usePrompt')} (P)`} aria-label={`${t('parameters.usePrompt')} (P)`} @@ -273,7 +268,7 @@ const CurrentImageButtons = () => { onClick={handleUsePrompt} /> } tooltip={`${t('parameters.useSeed')} (S)`} aria-label={`${t('parameters.useSeed')} (S)`} @@ -281,7 +276,7 @@ const CurrentImageButtons = () => { onClick={handleUseSeed} /> } tooltip={`${t('parameters.useAll')} (A)`} aria-label={`${t('parameters.useAll')} (A)`} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index 35a4e9f18c..38de235e38 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -1,8 +1,9 @@ import { Flex, MenuItem, Spinner } from '@chakra-ui/react'; import { useStore } from '@nanostores/react'; +import { skipToken } from '@reduxjs/toolkit/dist/query'; import { useAppToaster } from 'app/components/Toaster'; import { $customStarUI } from 'app/store/nanostores/customStarUI'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch } from 'app/store/storeHooks'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { imagesToChangeSelected, @@ -32,12 +33,13 @@ import { import { FaCircleNodes } from 'react-icons/fa6'; import { MdStar, MdStarBorder } from 'react-icons/md'; import { - useGetImageMetadataFromFileQuery, + useGetImageMetadataQuery, useStarImagesMutation, useUnstarImagesMutation, } from 'services/api/endpoints/images'; +import { useGetWorkflowQuery } from 'services/api/endpoints/workflows'; import { ImageDTO } from 'services/api/types'; -import { configSelector } from '../../../system/store/configSelectors'; +import { useDebounce } from 'use-debounce'; import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions'; type SingleSelectionMenuItemsProps = { @@ -53,18 +55,16 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { const toaster = useAppToaster(); const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled; - const { shouldFetchMetadataFromApi } = useAppSelector(configSelector); const customStarUi = useStore($customStarUI); - const { metadata, workflow, isLoading } = useGetImageMetadataFromFileQuery( - { image: imageDTO, shouldFetchMetadataFromApi }, - { - selectFromResult: (res) => ({ - isLoading: res.isFetching, - metadata: res?.currentData?.metadata, - workflow: res?.currentData?.workflow, - }), - } + const [debouncedImageName] = useDebounce(imageDTO?.image_name, 300); + const [debouncedWorkflowId] = useDebounce(imageDTO?.workflow_id, 300); + + const { data: metadata, isLoading: isLoadingMetadata } = + useGetImageMetadataQuery(debouncedImageName ?? skipToken); + + const { data: workflow, isLoading: isLoadingWorkflow } = useGetWorkflowQuery( + debouncedWorkflowId ?? skipToken ); const [starImages] = useStarImagesMutation(); @@ -181,17 +181,17 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { {t('parameters.downloadImage')} : } + icon={isLoadingWorkflow ? : } onClickCapture={handleLoadWorkflow} - isDisabled={isLoading || !workflow} + isDisabled={isLoadingWorkflow || !workflow} > {t('nodes.loadWorkflow')} : } + icon={isLoadingMetadata ? : } onClickCapture={handleRecallPrompt} isDisabled={ - isLoading || + isLoadingMetadata || (metadata?.positive_prompt === undefined && metadata?.negative_prompt === undefined) } @@ -199,16 +199,16 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { {t('parameters.usePrompt')} : } + icon={isLoadingMetadata ? : } onClickCapture={handleRecallSeed} - isDisabled={isLoading || metadata?.seed === undefined} + isDisabled={isLoadingMetadata || metadata?.seed === undefined} > {t('parameters.useSeed')} : } + icon={isLoadingMetadata ? : } onClickCapture={handleUseAllParameters} - isDisabled={isLoading || !metadata} + isDisabled={isLoadingMetadata || !metadata} > {t('parameters.useAll')} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx index e9cb3ffcaf..f6820b9d20 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx @@ -9,16 +9,17 @@ import { Tabs, Text, } from '@chakra-ui/react'; +import { skipToken } from '@reduxjs/toolkit/dist/query'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent'; import { memo } from 'react'; -import { useGetImageMetadataFromFileQuery } from 'services/api/endpoints/images'; +import { useTranslation } from 'react-i18next'; +import { useGetImageMetadataQuery } from 'services/api/endpoints/images'; +import { useGetWorkflowQuery } from 'services/api/endpoints/workflows'; import { ImageDTO } from 'services/api/types'; +import { useDebounce } from 'use-debounce'; import DataViewer from './DataViewer'; import ImageMetadataActions from './ImageMetadataActions'; -import { useAppSelector } from '../../../../app/store/storeHooks'; -import { configSelector } from '../../../system/store/configSelectors'; -import { useTranslation } from 'react-i18next'; -import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent'; type ImageMetadataViewerProps = { image: ImageDTO; @@ -32,16 +33,15 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => { // }); const { t } = useTranslation(); - const { shouldFetchMetadataFromApi } = useAppSelector(configSelector); + const [debouncedImageName] = useDebounce(image.image_name, 300); + const [debouncedWorkflowId] = useDebounce(image.workflow_id, 300); - const { metadata, workflow } = useGetImageMetadataFromFileQuery( - { image, shouldFetchMetadataFromApi }, - { - selectFromResult: (res) => ({ - metadata: res?.currentData?.metadata, - workflow: res?.currentData?.workflow, - }), - } + const { data: metadata } = useGetImageMetadataQuery( + debouncedImageName ?? skipToken + ); + + const { data: workflow } = useGetWorkflowQuery( + debouncedWorkflowId ?? skipToken ); return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/EmbedWorkflowCheckbox.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/EmbedWorkflowCheckbox.tsx index 447dfcbd97..3c06b9f9da 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/EmbedWorkflowCheckbox.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/EmbedWorkflowCheckbox.tsx @@ -1,13 +1,13 @@ import { Checkbox, Flex, FormControl, FormLabel } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; import { useEmbedWorkflow } from 'features/nodes/hooks/useEmbedWorkflow'; -import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput'; +import { useWithWorkflow } from 'features/nodes/hooks/useWithWorkflow'; import { nodeEmbedWorkflowChanged } from 'features/nodes/store/nodesSlice'; import { ChangeEvent, memo, useCallback } from 'react'; const EmbedWorkflowCheckbox = ({ nodeId }: { nodeId: string }) => { const dispatch = useAppDispatch(); - const hasImageOutput = useHasImageOutput(nodeId); + const withWorkflow = useWithWorkflow(nodeId); const embedWorkflow = useEmbedWorkflow(nodeId); const handleChange = useCallback( (e: ChangeEvent) => { @@ -21,7 +21,7 @@ const EmbedWorkflowCheckbox = ({ nodeId }: { nodeId: string }) => { [dispatch, nodeId] ); - if (!hasImageOutput) { + if (!withWorkflow) { return null; } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeFooter.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeFooter.tsx index ec5085221e..1424c6b837 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeFooter.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeFooter.tsx @@ -1,11 +1,11 @@ import { Flex } from '@chakra-ui/react'; +import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput'; import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; import { memo } from 'react'; +import { useFeatureStatus } from '../../../../../system/hooks/useFeatureStatus'; import EmbedWorkflowCheckbox from './EmbedWorkflowCheckbox'; import SaveToGalleryCheckbox from './SaveToGalleryCheckbox'; import UseCacheCheckbox from './UseCacheCheckbox'; -import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput'; -import { useFeatureStatus } from '../../../../../system/hooks/useFeatureStatus'; type Props = { nodeId: string; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useWithWorkflow.ts b/invokeai/frontend/web/src/features/nodes/hooks/useWithWorkflow.ts new file mode 100644 index 0000000000..3c83e01731 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useWithWorkflow.ts @@ -0,0 +1,31 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { useMemo } from 'react'; +import { isInvocationNode } from '../types/types'; + +export const useWithWorkflow = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return false; + } + const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? '']; + if (!nodeTemplate) { + return false; + } + return nodeTemplate.withWorkflow; + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const withWorkflow = useAppSelector(selector); + return withWorkflow; +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateSourceAndTargetTypes.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateSourceAndTargetTypes.ts index 8c2bef34fe..2f47e47a78 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateSourceAndTargetTypes.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateSourceAndTargetTypes.ts @@ -69,6 +69,8 @@ export const validateSourceAndTargetTypes = ( (sourceType === 'integer' || sourceType === 'float') && targetType === 'string'; + const isTargetAnyType = targetType === 'Any'; + return ( isCollectionItemToNonCollection || isNonCollectionToCollectionItem || @@ -76,6 +78,7 @@ export const validateSourceAndTargetTypes = ( isGenericCollectionToAnyCollectionOrPolymorphic || isCollectionToGenericCollection || isIntToFloat || - isIntOrFloatToString + isIntOrFloatToString || + isTargetAnyType ); }; diff --git a/invokeai/frontend/web/src/features/nodes/types/constants.ts b/invokeai/frontend/web/src/features/nodes/types/constants.ts index 076f71cc02..c6eec736da 100644 --- a/invokeai/frontend/web/src/features/nodes/types/constants.ts +++ b/invokeai/frontend/web/src/features/nodes/types/constants.ts @@ -33,6 +33,8 @@ export const COLLECTION_TYPES: FieldType[] = [ 'ColorCollection', 'T2IAdapterCollection', 'IPAdapterCollection', + 'MetadataItemCollection', + 'MetadataCollection', ]; export const POLYMORPHIC_TYPES: FieldType[] = [ @@ -47,6 +49,7 @@ export const POLYMORPHIC_TYPES: FieldType[] = [ 'ColorPolymorphic', 'T2IAdapterPolymorphic', 'IPAdapterPolymorphic', + 'MetadataItemPolymorphic', ]; export const MODEL_TYPES: FieldType[] = [ @@ -78,6 +81,8 @@ export const COLLECTION_MAP: FieldTypeMapWithNumber = { ColorField: 'ColorCollection', T2IAdapterField: 'T2IAdapterCollection', IPAdapterField: 'IPAdapterCollection', + MetadataItemField: 'MetadataItemCollection', + MetadataField: 'MetadataCollection', }; export const isCollectionItemType = ( itemType: string | undefined @@ -97,6 +102,7 @@ export const SINGLE_TO_POLYMORPHIC_MAP: FieldTypeMapWithNumber = { ColorField: 'ColorPolymorphic', T2IAdapterField: 'T2IAdapterPolymorphic', IPAdapterField: 'IPAdapterPolymorphic', + MetadataItemField: 'MetadataItemPolymorphic', }; export const POLYMORPHIC_TO_SINGLE_MAP: FieldTypeMap = { @@ -111,6 +117,7 @@ export const POLYMORPHIC_TO_SINGLE_MAP: FieldTypeMap = { ColorPolymorphic: 'ColorField', T2IAdapterPolymorphic: 'T2IAdapterField', IPAdapterPolymorphic: 'IPAdapterField', + MetadataItemPolymorphic: 'MetadataItemField', }; export const TYPES_WITH_INPUT_COMPONENTS: FieldType[] = [ @@ -144,6 +151,37 @@ export const isPolymorphicItemType = ( Boolean(itemType && itemType in SINGLE_TO_POLYMORPHIC_MAP); export const FIELDS: Record = { + Any: { + color: 'gray.500', + description: 'Any field type is accepted.', + title: 'Any', + }, + MetadataField: { + color: 'gray.500', + description: 'A metadata dict.', + title: 'Metadata Dict', + }, + MetadataCollection: { + color: 'gray.500', + description: 'A collection of metadata dicts.', + title: 'Metadata Dict Collection', + }, + MetadataItemField: { + color: 'gray.500', + description: 'A metadata item.', + title: 'Metadata Item', + }, + MetadataItemCollection: { + color: 'gray.500', + description: 'Any field type is accepted.', + title: 'Metadata Item Collection', + }, + MetadataItemPolymorphic: { + color: 'gray.500', + description: + 'MetadataItem or MetadataItemCollection field types are accepted.', + title: 'Metadata Item Polymorphic', + }, boolean: { color: 'green.500', description: t('nodes.booleanDescription'), diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts index 87c716bb81..ba1ca05c4d 100644 --- a/invokeai/frontend/web/src/features/nodes/types/types.ts +++ b/invokeai/frontend/web/src/features/nodes/types/types.ts @@ -54,6 +54,10 @@ export type InvocationTemplate = { * The type of this node's output */ outputType: string; // TODO: generate a union of output types + /** + * Whether or not this invocation supports workflows + */ + withWorkflow: boolean; /** * The invocation's version. */ @@ -72,6 +76,7 @@ export type FieldUIConfig = { // TODO: Get this from the OpenAPI schema? may be tricky... export const zFieldType = z.enum([ + 'Any', 'BoardField', 'boolean', 'BooleanCollection', @@ -109,6 +114,11 @@ export const zFieldType = z.enum([ 'LatentsPolymorphic', 'LoRAModelField', 'MainModelField', + 'MetadataField', + 'MetadataCollection', + 'MetadataItemField', + 'MetadataItemCollection', + 'MetadataItemPolymorphic', 'ONNXModelField', 'Scheduler', 'SDXLMainModelField', @@ -685,6 +695,57 @@ export type CollectionItemInputFieldValue = z.infer< typeof zCollectionItemInputFieldValue >; +export const zMetadataItemField = z.object({ + label: z.string(), + value: z.any(), +}); +export type MetadataItemField = z.infer; + +export const zMetadataItemInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('MetadataItemField'), + value: zMetadataItemField.optional(), +}); +export type MetadataItemInputFieldValue = z.infer< + typeof zMetadataItemInputFieldValue +>; + +export const zMetadataItemCollectionInputFieldValue = + zInputFieldValueBase.extend({ + type: z.literal('MetadataItemCollection'), + value: z.array(zMetadataItemField).optional(), + }); +export type MetadataItemCollectionInputFieldValue = z.infer< + typeof zMetadataItemCollectionInputFieldValue +>; + +export const zMetadataItemPolymorphicInputFieldValue = + zInputFieldValueBase.extend({ + type: z.literal('MetadataItemPolymorphic'), + value: z + .union([zMetadataItemField, z.array(zMetadataItemField)]) + .optional(), + }); +export type MetadataItemPolymorphicInputFieldValue = z.infer< + typeof zMetadataItemPolymorphicInputFieldValue +>; + +export const zMetadataField = z.record(z.any()); +export type MetadataField = z.infer; + +export const zMetadataInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('MetadataField'), + value: zMetadataField.optional(), +}); +export type MetadataInputFieldValue = z.infer; + +export const zMetadataCollectionInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('MetadataCollection'), + value: z.array(zMetadataField).optional(), +}); +export type MetadataCollectionInputFieldValue = z.infer< + typeof zMetadataCollectionInputFieldValue +>; + export const zColorField = z.object({ r: z.number().int().min(0).max(255), g: z.number().int().min(0).max(255), @@ -723,7 +784,13 @@ export type SchedulerInputFieldValue = z.infer< typeof zSchedulerInputFieldValue >; +export const zAnyInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('Any'), + value: z.any().optional(), +}); + export const zInputFieldValue = z.discriminatedUnion('type', [ + zAnyInputFieldValue, zBoardInputFieldValue, zBooleanCollectionInputFieldValue, zBooleanInputFieldValue, @@ -774,6 +841,11 @@ export const zInputFieldValue = z.discriminatedUnion('type', [ zUNetInputFieldValue, zVaeInputFieldValue, zVaeModelInputFieldValue, + zMetadataItemInputFieldValue, + zMetadataItemCollectionInputFieldValue, + zMetadataItemPolymorphicInputFieldValue, + zMetadataInputFieldValue, + zMetadataCollectionInputFieldValue, ]); export type InputFieldValue = z.infer; @@ -786,6 +858,11 @@ export type InputFieldTemplateBase = { fieldKind: 'input'; } & _InputField; +export type AnyInputFieldTemplate = InputFieldTemplateBase & { + type: 'Any'; + default: undefined; +}; + export type IntegerInputFieldTemplate = InputFieldTemplateBase & { type: 'integer'; default: number; @@ -939,6 +1016,11 @@ export type UNetInputFieldTemplate = InputFieldTemplateBase & { type: 'UNetField'; }; +export type MetadataItemFieldTemplate = InputFieldTemplateBase & { + default: undefined; + type: 'MetadataItemField'; +}; + export type ClipInputFieldTemplate = InputFieldTemplateBase & { default: undefined; type: 'ClipField'; @@ -1087,6 +1169,34 @@ export type WorkflowInputFieldTemplate = InputFieldTemplateBase & { type: 'WorkflowField'; }; +export type MetadataItemInputFieldTemplate = InputFieldTemplateBase & { + default: undefined; + type: 'MetadataItemField'; +}; + +export type MetadataItemCollectionInputFieldTemplate = + InputFieldTemplateBase & { + default: undefined; + type: 'MetadataItemCollection'; + }; + +export type MetadataItemPolymorphicInputFieldTemplate = Omit< + MetadataItemInputFieldTemplate, + 'type' +> & { + type: 'MetadataItemPolymorphic'; +}; + +export type MetadataInputFieldTemplate = InputFieldTemplateBase & { + default: undefined; + type: 'MetadataField'; +}; + +export type MetadataCollectionInputFieldTemplate = InputFieldTemplateBase & { + default: undefined; + type: 'MetadataCollection'; +}; + /** * An input field template is generated on each page load from the OpenAPI schema. * @@ -1094,6 +1204,7 @@ export type WorkflowInputFieldTemplate = InputFieldTemplateBase & { * maximum length, pattern to match, etc). */ export type InputFieldTemplate = + | AnyInputFieldTemplate | BoardInputFieldTemplate | BooleanCollectionInputFieldTemplate | BooleanPolymorphicInputFieldTemplate @@ -1143,7 +1254,12 @@ export type InputFieldTemplate = | T2IAdapterPolymorphicInputFieldTemplate | UNetInputFieldTemplate | VaeInputFieldTemplate - | VaeModelInputFieldTemplate; + | VaeModelInputFieldTemplate + | MetadataItemInputFieldTemplate + | MetadataItemCollectionInputFieldTemplate + | MetadataInputFieldTemplate + | MetadataItemPolymorphicInputFieldTemplate + | MetadataCollectionInputFieldTemplate; export const isInputFieldValue = ( field?: InputFieldValue | OutputFieldValue @@ -1264,7 +1380,7 @@ export const isInvocationFieldSchema = ( export type InvocationEdgeExtra = { type: 'default' | 'collapsed' }; -const zLoRAMetadataItem = z.object({ +export const zLoRAMetadataItem = z.object({ lora: zLoRAModelField.deepPartial(), weight: z.number(), }); diff --git a/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts b/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts index 3fd44207c0..92e44e9ab2 100644 --- a/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts +++ b/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts @@ -7,6 +7,7 @@ import { startCase, } from 'lodash-es'; import { OpenAPIV3_1 } from 'openapi-types'; +import { ControlField } from 'services/api/types'; import { COLLECTION_MAP, POLYMORPHIC_TYPES, @@ -15,36 +16,70 @@ import { isPolymorphicItemType, } from '../types/constants'; import { + AnyInputFieldTemplate, + BoardInputFieldTemplate, BooleanCollectionInputFieldTemplate, BooleanInputFieldTemplate, + BooleanPolymorphicInputFieldTemplate, ClipInputFieldTemplate, CollectionInputFieldTemplate, CollectionItemInputFieldTemplate, + ColorCollectionInputFieldTemplate, ColorInputFieldTemplate, + ColorPolymorphicInputFieldTemplate, + ConditioningCollectionInputFieldTemplate, + ConditioningField, ConditioningInputFieldTemplate, + ConditioningPolymorphicInputFieldTemplate, + ControlCollectionInputFieldTemplate, ControlInputFieldTemplate, ControlNetModelInputFieldTemplate, + ControlPolymorphicInputFieldTemplate, DenoiseMaskInputFieldTemplate, EnumInputFieldTemplate, FieldType, FloatCollectionInputFieldTemplate, - FloatPolymorphicInputFieldTemplate, FloatInputFieldTemplate, + FloatPolymorphicInputFieldTemplate, + IPAdapterCollectionInputFieldTemplate, + IPAdapterField, + IPAdapterInputFieldTemplate, + IPAdapterModelInputFieldTemplate, + IPAdapterPolymorphicInputFieldTemplate, ImageCollectionInputFieldTemplate, + ImageField, ImageInputFieldTemplate, + ImagePolymorphicInputFieldTemplate, + InputFieldTemplate, InputFieldTemplateBase, IntegerCollectionInputFieldTemplate, IntegerInputFieldTemplate, + IntegerPolymorphicInputFieldTemplate, InvocationFieldSchema, InvocationSchemaObject, + LatentsCollectionInputFieldTemplate, + LatentsField, LatentsInputFieldTemplate, + LatentsPolymorphicInputFieldTemplate, LoRAModelInputFieldTemplate, MainModelInputFieldTemplate, + MetadataCollectionInputFieldTemplate, + MetadataInputFieldTemplate, + MetadataItemCollectionInputFieldTemplate, + MetadataItemInputFieldTemplate, + MetadataItemPolymorphicInputFieldTemplate, + OpenAPIV3_1SchemaOrRef, SDXLMainModelInputFieldTemplate, SDXLRefinerModelInputFieldTemplate, SchedulerInputFieldTemplate, StringCollectionInputFieldTemplate, StringInputFieldTemplate, + StringPolymorphicInputFieldTemplate, + T2IAdapterCollectionInputFieldTemplate, + T2IAdapterField, + T2IAdapterInputFieldTemplate, + T2IAdapterModelInputFieldTemplate, + T2IAdapterPolymorphicInputFieldTemplate, UNetInputFieldTemplate, VaeInputFieldTemplate, VaeModelInputFieldTemplate, @@ -52,36 +87,7 @@ import { isNonArraySchemaObject, isRefObject, isSchemaObject, - ControlPolymorphicInputFieldTemplate, - ColorPolymorphicInputFieldTemplate, - ColorCollectionInputFieldTemplate, - IntegerPolymorphicInputFieldTemplate, - StringPolymorphicInputFieldTemplate, - BooleanPolymorphicInputFieldTemplate, - ImagePolymorphicInputFieldTemplate, - LatentsPolymorphicInputFieldTemplate, - LatentsCollectionInputFieldTemplate, - ConditioningPolymorphicInputFieldTemplate, - ConditioningCollectionInputFieldTemplate, - ControlCollectionInputFieldTemplate, - ImageField, - LatentsField, - ConditioningField, - IPAdapterField, - IPAdapterInputFieldTemplate, - IPAdapterModelInputFieldTemplate, - IPAdapterPolymorphicInputFieldTemplate, - IPAdapterCollectionInputFieldTemplate, - T2IAdapterField, - T2IAdapterInputFieldTemplate, - T2IAdapterModelInputFieldTemplate, - T2IAdapterPolymorphicInputFieldTemplate, - T2IAdapterCollectionInputFieldTemplate, - BoardInputFieldTemplate, - InputFieldTemplate, - OpenAPIV3_1SchemaOrRef, } from '../types/types'; -import { ControlField } from 'services/api/types'; export type BaseFieldProperties = 'name' | 'title' | 'description'; @@ -851,6 +857,78 @@ const buildCollectionItemInputFieldTemplate = ({ return template; }; +const buildAnyInputFieldTemplate = ({ + baseField, +}: BuildInputFieldArg): AnyInputFieldTemplate => { + const template: AnyInputFieldTemplate = { + ...baseField, + type: 'Any', + default: undefined, + }; + + return template; +}; + +const buildMetadataItemInputFieldTemplate = ({ + baseField, +}: BuildInputFieldArg): MetadataItemInputFieldTemplate => { + const template: MetadataItemInputFieldTemplate = { + ...baseField, + type: 'MetadataItemField', + default: undefined, + }; + + return template; +}; + +const buildMetadataItemCollectionInputFieldTemplate = ({ + baseField, +}: BuildInputFieldArg): MetadataItemCollectionInputFieldTemplate => { + const template: MetadataItemCollectionInputFieldTemplate = { + ...baseField, + type: 'MetadataItemCollection', + default: undefined, + }; + + return template; +}; + +const buildMetadataItemPolymorphicInputFieldTemplate = ({ + baseField, +}: BuildInputFieldArg): MetadataItemPolymorphicInputFieldTemplate => { + const template: MetadataItemPolymorphicInputFieldTemplate = { + ...baseField, + type: 'MetadataItemPolymorphic', + default: undefined, + }; + + return template; +}; + +const buildMetadataDictInputFieldTemplate = ({ + baseField, +}: BuildInputFieldArg): MetadataInputFieldTemplate => { + const template: MetadataInputFieldTemplate = { + ...baseField, + type: 'MetadataField', + default: undefined, + }; + + return template; +}; + +const buildMetadataCollectionInputFieldTemplate = ({ + baseField, +}: BuildInputFieldArg): MetadataCollectionInputFieldTemplate => { + const template: MetadataCollectionInputFieldTemplate = { + ...baseField, + type: 'MetadataCollection', + default: undefined, + }; + + return template; +}; + const buildColorInputFieldTemplate = ({ schemaObject, baseField, @@ -1012,6 +1090,7 @@ const TEMPLATE_BUILDER_MAP: { [key in FieldType]?: (arg: BuildInputFieldArg) => InputFieldTemplate; } = { BoardField: buildBoardInputFieldTemplate, + Any: buildAnyInputFieldTemplate, boolean: buildBooleanInputFieldTemplate, BooleanCollection: buildBooleanCollectionInputFieldTemplate, BooleanPolymorphic: buildBooleanPolymorphicInputFieldTemplate, @@ -1047,6 +1126,11 @@ const TEMPLATE_BUILDER_MAP: { LatentsField: buildLatentsInputFieldTemplate, LatentsPolymorphic: buildLatentsPolymorphicInputFieldTemplate, LoRAModelField: buildLoRAModelInputFieldTemplate, + MetadataItemField: buildMetadataItemInputFieldTemplate, + MetadataItemCollection: buildMetadataItemCollectionInputFieldTemplate, + MetadataItemPolymorphic: buildMetadataItemPolymorphicInputFieldTemplate, + MetadataField: buildMetadataDictInputFieldTemplate, + MetadataCollection: buildMetadataCollectionInputFieldTemplate, MainModelField: buildMainModelInputFieldTemplate, Scheduler: buildSchedulerInputFieldTemplate, SDXLMainModelField: buildSDXLMainModelInputFieldTemplate, diff --git a/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts b/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts index 97f520379a..ca2513649d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts +++ b/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts @@ -3,6 +3,7 @@ import { FieldType, InputFieldTemplate, InputFieldValue } from '../types/types'; const FIELD_VALUE_FALLBACK_MAP: { [key in FieldType]: InputFieldValue['value']; } = { + Any: undefined, enum: '', BoardField: undefined, boolean: false, @@ -38,6 +39,11 @@ const FIELD_VALUE_FALLBACK_MAP: { LatentsCollection: [], LatentsField: undefined, LatentsPolymorphic: undefined, + MetadataItemField: undefined, + MetadataItemCollection: [], + MetadataItemPolymorphic: undefined, + MetadataField: undefined, + MetadataCollection: [], LoRAModelField: undefined, MainModelField: undefined, ONNXModelField: undefined, diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addControlNetToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addControlNetToLinearGraph.ts index 37bd82d4f8..60d4e36dca 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addControlNetToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addControlNetToLinearGraph.ts @@ -5,14 +5,14 @@ import { CollectInvocation, ControlField, ControlNetInvocation, - MetadataAccumulatorInvocation, + CoreMetadataInvocation, } from 'services/api/types'; import { NonNullableGraph } from '../../types/types'; import { CANVAS_COHERENCE_DENOISE_LATENTS, CONTROL_NET_COLLECT, - METADATA_ACCUMULATOR, } from './constants'; +import { upsertMetadata } from './metadata'; export const addControlNetToLinearGraph = ( state: RootState, @@ -23,9 +23,11 @@ export const addControlNetToLinearGraph = ( (ca) => ca.model?.base_model === state.generation.model?.base_model ); - const metadataAccumulator = graph.nodes[METADATA_ACCUMULATOR] as - | MetadataAccumulatorInvocation - | undefined; + // const metadataAccumulator = graph.nodes[METADATA_ACCUMULATOR] as + // | MetadataAccumulatorInvocation + // | undefined; + + const controlNetMetadata: CoreMetadataInvocation['controlnets'] = []; if (validControlNets.length) { // Even though denoise_latents' control input is polymorphic, keep it simple and always use a collect @@ -99,15 +101,9 @@ export const addControlNetToLinearGraph = ( graph.nodes[controlNetNode.id] = controlNetNode as ControlNetInvocation; - if (metadataAccumulator?.controlnets) { - // metadata accumulator only needs a control field - not the whole node - // extract what we need and add to the accumulator - const controlField = omit(controlNetNode, [ - 'id', - 'type', - ]) as ControlField; - metadataAccumulator.controlnets.push(controlField); - } + controlNetMetadata.push( + omit(controlNetNode, ['id', 'type', 'is_intermediate']) as ControlField + ); graph.edges.push({ source: { node_id: controlNetNode.id, field: 'control' }, @@ -117,5 +113,6 @@ export const addControlNetToLinearGraph = ( }, }); }); + upsertMetadata(graph, { controlnets: controlNetMetadata }); } }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addHrfToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addHrfToGraph.ts index 4b4a8a8a03..8c23ae667e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addHrfToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addHrfToGraph.ts @@ -1,25 +1,25 @@ +import { logger } from 'app/logging/logger'; import { RootState } from 'app/store/store'; import { NonNullableGraph } from 'features/nodes/types/types'; import { DenoiseLatentsInvocation, - ResizeLatentsInvocation, - NoiseInvocation, - LatentsToImageInvocation, Edge, + LatentsToImageInvocation, + NoiseInvocation, + ResizeLatentsInvocation, } from 'services/api/types'; import { - LATENTS_TO_IMAGE, DENOISE_LATENTS, - NOISE, - MAIN_MODEL_LOADER, - METADATA_ACCUMULATOR, - LATENTS_TO_IMAGE_HRF, DENOISE_LATENTS_HRF, - RESCALE_LATENTS, + LATENTS_TO_IMAGE, + LATENTS_TO_IMAGE_HRF, + MAIN_MODEL_LOADER, + NOISE, NOISE_HRF, + RESCALE_LATENTS, VAE_LOADER, } from './constants'; -import { logger } from 'app/logging/logger'; +import { upsertMetadata } from './metadata'; // Copy certain connections from previous DENOISE_LATENTS to new DENOISE_LATENTS_HRF. function copyConnectionsToDenoiseLatentsHrf(graph: NonNullableGraph): void { @@ -71,10 +71,8 @@ export const addHrfToGraph = ( } const log = logger('txt2img'); - const { vae } = state.generation; + const { vae, hrfWidth, hrfHeight, hrfStrength } = state.generation; const isAutoVae = !vae; - const hrfWidth = state.generation.hrfWidth; - const hrfHeight = state.generation.hrfHeight; // Pre-existing (original) graph nodes. const originalDenoiseLatentsNode = graph.nodes[DENOISE_LATENTS] as @@ -116,7 +114,7 @@ export const addHrfToGraph = ( cfg_scale: originalDenoiseLatentsNode?.cfg_scale, scheduler: originalDenoiseLatentsNode?.scheduler, steps: originalDenoiseLatentsNode?.steps, - denoising_start: 1 - state.generation.hrfStrength, + denoising_start: 1 - hrfStrength, denoising_end: 1, }; @@ -221,16 +219,6 @@ export const addHrfToGraph = ( field: 'latents', }, }, - { - source: { - node_id: METADATA_ACCUMULATOR, - field: 'metadata', - }, - destination: { - node_id: LATENTS_TO_IMAGE_HRF, - field: 'metadata', - }, - }, { source: { node_id: isAutoVae ? MAIN_MODEL_LOADER : VAE_LOADER, @@ -243,5 +231,11 @@ export const addHrfToGraph = ( } ); + upsertMetadata(graph, { + hrf_height: hrfHeight, + hrf_width: hrfWidth, + hrf_strength: hrfStrength, + }); + copyConnectionsToDenoiseLatentsHrf(graph); }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addIPAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addIPAdapterToLinearGraph.ts index 19bf7d8338..93c6cdb284 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addIPAdapterToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addIPAdapterToLinearGraph.ts @@ -1,16 +1,18 @@ import { RootState } from 'app/store/store'; import { selectValidIPAdapters } from 'features/controlAdapters/store/controlAdaptersSlice'; +import { omit } from 'lodash-es'; import { CollectInvocation, + CoreMetadataInvocation, IPAdapterInvocation, - MetadataAccumulatorInvocation, + IPAdapterMetadataField, } from 'services/api/types'; import { NonNullableGraph } from '../../types/types'; import { CANVAS_COHERENCE_DENOISE_LATENTS, IP_ADAPTER_COLLECT, - METADATA_ACCUMULATOR, } from './constants'; +import { upsertMetadata } from './metadata'; export const addIPAdapterToLinearGraph = ( state: RootState, @@ -21,10 +23,6 @@ export const addIPAdapterToLinearGraph = ( (ca) => ca.model?.base_model === state.generation.model?.base_model ); - const metadataAccumulator = graph.nodes[METADATA_ACCUMULATOR] as - | MetadataAccumulatorInvocation - | undefined; - if (validIPAdapters.length) { // Even though denoise_latents' control input is polymorphic, keep it simple and always use a collect const ipAdapterCollectNode: CollectInvocation = { @@ -50,6 +48,7 @@ export const addIPAdapterToLinearGraph = ( }, }); } + const ipAdapterMetdata: CoreMetadataInvocation['ipAdapters'] = []; validIPAdapters.forEach((ipAdapter) => { if (!ipAdapter.model) { @@ -76,19 +75,13 @@ export const addIPAdapterToLinearGraph = ( graph.nodes[ipAdapterNode.id] = ipAdapterNode as IPAdapterInvocation; - if (metadataAccumulator?.ipAdapters) { - const ipAdapterField = { - image: { - image_name: ipAdapter.controlImage, - }, - weight, - ip_adapter_model: model, - begin_step_percent: beginStepPct, - end_step_percent: endStepPct, - }; - - metadataAccumulator.ipAdapters.push(ipAdapterField); - } + ipAdapterMetdata.push( + omit(ipAdapterNode, [ + 'id', + 'type', + 'is_intermediate', + ]) as IPAdapterMetadataField + ); graph.edges.push({ source: { node_id: ipAdapterNode.id, field: 'ip_adapter' }, @@ -98,5 +91,7 @@ export const addIPAdapterToLinearGraph = ( }, }); }); + + upsertMetadata(graph, { ipAdapters: ipAdapterMetdata }); } }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addLoRAsToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addLoRAsToGraph.ts index e199a78a20..66c2bd0444 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addLoRAsToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addLoRAsToGraph.ts @@ -2,20 +2,20 @@ import { RootState } from 'app/store/store'; import { NonNullableGraph } from 'features/nodes/types/types'; import { forEach, size } from 'lodash-es'; import { + CoreMetadataInvocation, LoraLoaderInvocation, - MetadataAccumulatorInvocation, } from 'services/api/types'; import { + CANVAS_COHERENCE_DENOISE_LATENTS, CANVAS_INPAINT_GRAPH, CANVAS_OUTPAINT_GRAPH, - CANVAS_COHERENCE_DENOISE_LATENTS, CLIP_SKIP, LORA_LOADER, MAIN_MODEL_LOADER, - METADATA_ACCUMULATOR, NEGATIVE_CONDITIONING, POSITIVE_CONDITIONING, } from './constants'; +import { upsertMetadata } from './metadata'; export const addLoRAsToGraph = ( state: RootState, @@ -33,29 +33,29 @@ export const addLoRAsToGraph = ( const { loras } = state.lora; const loraCount = size(loras); - const metadataAccumulator = graph.nodes[METADATA_ACCUMULATOR] as - | MetadataAccumulatorInvocation - | undefined; - if (loraCount > 0) { - // Remove modelLoaderNodeId unet connection to feed it to LoRAs - graph.edges = graph.edges.filter( - (e) => - !( - e.source.node_id === modelLoaderNodeId && - ['unet'].includes(e.source.field) - ) - ); - // Remove CLIP_SKIP connections to conditionings to feed it through LoRAs - graph.edges = graph.edges.filter( - (e) => - !(e.source.node_id === CLIP_SKIP && ['clip'].includes(e.source.field)) - ); + if (loraCount === 0) { + return; } + // Remove modelLoaderNodeId unet connection to feed it to LoRAs + graph.edges = graph.edges.filter( + (e) => + !( + e.source.node_id === modelLoaderNodeId && + ['unet'].includes(e.source.field) + ) + ); + // Remove CLIP_SKIP connections to conditionings to feed it through LoRAs + graph.edges = graph.edges.filter( + (e) => + !(e.source.node_id === CLIP_SKIP && ['clip'].includes(e.source.field)) + ); + // we need to remember the last lora so we can chain from it let lastLoraNodeId = ''; let currentLoraIndex = 0; + const loraMetadata: CoreMetadataInvocation['loras'] = []; forEach(loras, (lora) => { const { model_name, base_model, weight } = lora; @@ -69,13 +69,10 @@ export const addLoRAsToGraph = ( weight, }; - // add the lora to the metadata accumulator - if (metadataAccumulator?.loras) { - metadataAccumulator.loras.push({ - lora: { model_name, base_model }, - weight, - }); - } + loraMetadata.push({ + lora: { model_name, base_model }, + weight, + }); // add to graph graph.nodes[currentLoraNodeId] = loraLoaderNode; @@ -182,4 +179,6 @@ export const addLoRAsToGraph = ( lastLoraNodeId = currentLoraNodeId; currentLoraIndex += 1; }); + + upsertMetadata(graph, { loras: loraMetadata }); }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSDXLLoRAstoGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSDXLLoRAstoGraph.ts index cb052984d4..04841f0def 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSDXLLoRAstoGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSDXLLoRAstoGraph.ts @@ -1,14 +1,14 @@ import { RootState } from 'app/store/store'; -import { NonNullableGraph } from 'features/nodes/types/types'; -import { forEach, size } from 'lodash-es'; import { - MetadataAccumulatorInvocation, - SDXLLoraLoaderInvocation, -} from 'services/api/types'; + LoRAMetadataItem, + NonNullableGraph, + zLoRAMetadataItem, +} from 'features/nodes/types/types'; +import { forEach, size } from 'lodash-es'; +import { SDXLLoraLoaderInvocation } from 'services/api/types'; import { CANVAS_COHERENCE_DENOISE_LATENTS, LORA_LOADER, - METADATA_ACCUMULATOR, NEGATIVE_CONDITIONING, POSITIVE_CONDITIONING, SDXL_CANVAS_INPAINT_GRAPH, @@ -17,6 +17,7 @@ import { SDXL_REFINER_INPAINT_CREATE_MASK, SEAMLESS, } from './constants'; +import { upsertMetadata } from './metadata'; export const addSDXLLoRAsToGraph = ( state: RootState, @@ -34,9 +35,12 @@ export const addSDXLLoRAsToGraph = ( const { loras } = state.lora; const loraCount = size(loras); - const metadataAccumulator = graph.nodes[METADATA_ACCUMULATOR] as - | MetadataAccumulatorInvocation - | undefined; + + if (loraCount === 0) { + return; + } + + const loraMetadata: LoRAMetadataItem[] = []; // Handle Seamless Plugs const unetLoaderId = modelLoaderNodeId; @@ -47,22 +51,17 @@ export const addSDXLLoRAsToGraph = ( clipLoaderId = SDXL_MODEL_LOADER; } - if (loraCount > 0) { - // Remove modelLoaderNodeId unet/clip/clip2 connections to feed it to LoRAs - graph.edges = graph.edges.filter( - (e) => - !( - e.source.node_id === unetLoaderId && ['unet'].includes(e.source.field) - ) && - !( - e.source.node_id === clipLoaderId && ['clip'].includes(e.source.field) - ) && - !( - e.source.node_id === clipLoaderId && - ['clip2'].includes(e.source.field) - ) - ); - } + // Remove modelLoaderNodeId unet/clip/clip2 connections to feed it to LoRAs + graph.edges = graph.edges.filter( + (e) => + !( + e.source.node_id === unetLoaderId && ['unet'].includes(e.source.field) + ) && + !( + e.source.node_id === clipLoaderId && ['clip'].includes(e.source.field) + ) && + !(e.source.node_id === clipLoaderId && ['clip2'].includes(e.source.field)) + ); // we need to remember the last lora so we can chain from it let lastLoraNodeId = ''; @@ -80,16 +79,12 @@ export const addSDXLLoRAsToGraph = ( weight, }; - // add the lora to the metadata accumulator - if (metadataAccumulator) { - if (!metadataAccumulator.loras) { - metadataAccumulator.loras = []; - } - metadataAccumulator.loras.push({ + loraMetadata.push( + zLoRAMetadataItem.parse({ lora: { model_name, base_model }, weight, - }); - } + }) + ); // add to graph graph.nodes[currentLoraNodeId] = loraLoaderNode; @@ -242,4 +237,6 @@ export const addSDXLLoRAsToGraph = ( lastLoraNodeId = currentLoraNodeId; currentLoraIndex += 1; }); + + upsertMetadata(graph, { loras: loraMetadata }); }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSDXLRefinerToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSDXLRefinerToGraph.ts index a6ee6a091d..136263f63e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSDXLRefinerToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSDXLRefinerToGraph.ts @@ -2,7 +2,6 @@ import { RootState } from 'app/store/store'; import { CreateDenoiseMaskInvocation, ImageDTO, - MetadataAccumulatorInvocation, SeamlessModeInvocation, } from 'services/api/types'; import { NonNullableGraph } from '../../types/types'; @@ -12,7 +11,6 @@ import { LATENTS_TO_IMAGE, MASK_COMBINE, MASK_RESIZE_UP, - METADATA_ACCUMULATOR, SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH, SDXL_CANVAS_INPAINT_GRAPH, SDXL_CANVAS_OUTPAINT_GRAPH, @@ -26,6 +24,7 @@ import { SDXL_REFINER_SEAMLESS, } from './constants'; import { buildSDXLStylePrompts } from './helpers/craftSDXLStylePrompt'; +import { upsertMetadata } from './metadata'; export const addSDXLRefinerToGraph = ( state: RootState, @@ -58,21 +57,15 @@ export const addSDXLRefinerToGraph = ( return; } - const metadataAccumulator = graph.nodes[METADATA_ACCUMULATOR] as - | MetadataAccumulatorInvocation - | undefined; - - if (metadataAccumulator) { - metadataAccumulator.refiner_model = refinerModel; - metadataAccumulator.refiner_positive_aesthetic_score = - refinerPositiveAestheticScore; - metadataAccumulator.refiner_negative_aesthetic_score = - refinerNegativeAestheticScore; - metadataAccumulator.refiner_cfg_scale = refinerCFGScale; - metadataAccumulator.refiner_scheduler = refinerScheduler; - metadataAccumulator.refiner_start = refinerStart; - metadataAccumulator.refiner_steps = refinerSteps; - } + upsertMetadata(graph, { + refiner_model: refinerModel, + refiner_positive_aesthetic_score: refinerPositiveAestheticScore, + refiner_negative_aesthetic_score: refinerNegativeAestheticScore, + refiner_cfg_scale: refinerCFGScale, + refiner_scheduler: refinerScheduler, + refiner_start: refinerStart, + refiner_steps: refinerSteps, + }); const modelLoaderId = modelLoaderNodeId ? modelLoaderNodeId diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSaveImageNode.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSaveImageNode.ts index d5a6addf8a..79aace8f62 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSaveImageNode.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSaveImageNode.ts @@ -1,19 +1,15 @@ +import { RootState } from 'app/store/store'; import { NonNullableGraph } from 'features/nodes/types/types'; +import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; +import { SaveImageInvocation } from 'services/api/types'; import { CANVAS_OUTPUT, LATENTS_TO_IMAGE, LATENTS_TO_IMAGE_HRF, - METADATA_ACCUMULATOR, NSFW_CHECKER, SAVE_IMAGE, WATERMARKER, } from './constants'; -import { - MetadataAccumulatorInvocation, - SaveImageInvocation, -} from 'services/api/types'; -import { RootState } from 'app/store/store'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; /** * Set the `use_cache` field on the linear/canvas graph's final image output node to False. @@ -37,23 +33,6 @@ export const addSaveImageNode = ( graph.nodes[SAVE_IMAGE] = saveImageNode; - const metadataAccumulator = graph.nodes[METADATA_ACCUMULATOR] as - | MetadataAccumulatorInvocation - | undefined; - - if (metadataAccumulator) { - graph.edges.push({ - source: { - node_id: METADATA_ACCUMULATOR, - field: 'metadata', - }, - destination: { - node_id: SAVE_IMAGE, - field: 'metadata', - }, - }); - } - const destination = { node_id: SAVE_IMAGE, field: 'image', diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSeamlessToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSeamlessToLinearGraph.ts index bdbaacd384..ba341a8a3d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSeamlessToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addSeamlessToLinearGraph.ts @@ -1,6 +1,7 @@ import { RootState } from 'app/store/store'; import { SeamlessModeInvocation } from 'services/api/types'; import { NonNullableGraph } from '../../types/types'; +import { upsertMetadata } from './metadata'; import { CANVAS_COHERENCE_DENOISE_LATENTS, CANVAS_INPAINT_GRAPH, @@ -31,6 +32,17 @@ export const addSeamlessToLinearGraph = ( seamless_y: seamlessYAxis, } as SeamlessModeInvocation; + if (seamlessXAxis) { + upsertMetadata(graph, { + seamless_x: seamlessXAxis, + }); + } + if (seamlessYAxis) { + upsertMetadata(graph, { + seamless_y: seamlessYAxis, + }); + } + let denoisingNodeId = DENOISE_LATENTS; if ( diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addT2IAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addT2IAdapterToLinearGraph.ts index 9511475bb3..71c2aaeede 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addT2IAdapterToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addT2IAdapterToLinearGraph.ts @@ -3,15 +3,15 @@ import { selectValidT2IAdapters } from 'features/controlAdapters/store/controlAd import { omit } from 'lodash-es'; import { CollectInvocation, - MetadataAccumulatorInvocation, + CoreMetadataInvocation, T2IAdapterInvocation, } from 'services/api/types'; import { NonNullableGraph, T2IAdapterField } from '../../types/types'; import { CANVAS_COHERENCE_DENOISE_LATENTS, - METADATA_ACCUMULATOR, T2I_ADAPTER_COLLECT, } from './constants'; +import { upsertMetadata } from './metadata'; export const addT2IAdaptersToLinearGraph = ( state: RootState, @@ -22,10 +22,6 @@ export const addT2IAdaptersToLinearGraph = ( (ca) => ca.model?.base_model === state.generation.model?.base_model ); - const metadataAccumulator = graph.nodes[METADATA_ACCUMULATOR] as - | MetadataAccumulatorInvocation - | undefined; - if (validT2IAdapters.length) { // Even though denoise_latents' control input is polymorphic, keep it simple and always use a collect const t2iAdapterCollectNode: CollectInvocation = { @@ -51,6 +47,7 @@ export const addT2IAdaptersToLinearGraph = ( }, }); } + const t2iAdapterMetdata: CoreMetadataInvocation['t2iAdapters'] = []; validT2IAdapters.forEach((t2iAdapter) => { if (!t2iAdapter.model) { @@ -96,15 +93,13 @@ export const addT2IAdaptersToLinearGraph = ( graph.nodes[t2iAdapterNode.id] = t2iAdapterNode as T2IAdapterInvocation; - if (metadataAccumulator?.t2iAdapters) { - // metadata accumulator only needs a control field - not the whole node - // extract what we need and add to the accumulator - const t2iAdapterField = omit(t2iAdapterNode, [ + t2iAdapterMetdata.push( + omit(t2iAdapterNode, [ 'id', 'type', - ]) as T2IAdapterField; - metadataAccumulator.t2iAdapters.push(t2iAdapterField); - } + 'is_intermediate', + ]) as T2IAdapterField + ); graph.edges.push({ source: { node_id: t2iAdapterNode.id, field: 't2i_adapter' }, @@ -114,5 +109,7 @@ export const addT2IAdaptersToLinearGraph = ( }, }); }); + + upsertMetadata(graph, { t2iAdapters: t2iAdapterMetdata }); } }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addVAEToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addVAEToGraph.ts index 696c8afff2..f049a89e36 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addVAEToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addVAEToGraph.ts @@ -1,6 +1,5 @@ import { RootState } from 'app/store/store'; import { NonNullableGraph } from 'features/nodes/types/types'; -import { MetadataAccumulatorInvocation } from 'services/api/types'; import { CANVAS_COHERENCE_INPAINT_CREATE_MASK, CANVAS_IMAGE_TO_IMAGE_GRAPH, @@ -14,7 +13,6 @@ import { INPAINT_IMAGE, LATENTS_TO_IMAGE, MAIN_MODEL_LOADER, - METADATA_ACCUMULATOR, ONNX_MODEL_LOADER, SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH, SDXL_CANVAS_INPAINT_GRAPH, @@ -26,6 +24,7 @@ import { TEXT_TO_IMAGE_GRAPH, VAE_LOADER, } from './constants'; +import { upsertMetadata } from './metadata'; export const addVAEToGraph = ( state: RootState, @@ -41,9 +40,6 @@ export const addVAEToGraph = ( ); const isAutoVae = !vae; - const metadataAccumulator = graph.nodes[METADATA_ACCUMULATOR] as - | MetadataAccumulatorInvocation - | undefined; if (!isAutoVae) { graph.nodes[VAE_LOADER] = { @@ -181,7 +177,7 @@ export const addVAEToGraph = ( } } - if (vae && metadataAccumulator) { - metadataAccumulator.vae = vae; + if (vae) { + upsertMetadata(graph, { vae }); } }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addWatermarkerToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addWatermarkerToGraph.ts index 4e515906b6..c43437e4fc 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addWatermarkerToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addWatermarkerToGraph.ts @@ -5,14 +5,8 @@ import { ImageNSFWBlurInvocation, ImageWatermarkInvocation, LatentsToImageInvocation, - MetadataAccumulatorInvocation, } from 'services/api/types'; -import { - LATENTS_TO_IMAGE, - METADATA_ACCUMULATOR, - NSFW_CHECKER, - WATERMARKER, -} from './constants'; +import { LATENTS_TO_IMAGE, NSFW_CHECKER, WATERMARKER } from './constants'; export const addWatermarkerToGraph = ( state: RootState, @@ -32,10 +26,6 @@ export const addWatermarkerToGraph = ( | ImageNSFWBlurInvocation | undefined; - const metadataAccumulator = graph.nodes[METADATA_ACCUMULATOR] as - | MetadataAccumulatorInvocation - | undefined; - if (!nodeToAddTo) { // something has gone terribly awry return; @@ -80,17 +70,4 @@ export const addWatermarkerToGraph = ( }, }); } - - if (metadataAccumulator) { - graph.edges.push({ - source: { - node_id: METADATA_ACCUMULATOR, - field: 'metadata', - }, - destination: { - node_id: WATERMARKER, - field: 'metadata', - }, - }); - } }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildAdHocUpscaleGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildAdHocUpscaleGraph.ts index c612e88598..46e415a886 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildAdHocUpscaleGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildAdHocUpscaleGraph.ts @@ -1,12 +1,13 @@ +import { BoardId } from 'features/gallery/store/types'; import { NonNullableGraph } from 'features/nodes/types/types'; import { ESRGANModelName } from 'features/parameters/store/postprocessingSlice'; import { - Graph, ESRGANInvocation, + Graph, SaveImageInvocation, } from 'services/api/types'; import { REALESRGAN as ESRGAN, SAVE_IMAGE } from './constants'; -import { BoardId } from 'features/gallery/store/types'; +import { addCoreMetadataNode } from './metadata'; type Arg = { image_name: string; @@ -55,5 +56,9 @@ export const buildAdHocUpscaleGraph = ({ ], }; + addCoreMetadataNode(graph, { + esrgan_model: esrganModelName, + }); + return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasImageToImageGraph.ts index 59bdf669e6..9d957c3a4a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasImageToImageGraph.ts @@ -20,12 +20,12 @@ import { IMG2IMG_RESIZE, LATENTS_TO_IMAGE, MAIN_MODEL_LOADER, - METADATA_ACCUMULATOR, NEGATIVE_CONDITIONING, NOISE, POSITIVE_CONDITIONING, SEAMLESS, } from './constants'; +import { addCoreMetadataNode } from './metadata'; /** * Builds the Canvas tab's Image to Image graph. @@ -308,10 +308,7 @@ export const buildCanvasImageToImageGraph = ( }); } - // add metadata accumulator, which is only mostly populated - some fields are added later - graph.nodes[METADATA_ACCUMULATOR] = { - id: METADATA_ACCUMULATOR, - type: 'metadata_accumulator', + addCoreMetadataNode(graph, { generation_mode: 'img2img', cfg_scale, width: !isUsingScaledDimensions ? width : scaledBoundingBoxDimensions.width, @@ -325,15 +322,10 @@ export const buildCanvasImageToImageGraph = ( steps, rand_device: use_cpu ? 'cpu' : 'cuda', scheduler, - vae: undefined, // option; set in addVAEToGraph - controlnets: [], // populated in addControlNetToLinearGraph - loras: [], // populated in addLoRAsToGraph - ipAdapters: [], // populated in addIPAdapterToLinearGraph - t2iAdapters: [], clip_skip: clipSkip, strength, init_image: initialImage.image_name, - }; + }); // Add Seamless To Graph if (seamlessXAxis || seamlessYAxis) { diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLImageToImageGraph.ts index b9c0c9eff3..c1ecde5395 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLImageToImageGraph.ts @@ -16,7 +16,6 @@ import { IMAGE_TO_LATENTS, IMG2IMG_RESIZE, LATENTS_TO_IMAGE, - METADATA_ACCUMULATOR, NEGATIVE_CONDITIONING, NOISE, POSITIVE_CONDITIONING, @@ -28,6 +27,7 @@ import { } from './constants'; import { buildSDXLStylePrompts } from './helpers/craftSDXLStylePrompt'; import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; +import { addCoreMetadataNode } from './metadata'; /** * Builds the Canvas tab's Image to Image graph. @@ -319,10 +319,7 @@ export const buildCanvasSDXLImageToImageGraph = ( }); } - // add metadata accumulator, which is only mostly populated - some fields are added later - graph.nodes[METADATA_ACCUMULATOR] = { - id: METADATA_ACCUMULATOR, - type: 'metadata_accumulator', + addCoreMetadataNode(graph, { generation_mode: 'img2img', cfg_scale, width: !isUsingScaledDimensions ? width : scaledBoundingBoxDimensions.width, @@ -336,24 +333,8 @@ export const buildCanvasSDXLImageToImageGraph = ( steps, rand_device: use_cpu ? 'cpu' : 'cuda', scheduler, - vae: undefined, // option; set in addVAEToGraph - controlnets: [], // populated in addControlNetToLinearGraph - loras: [], // populated in addLoRAsToGraph - ipAdapters: [], // populated in addIPAdapterToLinearGraph - t2iAdapters: [], strength, init_image: initialImage.image_name, - }; - - graph.edges.push({ - source: { - node_id: METADATA_ACCUMULATOR, - field: 'metadata', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'metadata', - }, }); // Add Seamless To Graph diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLTextToImageGraph.ts index df636669dc..e43891eba4 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasSDXLTextToImageGraph.ts @@ -18,7 +18,6 @@ import { addWatermarkerToGraph } from './addWatermarkerToGraph'; import { CANVAS_OUTPUT, LATENTS_TO_IMAGE, - METADATA_ACCUMULATOR, NEGATIVE_CONDITIONING, NOISE, ONNX_MODEL_LOADER, @@ -30,6 +29,7 @@ import { SEAMLESS, } from './constants'; import { buildSDXLStylePrompts } from './helpers/craftSDXLStylePrompt'; +import { addCoreMetadataNode } from './metadata'; /** * Builds the Canvas tab's Text to Image graph. @@ -301,10 +301,7 @@ export const buildCanvasSDXLTextToImageGraph = ( }); } - // add metadata accumulator, which is only mostly populated - some fields are added later - graph.nodes[METADATA_ACCUMULATOR] = { - id: METADATA_ACCUMULATOR, - type: 'metadata_accumulator', + addCoreMetadataNode(graph, { generation_mode: 'txt2img', cfg_scale, width: !isUsingScaledDimensions ? width : scaledBoundingBoxDimensions.width, @@ -318,22 +315,6 @@ export const buildCanvasSDXLTextToImageGraph = ( steps, rand_device: use_cpu ? 'cpu' : 'cuda', scheduler, - vae: undefined, // option; set in addVAEToGraph - controlnets: [], // populated in addControlNetToLinearGraph - loras: [], // populated in addLoRAsToGraph - ipAdapters: [], // populated in addIPAdapterToLinearGraph - t2iAdapters: [], - }; - - graph.edges.push({ - source: { - node_id: METADATA_ACCUMULATOR, - field: 'metadata', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'metadata', - }, }); // Add Seamless To Graph diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasTextToImageGraph.ts index 38f11f14ac..6e48c14086 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasTextToImageGraph.ts @@ -21,13 +21,13 @@ import { DENOISE_LATENTS, LATENTS_TO_IMAGE, MAIN_MODEL_LOADER, - METADATA_ACCUMULATOR, NEGATIVE_CONDITIONING, NOISE, ONNX_MODEL_LOADER, POSITIVE_CONDITIONING, SEAMLESS, } from './constants'; +import { addCoreMetadataNode } from './metadata'; /** * Builds the Canvas tab's Text to Image graph. @@ -289,10 +289,7 @@ export const buildCanvasTextToImageGraph = ( }); } - // add metadata accumulator, which is only mostly populated - some fields are added later - graph.nodes[METADATA_ACCUMULATOR] = { - id: METADATA_ACCUMULATOR, - type: 'metadata_accumulator', + addCoreMetadataNode(graph, { generation_mode: 'txt2img', cfg_scale, width: !isUsingScaledDimensions ? width : scaledBoundingBoxDimensions.width, @@ -306,23 +303,7 @@ export const buildCanvasTextToImageGraph = ( steps, rand_device: use_cpu ? 'cpu' : 'cuda', scheduler, - vae: undefined, // option; set in addVAEToGraph - controlnets: [], // populated in addControlNetToLinearGraph - loras: [], // populated in addLoRAsToGraph - ipAdapters: [], // populated in addIPAdapterToLinearGraph - t2iAdapters: [], clip_skip: clipSkip, - }; - - graph.edges.push({ - source: { - node_id: METADATA_ACCUMULATOR, - field: 'metadata', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'metadata', - }, }); // Add Seamless To Graph diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearBatchConfig.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearBatchConfig.ts index 9c25ee3b8f..313826452c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearBatchConfig.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearBatchConfig.ts @@ -7,10 +7,12 @@ import { components } from 'services/api/schema'; import { Batch, BatchConfig } from 'services/api/types'; import { CANVAS_COHERENCE_NOISE, + METADATA, METADATA_ACCUMULATOR, NOISE, POSITIVE_CONDITIONING, } from './constants'; +import { removeMetadata } from './metadata'; export const prepareLinearUIBatch = ( state: RootState, @@ -24,7 +26,6 @@ export const prepareLinearUIBatch = ( const data: Batch['data'] = []; if (prompts.length === 1) { - unset(graph.nodes[METADATA_ACCUMULATOR], 'seed'); const seeds = generateSeeds({ count: iterations, start: shouldRandomizeSeed ? undefined : seed, @@ -40,13 +41,13 @@ export const prepareLinearUIBatch = ( }); } - if (graph.nodes[METADATA_ACCUMULATOR]) { - zipped.push({ - node_path: METADATA_ACCUMULATOR, - field_name: 'seed', - items: seeds, - }); - } + // add to metadata + removeMetadata(graph, 'seed'); + zipped.push({ + node_path: METADATA, + field_name: 'seed', + items: seeds, + }); if (graph.nodes[CANVAS_COHERENCE_NOISE]) { zipped.push({ @@ -77,13 +78,13 @@ export const prepareLinearUIBatch = ( }); } - if (graph.nodes[METADATA_ACCUMULATOR]) { - firstBatchDatumList.push({ - node_path: METADATA_ACCUMULATOR, - field_name: 'seed', - items: seeds, - }); - } + // add to metadata + removeMetadata(graph, 'seed'); + firstBatchDatumList.push({ + node_path: METADATA, + field_name: 'seed', + items: seeds, + }); if (graph.nodes[CANVAS_COHERENCE_NOISE]) { firstBatchDatumList.push({ @@ -106,13 +107,15 @@ export const prepareLinearUIBatch = ( items: seeds, }); } - if (graph.nodes[METADATA_ACCUMULATOR]) { - secondBatchDatumList.push({ - node_path: METADATA_ACCUMULATOR, - field_name: 'seed', - items: seeds, - }); - } + + // add to metadata + removeMetadata(graph, 'seed'); + secondBatchDatumList.push({ + node_path: METADATA, + field_name: 'seed', + items: seeds, + }); + if (graph.nodes[CANVAS_COHERENCE_NOISE]) { secondBatchDatumList.push({ node_path: CANVAS_COHERENCE_NOISE, @@ -137,13 +140,13 @@ export const prepareLinearUIBatch = ( }); } - if (graph.nodes[METADATA_ACCUMULATOR]) { - firstBatchDatumList.push({ - node_path: METADATA_ACCUMULATOR, - field_name: 'positive_prompt', - items: extendedPrompts, - }); - } + // add to metadata + removeMetadata(graph, 'positive_prompt'); + firstBatchDatumList.push({ + node_path: METADATA, + field_name: 'positive_prompt', + items: extendedPrompts, + }); if (shouldConcatSDXLStylePrompt && model?.base_model === 'sdxl') { unset(graph.nodes[METADATA_ACCUMULATOR], 'positive_style_prompt'); @@ -160,13 +163,13 @@ export const prepareLinearUIBatch = ( }); } - if (graph.nodes[METADATA_ACCUMULATOR]) { - firstBatchDatumList.push({ - node_path: METADATA_ACCUMULATOR, - field_name: 'positive_style_prompt', - items: stylePrompts, - }); - } + // add to metadata + removeMetadata(graph, 'positive_style_prompt'); + firstBatchDatumList.push({ + node_path: METADATA, + field_name: 'positive_style_prompt', + items: extendedPrompts, + }); } data.push(firstBatchDatumList); diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearImageToImageGraph.ts index 0eeba988f2..3b13c746c9 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearImageToImageGraph.ts @@ -21,13 +21,13 @@ import { IMAGE_TO_LATENTS, LATENTS_TO_IMAGE, MAIN_MODEL_LOADER, - METADATA_ACCUMULATOR, NEGATIVE_CONDITIONING, NOISE, POSITIVE_CONDITIONING, RESIZE, SEAMLESS, } from './constants'; +import { addCoreMetadataNode } from './metadata'; /** * Builds the Image to Image tab graph. @@ -311,10 +311,7 @@ export const buildLinearImageToImageGraph = ( }); } - // add metadata accumulator, which is only mostly populated - some fields are added later - graph.nodes[METADATA_ACCUMULATOR] = { - id: METADATA_ACCUMULATOR, - type: 'metadata_accumulator', + addCoreMetadataNode(graph, { generation_mode: 'img2img', cfg_scale, height, @@ -326,25 +323,9 @@ export const buildLinearImageToImageGraph = ( steps, rand_device: use_cpu ? 'cpu' : 'cuda', scheduler, - vae: undefined, // option; set in addVAEToGraph - controlnets: [], // populated in addControlNetToLinearGraph - loras: [], // populated in addLoRAsToGraph - ipAdapters: [], // populated in addIPAdapterToLinearGraph - t2iAdapters: [], // populated in addT2IAdapterToLinearGraph clip_skip: clipSkip, strength, init_image: initialImage.imageName, - }; - - graph.edges.push({ - source: { - node_id: METADATA_ACCUMULATOR, - field: 'metadata', - }, - destination: { - node_id: LATENTS_TO_IMAGE, - field: 'metadata', - }, }); // Add Seamless To Graph diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearSDXLImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearSDXLImageToImageGraph.ts index f818768fb5..54f8e05d21 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearSDXLImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearSDXLImageToImageGraph.ts @@ -18,7 +18,6 @@ import { addWatermarkerToGraph } from './addWatermarkerToGraph'; import { IMAGE_TO_LATENTS, LATENTS_TO_IMAGE, - METADATA_ACCUMULATOR, NEGATIVE_CONDITIONING, NOISE, POSITIVE_CONDITIONING, @@ -30,6 +29,7 @@ import { SEAMLESS, } from './constants'; import { buildSDXLStylePrompts } from './helpers/craftSDXLStylePrompt'; +import { addCoreMetadataNode } from './metadata'; /** * Builds the Image to Image tab graph. @@ -331,10 +331,7 @@ export const buildLinearSDXLImageToImageGraph = ( }); } - // add metadata accumulator, which is only mostly populated - some fields are added later - graph.nodes[METADATA_ACCUMULATOR] = { - id: METADATA_ACCUMULATOR, - type: 'metadata_accumulator', + addCoreMetadataNode(graph, { generation_mode: 'sdxl_img2img', cfg_scale, height, @@ -346,26 +343,10 @@ export const buildLinearSDXLImageToImageGraph = ( steps, rand_device: use_cpu ? 'cpu' : 'cuda', scheduler, - vae: undefined, - controlnets: [], - loras: [], - ipAdapters: [], - t2iAdapters: [], - strength: strength, + strength, init_image: initialImage.imageName, positive_style_prompt: positiveStylePrompt, negative_style_prompt: negativeStylePrompt, - }; - - graph.edges.push({ - source: { - node_id: METADATA_ACCUMULATOR, - field: 'metadata', - }, - destination: { - node_id: LATENTS_TO_IMAGE, - field: 'metadata', - }, }); // Add Seamless To Graph diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearSDXLTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearSDXLTextToImageGraph.ts index 4cb90678c3..37fbbf7f43 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearSDXLTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearSDXLTextToImageGraph.ts @@ -11,9 +11,9 @@ import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; import { addVAEToGraph } from './addVAEToGraph'; import { addWatermarkerToGraph } from './addWatermarkerToGraph'; +import { addCoreMetadataNode } from './metadata'; import { LATENTS_TO_IMAGE, - METADATA_ACCUMULATOR, NEGATIVE_CONDITIONING, NOISE, POSITIVE_CONDITIONING, @@ -225,10 +225,7 @@ export const buildLinearSDXLTextToImageGraph = ( ], }; - // add metadata accumulator, which is only mostly populated - some fields are added later - graph.nodes[METADATA_ACCUMULATOR] = { - id: METADATA_ACCUMULATOR, - type: 'metadata_accumulator', + addCoreMetadataNode(graph, { generation_mode: 'sdxl_txt2img', cfg_scale, height, @@ -240,24 +237,8 @@ export const buildLinearSDXLTextToImageGraph = ( steps, rand_device: use_cpu ? 'cpu' : 'cuda', scheduler, - vae: undefined, - controlnets: [], - loras: [], - ipAdapters: [], - t2iAdapters: [], positive_style_prompt: positiveStylePrompt, negative_style_prompt: negativeStylePrompt, - }; - - graph.edges.push({ - source: { - node_id: METADATA_ACCUMULATOR, - field: 'metadata', - }, - destination: { - node_id: LATENTS_TO_IMAGE, - field: 'metadata', - }, }); // Add Seamless To Graph diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearTextToImageGraph.ts index e692e12fa4..8e0143f180 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearTextToImageGraph.ts @@ -15,12 +15,12 @@ import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; import { addVAEToGraph } from './addVAEToGraph'; import { addWatermarkerToGraph } from './addWatermarkerToGraph'; +import { addCoreMetadataNode } from './metadata'; import { CLIP_SKIP, DENOISE_LATENTS, LATENTS_TO_IMAGE, MAIN_MODEL_LOADER, - METADATA_ACCUMULATOR, NEGATIVE_CONDITIONING, NOISE, ONNX_MODEL_LOADER, @@ -48,10 +48,6 @@ export const buildLinearTextToImageGraph = ( seamlessXAxis, seamlessYAxis, seed, - hrfWidth, - hrfHeight, - hrfStrength, - hrfEnabled: hrfEnabled, } = state.generation; const use_cpu = shouldUseCpuNoise; @@ -238,10 +234,7 @@ export const buildLinearTextToImageGraph = ( ], }; - // add metadata accumulator, which is only mostly populated - some fields are added later - graph.nodes[METADATA_ACCUMULATOR] = { - id: METADATA_ACCUMULATOR, - type: 'metadata_accumulator', + addCoreMetadataNode(graph, { generation_mode: 'txt2img', cfg_scale, height, @@ -253,26 +246,7 @@ export const buildLinearTextToImageGraph = ( steps, rand_device: use_cpu ? 'cpu' : 'cuda', scheduler, - vae: undefined, // option; set in addVAEToGraph - controlnets: [], // populated in addControlNetToLinearGraph - loras: [], // populated in addLoRAsToGraph - ipAdapters: [], // populated in addIPAdapterToLinearGraph - t2iAdapters: [], // populated in addT2IAdapterToLinearGraph clip_skip: clipSkip, - hrf_width: hrfEnabled ? hrfWidth : undefined, - hrf_height: hrfEnabled ? hrfHeight : undefined, - hrf_strength: hrfEnabled ? hrfStrength : undefined, - }; - - graph.edges.push({ - source: { - node_id: METADATA_ACCUMULATOR, - field: 'metadata', - }, - destination: { - node_id: LATENTS_TO_IMAGE, - field: 'metadata', - }, }); // Add Seamless To Graph diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildNodesGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildNodesGraph.ts index 7be06ac110..4437e14f66 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildNodesGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildNodesGraph.ts @@ -35,7 +35,7 @@ export const buildNodesGraph = (nodesState: NodesState): Graph => { const { nodes, edges } = nodesState; const filteredNodes = nodes.filter(isInvocationNode); - const workflowJSON = JSON.stringify(buildWorkflow(nodesState)); + // const workflowJSON = JSON.stringify(buildWorkflow(nodesState)); // Reduce the node editor nodes into invocation graph nodes const parsedNodes = filteredNodes.reduce>( @@ -68,7 +68,8 @@ export const buildNodesGraph = (nodesState: NodesState): Graph => { if (embedWorkflow) { // add the workflow to the node - Object.assign(graphNode, { workflow: workflowJSON }); + // Object.assign(graphNode, { workflow: workflowJSON }); + Object.assign(graphNode, { workflow: buildWorkflow(nodesState) }); } // Add it to the nodes object diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/constants.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/constants.ts index 7d547d09e6..e0dc52063b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/constants.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/constants.ts @@ -56,7 +56,15 @@ export const IP_ADAPTER = 'ip_adapter'; export const DYNAMIC_PROMPT = 'dynamic_prompt'; export const IMAGE_COLLECTION = 'image_collection'; export const IMAGE_COLLECTION_ITERATE = 'image_collection_iterate'; +export const METADATA = 'core_metadata'; +export const BATCH_METADATA = 'batch_metadata'; +export const BATCH_METADATA_COLLECT = 'batch_metadata_collect'; +export const BATCH_SEED = 'batch_seed'; +export const BATCH_PROMPT = 'batch_prompt'; +export const BATCH_STYLE_PROMPT = 'batch_style_prompt'; +export const METADATA_COLLECT = 'metadata_collect'; export const METADATA_ACCUMULATOR = 'metadata_accumulator'; +export const MERGE_METADATA = 'merge_metadata'; export const REALESRGAN = 'esrgan'; export const DIVIDE = 'divide'; export const SCALE = 'scale_image'; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/metadata.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/metadata.ts new file mode 100644 index 0000000000..547c45addf --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/metadata.ts @@ -0,0 +1,58 @@ +import { NonNullableGraph } from 'features/nodes/types/types'; +import { CoreMetadataInvocation } from 'services/api/types'; +import { JsonObject } from 'type-fest'; +import { METADATA, SAVE_IMAGE } from './constants'; + +export const addCoreMetadataNode = ( + graph: NonNullableGraph, + metadata: Partial | JsonObject +): void => { + graph.nodes[METADATA] = { + id: METADATA, + type: 'core_metadata', + ...metadata, + }; + + graph.edges.push({ + source: { + node_id: METADATA, + field: 'metadata', + }, + destination: { + node_id: SAVE_IMAGE, + field: 'metadata', + }, + }); + + return; +}; + +export const upsertMetadata = ( + graph: NonNullableGraph, + metadata: Partial | JsonObject +): void => { + const metadataNode = graph.nodes[METADATA] as + | CoreMetadataInvocation + | undefined; + + if (!metadataNode) { + return; + } + + Object.assign(metadataNode, metadata); +}; + +export const removeMetadata = ( + graph: NonNullableGraph, + key: keyof CoreMetadataInvocation +): void => { + const metadataNode = graph.nodes[METADATA] as + | CoreMetadataInvocation + | undefined; + + if (!metadataNode) { + return; + } + + delete metadataNode[key]; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts b/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts index 93cd75dd75..7c6f4e638f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts +++ b/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts @@ -4,7 +4,6 @@ import { reduce, startCase } from 'lodash-es'; import { OpenAPIV3_1 } from 'openapi-types'; import { AnyInvocationType } from 'services/events/types'; import { - FieldType, InputFieldTemplate, InvocationSchemaObject, InvocationTemplate, @@ -16,18 +15,11 @@ import { } from '../types/types'; import { buildInputFieldTemplate, getFieldType } from './fieldTemplateBuilders'; -const RESERVED_INPUT_FIELD_NAMES = ['id', 'type', 'metadata', 'use_cache']; +const RESERVED_INPUT_FIELD_NAMES = ['id', 'type', 'use_cache']; const RESERVED_OUTPUT_FIELD_NAMES = ['type']; -const RESERVED_FIELD_TYPES = [ - 'WorkflowField', - 'MetadataField', - 'IsIntermediate', -]; +const RESERVED_FIELD_TYPES = ['IsIntermediate']; -const invocationDenylist: AnyInvocationType[] = [ - 'graph', - 'metadata_accumulator', -]; +const invocationDenylist: AnyInvocationType[] = ['graph']; const isReservedInputField = (nodeType: string, fieldName: string) => { if (RESERVED_INPUT_FIELD_NAMES.includes(fieldName)) { @@ -42,7 +34,7 @@ const isReservedInputField = (nodeType: string, fieldName: string) => { return false; }; -const isReservedFieldType = (fieldType: FieldType) => { +const isReservedFieldType = (fieldType: string) => { if (RESERVED_FIELD_TYPES.includes(fieldType)) { return true; } @@ -86,6 +78,7 @@ export const parseSchema = ( const tags = schema.tags ?? []; const description = schema.description ?? ''; const version = schema.version; + let withWorkflow = false; const inputs = reduce( schema.properties, @@ -112,7 +105,7 @@ export const parseSchema = ( const fieldType = property.ui_type ?? getFieldType(property); - if (!isFieldType(fieldType)) { + if (!fieldType) { logger('nodes').warn( { node: type, @@ -120,11 +113,16 @@ export const parseSchema = ( fieldType, field: parseify(property), }, - 'Skipping unknown input field type' + 'Missing input field type' ); return inputsAccumulator; } + if (fieldType === 'WorkflowField') { + withWorkflow = true; + return inputsAccumulator; + } + if (isReservedFieldType(fieldType)) { logger('nodes').trace( { @@ -133,7 +131,20 @@ export const parseSchema = ( fieldType, field: parseify(property), }, - 'Skipping reserved field type' + `Skipping reserved input field type: ${fieldType}` + ); + return inputsAccumulator; + } + + if (!isFieldType(fieldType)) { + logger('nodes').warn( + { + node: type, + fieldName: propertyName, + fieldType, + field: parseify(property), + }, + `Skipping unknown input field type: ${fieldType}` ); return inputsAccumulator; } @@ -146,7 +157,7 @@ export const parseSchema = ( ); if (!field) { - logger('nodes').debug( + logger('nodes').warn( { node: type, fieldName: propertyName, @@ -248,6 +259,7 @@ export const parseSchema = ( inputs, outputs, useCache, + withWorkflow, }; Object.assign(invocationsAccumulator, { [type]: invocation }); diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts index c8d42d17f6..36c00ee1c9 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/images.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts @@ -1,6 +1,7 @@ import { EntityState, Update } from '@reduxjs/toolkit'; import { fetchBaseQuery } from '@reduxjs/toolkit/dist/query'; import { PatchCollection } from '@reduxjs/toolkit/dist/query/core/buildThunks'; +import { logger } from 'app/logging/logger'; import { ASSETS_CATEGORIES, BoardId, @@ -8,6 +9,7 @@ import { IMAGE_LIMIT, } from 'features/gallery/store/types'; import { + CoreMetadata, ImageMetadataAndWorkflow, zCoreMetadata, } from 'features/nodes/types/types'; @@ -23,7 +25,6 @@ import { ListImagesArgs, OffsetPaginatedResults_ImageDTO_, PostUploadAction, - UnsafeImageMetadata, } from '../types'; import { getCategories, @@ -114,11 +115,24 @@ export const imagesApi = api.injectEndpoints({ ], keepUnusedDataFor: 86400, // 24 hours }), - getImageMetadata: build.query({ + getImageMetadata: build.query({ query: (image_name) => ({ url: `images/i/${image_name}/metadata` }), providesTags: (result, error, image_name) => [ { type: 'ImageMetadata', id: image_name }, ], + transformResponse: ( + response: paths['/api/v1/images/i/{image_name}/metadata']['get']['responses']['200']['content']['application/json'] + ) => { + if (response) { + const result = zCoreMetadata.safeParse(response); + if (result.success) { + return result.data; + } else { + logger('images').warn('Problem parsing metadata'); + } + } + return; + }, keepUnusedDataFor: 86400, // 24 hours }), getImageMetadataFromFile: build.query< diff --git a/invokeai/frontend/web/src/services/api/endpoints/workflows.ts b/invokeai/frontend/web/src/services/api/endpoints/workflows.ts new file mode 100644 index 0000000000..4c69d2e286 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/endpoints/workflows.ts @@ -0,0 +1,31 @@ +import { logger } from 'app/logging/logger'; +import { Workflow, zWorkflow } from 'features/nodes/types/types'; +import { api } from '..'; +import { paths } from '../schema'; + +export const workflowsApi = api.injectEndpoints({ + endpoints: (build) => ({ + getWorkflow: build.query({ + query: (workflow_id) => `workflows/i/${workflow_id}`, + keepUnusedDataFor: 86400, // 24 hours + 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 = zWorkflow.safeParse(response); + if (result.success) { + return result.data; + } else { + logger('images').warn('Problem parsing metadata'); + } + } + return; + }, + }), + }), +}); + +export const { useGetWorkflowQuery } = workflowsApi; diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index f423b2b0ed..b7595b3d52 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -37,6 +37,7 @@ export const tagTypes = [ 'ControlNetModel', 'LoRAModel', 'SDXLRefinerModel', + 'Workflow', ] 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 62f60c1dbc..932891c862 100644 --- a/invokeai/frontend/web/src/services/api/schema.d.ts +++ b/invokeai/frontend/web/src/services/api/schema.d.ts @@ -378,6 +378,13 @@ export type paths = { */ put: operations["cancel_queue_item"]; }; + "/api/v1/workflows/i/{workflow_id}": { + /** + * Get Workflow + * @description Gets a workflow + */ + get: operations["get_workflow"]; + }; }; export type webhooks = Record; @@ -413,17 +420,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * A * @description The first number @@ -572,6 +574,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -583,17 +589,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Width * @description The width of the image @@ -646,17 +647,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description Latents tensor */ latents_a?: components["schemas"]["LatentsField"]; /** @description Latents tensor */ @@ -899,17 +895,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Collection * @description The collection of boolean values @@ -955,17 +946,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Value * @description The boolean value @@ -1033,6 +1019,10 @@ export type components = { * @description Infills transparent areas of an image using OpenCV Inpainting */ 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. @@ -1044,17 +1034,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to infill */ image?: components["schemas"]["ImageField"]; /** @@ -1080,6 +1065,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -1091,17 +1080,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to process */ image?: components["schemas"]["ImageField"]; /** @@ -1167,17 +1151,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * CLIP * @description CLIP (tokenizer, text encoder, LoRAs) and skipped layer count @@ -1229,17 +1208,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Collection Item * @description The item to collect (all inputs must be of the same type) @@ -1294,6 +1268,10 @@ export type components = { * using a mask to only color-correct certain regions of the target image. */ 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. @@ -1305,17 +1283,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to color-correct */ image?: components["schemas"]["ImageField"]; /** @description Reference image for color-correction */ @@ -1377,17 +1350,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * @description The color value * @default { @@ -1410,6 +1378,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -1421,17 +1393,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to process */ image?: components["schemas"]["ImageField"]; /** @@ -1477,17 +1444,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Prompt * @description Prompt to be parsed by Compel to create a conditioning tensor @@ -1522,17 +1484,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Collection * @description The collection of conditioning tensors @@ -1589,17 +1546,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description Conditioning tensor */ conditioning?: components["schemas"]["ConditioningField"]; /** @@ -1628,6 +1580,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -1639,17 +1595,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to process */ image?: components["schemas"]["ImageField"]; /** @@ -1744,17 +1695,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The control image */ image?: components["schemas"]["ImageField"]; /** @description ControlNet model to load */ @@ -1872,26 +1818,33 @@ export type components = { type: "control_output"; }; /** - * CoreMetadata - * @description Core generation metadata for an image generated in InvokeAI. + * Core Metadata + * @description Collects core generation metadata into a MetadataField */ - CoreMetadata: { + CoreMetadataInvocation: { /** - * App Version - * @description The version of InvokeAI used to generate this image - * @default 3.3.0 + * Id + * @description The id of this instance of an invocation. Must be unique among all instances of invocations. */ - app_version?: string; + id: string; + /** + * Is Intermediate + * @description Whether or not this is an intermediate invocation. + * @default false + */ + is_intermediate?: boolean | null; + /** + * Use Cache + * @description Whether or not to use the cache + * @default true + */ + use_cache?: boolean; /** * Generation Mode * @description The generation mode that output this image + * @enum {string} */ - generation_mode?: string | null; - /** - * Created By - * @description The name of the creator of the image - */ - created_by?: string | null; + generation_mode?: "txt2img" | "img2img" | "inpaint" | "outpaint"; /** * Positive Prompt * @description The positive prompt parameter @@ -1937,6 +1890,16 @@ export type components = { * @description The scheduler used for inference */ scheduler?: string | null; + /** + * Seamless X + * @description Whether seamless tiling was used on the X axis + */ + seamless_x?: boolean | null; + /** + * Seamless Y + * @description Whether seamless tiling was used on the Y axis + */ + seamless_y?: boolean | null; /** * Clip Skip * @description The number of skipped CLIP layers @@ -1964,8 +1927,6 @@ export type components = { * @description The LoRAs used for inference */ loras?: components["schemas"]["LoRAMetadataField"][] | null; - /** @description The VAE used for decoding, if the main model's default was not used */ - vae?: components["schemas"]["VAEModelField"] | null; /** * Strength * @description The strength used for latents-to-latents @@ -1976,6 +1937,23 @@ export type components = { * @description The name of the initial image */ init_image?: string | null; + /** @description The VAE used for decoding, if the main model's default was not used */ + vae?: components["schemas"]["VAEModelField"] | null; + /** + * Hrf Width + * @description The high resolution fix height and width multipler. + */ + hrf_width?: number | null; + /** + * Hrf Height + * @description The high resolution fix height and width multipler. + */ + hrf_height?: number | null; + /** + * Hrf Strength + * @description The high resolution fix img2img strength used in the upscale pass. + */ + hrf_strength?: number | null; /** * Positive Style Prompt * @description The positive style prompt parameter @@ -2018,6 +1996,13 @@ export type components = { * @description The start value used for refiner denoising */ refiner_start?: number | null; + /** + * type + * @default core_metadata + * @constant + */ + type: "core_metadata"; + [key: string]: unknown; }; /** * Create Denoise Mask @@ -2035,17 +2020,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description VAE */ vae?: components["schemas"]["VaeField"]; /** @description Image which will be masked */ @@ -2094,6 +2074,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -2105,17 +2089,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to inpaint */ image?: components["schemas"]["ImageField"]; /** @description The mask to use when inpainting */ @@ -2166,17 +2145,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description Positive conditioning tensor */ positive_conditioning?: components["schemas"]["ConditioningField"]; /** @description Negative conditioning tensor */ @@ -2288,17 +2262,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * A * @description The first number @@ -2334,17 +2303,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache - * @default false + * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Prompt * @description The prompt to parse with dynamicprompts @@ -2381,6 +2345,10 @@ export type components = { * @description Upscales an image using RealESRGAN. */ 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. @@ -2392,17 +2360,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The input image */ image?: components["schemas"]["ImageField"]; /** @@ -2475,6 +2438,10 @@ export type components = { * @description Outputs an image with detected face IDs printed on each face. For use with other FaceTools. */ 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. @@ -2486,17 +2453,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description Image to face detect */ image?: components["schemas"]["ImageField"]; /** @@ -2523,6 +2485,10 @@ export type components = { * @description Face mask creation using mediapipe face detection */ 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. @@ -2534,17 +2500,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description Image to face detect */ image?: components["schemas"]["ImageField"]; /** @@ -2621,6 +2582,10 @@ export type components = { * @description Bound, extract, and mask a face from an image using MediaPipe detection */ 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. @@ -2632,17 +2597,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description Image for face detection */ image?: components["schemas"]["ImageField"]; /** @@ -2740,17 +2700,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Collection * @description The collection of float values @@ -2796,17 +2751,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Value * @description The float value @@ -2836,17 +2786,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Start * @description The first value of the range @@ -2888,17 +2833,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Operation * @description The operation to perform @@ -2958,17 +2898,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Value * @description The value to round @@ -3007,7 +2942,7 @@ export type components = { * @description The nodes in this graph */ nodes?: { - [key: string]: components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ONNXPromptInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["ONNXLatentsToImageInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["OnnxModelLoaderInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["MetadataAccumulatorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["ONNXTextToLatentsInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["BlendLatentsInvocation"]; + [key: string]: components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["OnnxModelLoaderInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["ONNXLatentsToImageInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ONNXTextToLatentsInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["ONNXPromptInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["GraphInvocation"]; }; /** * Edges @@ -3044,7 +2979,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["IntegerOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["VaeLoaderOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["ONNXModelLoaderOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["MetadataAccumulatorOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["String2Output"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["ClipSkipInvocationOutput"]; + [key: string]: components["schemas"]["FaceOffOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ONNXModelLoaderOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["String2Output"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["VaeLoaderOutput"] | components["schemas"]["LatentsOutput"]; }; /** * Errors @@ -3084,17 +3019,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The graph to run */ graph?: components["schemas"]["Graph"]; /** @@ -3123,6 +3053,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -3134,17 +3068,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to process */ image?: components["schemas"]["ImageField"]; /** @@ -3218,17 +3147,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Image * @description The IP-Adapter image prompt(s). @@ -3264,17 +3188,21 @@ export type components = { */ type: "ip_adapter"; }; - /** IPAdapterMetadataField */ + /** + * IPAdapterMetadataField + * @description IP Adapter Field, minus the CLIP Vision Encoder model + */ IPAdapterMetadataField: { /** @description The IP-Adapter image prompt. */ image: components["schemas"]["ImageField"]; - /** @description The IP-Adapter model to use. */ + /** @description The IP-Adapter model. */ ip_adapter_model: components["schemas"]["IPAdapterModelField"]; /** * Weight - * @description The weight of the IP-Adapter model + * @description The weight given to the IP-Adapter + * @default 1 */ - weight: number; + weight?: number | number[]; /** * Begin Step Percent * @description When the IP-Adapter is first applied (% of total steps) @@ -3339,6 +3267,10 @@ export type components = { * @description Blurs an image */ 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. @@ -3350,17 +3282,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to blur */ image?: components["schemas"]["ImageField"]; /** @@ -3400,6 +3327,10 @@ export type components = { * @description Gets a channel from an image. */ 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. @@ -3411,17 +3342,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to get the channel from */ image?: components["schemas"]["ImageField"]; /** @@ -3443,6 +3369,10 @@ export type components = { * @description Scale a specific color channel of an image. */ 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. @@ -3454,17 +3384,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to adjust */ image?: components["schemas"]["ImageField"]; /** @@ -3497,6 +3422,10 @@ export type components = { * @description Add or subtract a value from a specific color channel of an image. */ 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. @@ -3508,17 +3437,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to adjust */ image?: components["schemas"]["ImageField"]; /** @@ -3556,17 +3480,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Collection * @description The collection of image values @@ -3601,6 +3520,10 @@ export type components = { * @description Converts an image to a different mode. */ 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. @@ -3612,17 +3535,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to convert */ image?: components["schemas"]["ImageField"]; /** @@ -3644,6 +3562,10 @@ export type components = { * @description Crops an image to a specified box. The box can be outside of the image. */ 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. @@ -3655,17 +3577,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to crop */ image?: components["schemas"]["ImageField"]; /** @@ -3758,6 +3675,11 @@ export type components = { * @description The session ID that generated this image, if it is a generated image. */ session_id?: string | null; + /** + * Workflow Id + * @description The workflow that generated this image. + */ + workflow_id?: string | null; /** * Node Id * @description The node ID that generated this image, if it is a generated image. @@ -3790,6 +3712,10 @@ export type components = { * @description Adjusts the Hue of an image. */ 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. @@ -3801,17 +3727,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to adjust */ image?: components["schemas"]["ImageField"]; /** @@ -3832,6 +3753,10 @@ export type components = { * @description Inverse linear interpolation of all pixels of an image */ 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. @@ -3843,17 +3768,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to lerp */ image?: components["schemas"]["ImageField"]; /** @@ -3891,17 +3811,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to load */ image?: components["schemas"]["ImageField"]; /** @@ -3916,6 +3831,10 @@ export type components = { * @description Linear interpolation of all pixels of an image */ 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. @@ -3927,17 +3846,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to lerp */ image?: components["schemas"]["ImageField"]; /** @@ -3959,27 +3873,15 @@ export type components = { */ type: "img_lerp"; }; - /** - * ImageMetadata - * @description An image's generation metadata - */ - ImageMetadata: { - /** - * Metadata - * @description The image's core metadata, if it was created in the Linear or Canvas UI - */ - metadata?: Record | null; - /** - * Graph - * @description The graph that created the image - */ - graph?: Record | null; - }; /** * Multiply Images * @description Multiplies two images together using `PIL.ImageChops.multiply()`. */ 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. @@ -3991,17 +3893,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The first image to multiply */ image1?: components["schemas"]["ImageField"]; /** @description The second image to multiply */ @@ -4018,6 +3915,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -4029,21 +3930,14 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to check */ image?: components["schemas"]["ImageField"]; - /** @description Optional core metadata to be written to image */ - metadata?: components["schemas"]["CoreMetadata"] | null; /** * type * @default img_nsfw @@ -4080,6 +3974,10 @@ export type components = { * @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. @@ -4091,17 +3989,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The base image */ base_image?: components["schemas"]["ImageField"]; /** @description The image to paste */ @@ -4168,6 +4061,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -4179,17 +4076,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to resize */ image?: components["schemas"]["ImageField"]; /** @@ -4211,8 +4103,6 @@ export type components = { * @enum {string} */ resample_mode?: "nearest" | "box" | "bilinear" | "hamming" | "bicubic" | "lanczos"; - /** @description Optional core metadata to be written to image */ - metadata?: components["schemas"]["CoreMetadata"] | null; /** * type * @default img_resize @@ -4225,6 +4115,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -4236,17 +4130,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to scale */ image?: components["schemas"]["ImageField"]; /** @@ -4285,17 +4174,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to encode */ image?: components["schemas"]["ImageField"]; /** @description VAE */ @@ -4345,6 +4229,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -4356,17 +4244,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to check */ image?: components["schemas"]["ImageField"]; /** @@ -4375,8 +4258,6 @@ export type components = { * @default InvokeAI */ text?: string; - /** @description Optional core metadata to be written to image */ - metadata?: components["schemas"]["CoreMetadata"] | null; /** * type * @default img_watermark @@ -4405,6 +4286,10 @@ export type components = { * @description Infills transparent areas of an image with a solid color */ 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. @@ -4416,17 +4301,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to infill */ image?: components["schemas"]["ImageField"]; /** @@ -4451,6 +4331,10 @@ export type components = { * @description Infills transparent areas of an image using the PatchMatch algorithm */ 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. @@ -4462,17 +4346,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to infill */ image?: components["schemas"]["ImageField"]; /** @@ -4500,6 +4379,10 @@ export type components = { * @description Infills transparent areas of an image with tiles of the image */ 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. @@ -4511,17 +4394,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to infill */ image?: components["schemas"]["ImageField"]; /** @@ -4558,17 +4436,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Collection * @description The collection of integer values @@ -4614,17 +4487,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Value * @description The integer value @@ -4654,17 +4522,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Operation * @description The operation to perform @@ -4752,17 +4615,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Collection * @description The list of items to iterate over @@ -4803,6 +4661,10 @@ export type components = { * @description Infills transparent areas of an image using the LaMa model */ 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. @@ -4814,17 +4676,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to infill */ image?: components["schemas"]["ImageField"]; /** @@ -4850,17 +4707,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Collection * @description The collection of latents tensors @@ -4922,17 +4774,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The latents tensor */ latents?: components["schemas"]["LatentsField"]; /** @@ -4971,6 +4818,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -4982,17 +4833,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description Latents tensor */ latents?: components["schemas"]["LatentsField"]; /** @description VAE */ @@ -5009,8 +4855,6 @@ export type components = { * @default false */ fp32?: boolean; - /** @description Optional core metadata to be written to image */ - metadata?: components["schemas"]["CoreMetadata"] | null; /** * type * @default l2i @@ -5023,6 +4867,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -5034,17 +4882,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to process */ image?: components["schemas"]["ImageField"]; /** @@ -5089,6 +4932,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -5100,17 +4947,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to process */ image?: components["schemas"]["ImageField"]; /** @@ -5137,6 +4979,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -5148,17 +4994,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to process */ image?: components["schemas"]["ImageField"]; /** @@ -5188,14 +5029,14 @@ export type components = { }; /** * LoRAMetadataField - * @description LoRA metadata for an image generated in InvokeAI. + * @description LoRA Metadata Field */ LoRAMetadataField: { - /** @description The LoRA model */ + /** @description LoRA model to load */ lora: components["schemas"]["LoRAModelField"]; /** * Weight - * @description The weight of the LoRA model + * @description The weight at which the LoRA is applied to each model */ weight: number; }; @@ -5275,17 +5116,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * LoRA * @description LoRA model to load @@ -5367,17 +5203,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description Main model (UNet, VAE, CLIP) to load */ model: components["schemas"]["MainModelField"]; /** @@ -5392,6 +5223,10 @@ export type components = { * @description Combine two masks together by multiplying them using `PIL.ImageChops.multiply()`. */ 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. @@ -5403,17 +5238,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The first mask to combine */ mask1?: components["schemas"]["ImageField"]; /** @description The second image to combine */ @@ -5430,6 +5260,10 @@ export type components = { * @description Applies an edge mask to an image */ 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. @@ -5441,17 +5275,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to apply the mask to */ image?: components["schemas"]["ImageField"]; /** @@ -5486,6 +5315,10 @@ export type components = { * @description Extracts the alpha channel of an image as a mask. */ 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. @@ -5497,17 +5330,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to create the mask from */ image?: components["schemas"]["ImageField"]; /** @@ -5528,6 +5356,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -5539,17 +5371,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to process */ image?: components["schemas"]["ImageField"]; /** @@ -5576,6 +5403,40 @@ export type components = { * @enum {string} */ MergeInterpolationMethod: "weighted_sum" | "sigmoid" | "inv_sigmoid" | "add_difference"; + /** + * Metadata Merge + * @description Merged a collection of MetadataDict into a single MetadataDict. + */ + MergeMetadataInvocation: { + /** + * 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 | null; + /** + * Use Cache + * @description Whether or not to use the cache + * @default true + */ + use_cache?: boolean; + /** + * Collection + * @description Collection of Metadata + */ + collection?: components["schemas"]["MetadataField"][]; + /** + * type + * @default merge_metadata + * @constant + */ + type: "merge_metadata"; + }; /** MergeModelsBody */ MergeModelsBody: { /** @@ -5609,10 +5470,16 @@ export type components = { merge_dest_directory?: string | null; }; /** - * Metadata Accumulator - * @description Outputs a Core Metadata Object + * MetadataField + * @description Pydantic model for metadata with custom root of type dict[str, Any]. + * Metadata is stored without a strict schema. */ - MetadataAccumulatorInvocation: { + MetadataField: Record; + /** + * Metadata + * @description Takes a MetadataItem or collection of MetadataItems and outputs a MetadataDict. + */ + MetadataInvocation: { /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -5625,188 +5492,109 @@ export type components = { */ is_intermediate?: boolean | null; /** - * Workflow - * @description The workflow to save with the image + * Use Cache + * @description Whether or not to use the cache + * @default true */ - workflow?: string | null; + use_cache?: boolean; + /** + * Items + * @description A single metadata item or collection of metadata items + */ + items?: components["schemas"]["MetadataItemField"][] | components["schemas"]["MetadataItemField"]; + /** + * type + * @default metadata + * @constant + */ + type: "metadata"; + }; + /** MetadataItemField */ + MetadataItemField: { + /** + * Label + * @description Label for this metadata item + */ + label: string; + /** + * Value + * @description The value for this metadata item (may be any type) + */ + value: unknown; + }; + /** + * Metadata Item + * @description Used to create an arbitrary metadata item. Provide "label" and make a connection to "value" to store that data as the value. + */ + MetadataItemInvocation: { + /** + * 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 | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** - * Generation Mode - * @description The generation mode that output this image + * Label + * @description Label for this metadata item */ - generation_mode?: string | null; + label?: string; /** - * Positive Prompt - * @description The positive prompt parameter + * Value + * @description The value for this metadata item (may be any type) */ - positive_prompt?: string | null; - /** - * Negative Prompt - * @description The negative prompt parameter - */ - negative_prompt?: string | null; - /** - * Width - * @description The width parameter - */ - width?: number | null; - /** - * Height - * @description The height parameter - */ - height?: number | null; - /** - * Seed - * @description The seed used for noise generation - */ - seed?: number | null; - /** - * Rand Device - * @description The device used for random number generation - */ - rand_device?: string | null; - /** - * Cfg Scale - * @description The classifier-free guidance scale parameter - */ - cfg_scale?: number | null; - /** - * Steps - * @description The number of steps used for inference - */ - steps?: number | null; - /** - * Scheduler - * @description The scheduler used for inference - */ - scheduler?: string | null; - /** - * Clip Skip - * @description The number of skipped CLIP layers - */ - clip_skip?: number | null; - /** @description The main model used for inference */ - model?: components["schemas"]["MainModelField"] | null; - /** - * Controlnets - * @description The ControlNets used for inference - */ - controlnets?: components["schemas"]["ControlField"][] | null; - /** - * Ipadapters - * @description The IP Adapters used for inference - */ - ipAdapters?: components["schemas"]["IPAdapterMetadataField"][] | null; - /** - * T2Iadapters - * @description The IP Adapters used for inference - */ - t2iAdapters?: components["schemas"]["T2IAdapterField"][] | null; - /** - * Loras - * @description The LoRAs used for inference - */ - loras?: components["schemas"]["LoRAMetadataField"][] | null; - /** - * Strength - * @description The strength used for latents-to-latents - */ - strength?: number | null; - /** - * Init Image - * @description The name of the initial image - */ - init_image?: string | null; - /** @description The VAE used for decoding, if the main model's default was not used */ - vae?: components["schemas"]["VAEModelField"] | null; - /** - * Hrf Width - * @description The high resolution fix height and width multipler. - */ - hrf_width?: number | null; - /** - * Hrf Height - * @description The high resolution fix height and width multipler. - */ - hrf_height?: number | null; - /** - * Hrf Strength - * @description The high resolution fix img2img strength used in the upscale pass. - */ - hrf_strength?: number | null; - /** - * Positive Style Prompt - * @description The positive style prompt parameter - */ - positive_style_prompt?: string | null; - /** - * Negative Style Prompt - * @description The negative style prompt parameter - */ - negative_style_prompt?: string | null; - /** @description The SDXL Refiner model used */ - refiner_model?: components["schemas"]["MainModelField"] | null; - /** - * Refiner Cfg Scale - * @description The classifier-free guidance scale parameter used for the refiner - */ - refiner_cfg_scale?: number | null; - /** - * Refiner Steps - * @description The number of steps used for the refiner - */ - refiner_steps?: number | null; - /** - * Refiner Scheduler - * @description The scheduler used for the refiner - */ - refiner_scheduler?: string | null; - /** - * Refiner Positive Aesthetic Score - * @description The aesthetic score used for the refiner - */ - refiner_positive_aesthetic_score?: number | null; - /** - * Refiner Negative Aesthetic Score - * @description The aesthetic score used for the refiner - */ - refiner_negative_aesthetic_score?: number | null; - /** - * Refiner Start - * @description The start value used for refiner denoising - */ - refiner_start?: number | null; + value?: unknown; /** * type - * @default metadata_accumulator + * @default metadata_item * @constant */ - type: "metadata_accumulator"; + type: "metadata_item"; }; /** - * MetadataAccumulatorOutput - * @description The output of the MetadataAccumulator node + * MetadataItemOutput + * @description Metadata Item Output */ - MetadataAccumulatorOutput: { - /** @description The core metadata for the image */ - metadata: components["schemas"]["CoreMetadata"]; + MetadataItemOutput: { + /** @description Metadata Item */ + item: components["schemas"]["MetadataItemField"]; /** * type - * @default metadata_accumulator_output + * @default metadata_item_output * @constant */ - type: "metadata_accumulator_output"; + type: "metadata_item_output"; + }; + /** MetadataOutput */ + MetadataOutput: { + /** @description Metadata Dict */ + metadata: components["schemas"]["MetadataField"]; + /** + * type + * @default metadata_output + * @constant + */ + type: "metadata_output"; }; /** * Midas Depth Processor * @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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -5818,17 +5606,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to process */ image?: components["schemas"]["ImageField"]; /** @@ -5855,6 +5638,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -5866,17 +5653,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to process */ image?: components["schemas"]["ImageField"]; /** @@ -5987,17 +5769,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * A * @description The first number @@ -6051,17 +5828,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Seed * @description Seed for random number generation @@ -6121,6 +5893,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -6132,17 +5908,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to process */ image?: components["schemas"]["ImageField"]; /** @@ -6169,6 +5940,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -6180,23 +5955,16 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description Denoised latents tensor */ latents?: components["schemas"]["LatentsField"]; /** @description VAE */ vae?: components["schemas"]["VaeField"]; - /** @description Optional core metadata to be written to image */ - metadata?: components["schemas"]["CoreMetadata"] | null; /** * type * @default l2i_onnx @@ -6249,17 +6017,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Prompt * @description Raw prompt text (no parsing) @@ -6340,17 +6103,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description Positive conditioning tensor */ positive_conditioning?: components["schemas"]["ConditioningField"]; /** @description Negative conditioning tensor */ @@ -6474,17 +6232,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description ONNX Main model (UNet, VAE, CLIP) to load */ model: components["schemas"]["OnnxModelField"]; /** @@ -6499,6 +6252,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -6510,17 +6267,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to process */ image?: components["schemas"]["ImageField"]; /** @@ -6553,6 +6305,10 @@ export type components = { * @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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -6564,17 +6320,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to process */ image?: components["schemas"]["ImageField"]; /** @@ -6624,17 +6375,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * File Path * @description Path to prompt text file @@ -6696,17 +6442,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache - * @default false + * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Low * @description The inclusive low value @@ -6748,17 +6489,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache - * @default false + * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Low * @description The inclusive low value @@ -6794,17 +6530,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache - * @default false + * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Low * @description The inclusive low value @@ -6851,17 +6582,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Start * @description The start of the range @@ -6903,17 +6629,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Start * @description The start of the range @@ -6963,17 +6684,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description Latents tensor */ latents?: components["schemas"]["LatentsField"]; /** @@ -7032,17 +6748,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Value * @description The float value @@ -7078,17 +6789,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Prompt * @description Prompt to be parsed by Compel to create a conditioning tensor @@ -7164,17 +6870,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * LoRA * @description LoRA model to load @@ -7251,17 +6952,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description SDXL Main model (UNet, VAE, CLIP1, CLIP2) to load */ model: components["schemas"]["MainModelField"]; /** @@ -7319,17 +7015,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Style * @description Prompt to be parsed by Compel to create a conditioning tensor @@ -7387,17 +7078,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description SDXL Refiner Main Modde (UNet, VAE, CLIP2) to load */ model: components["schemas"]["MainModelField"]; /** @@ -7439,6 +7125,10 @@ export type components = { * @description Saves an image. Unlike an image primitive, this invocation stores a copy of the image. */ 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. @@ -7450,23 +7140,16 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache - * @default false + * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to process */ image?: components["schemas"]["ImageField"]; /** @description The board to save the image to */ - board?: components["schemas"]["BoardField"] | null; - /** @description Optional core metadata to be written to image */ - metadata?: components["schemas"]["CoreMetadata"] | null; + board?: components["schemas"]["BoardField"]; /** * type * @default save_image @@ -7490,17 +7173,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description Latents tensor */ latents?: components["schemas"]["LatentsField"]; /** @@ -7544,17 +7222,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Scheduler * @description Scheduler to use during inference @@ -7605,17 +7278,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * UNet * @description UNet (scheduler, LoRAs) @@ -7672,6 +7340,10 @@ 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -7683,17 +7355,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to process */ image?: components["schemas"]["ImageField"]; /** @@ -7927,17 +7594,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to show */ image?: components["schemas"]["ImageField"]; /** @@ -8119,17 +7781,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Easing * @description The easing function to use @@ -8234,17 +7891,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Collection * @description The collection of string values @@ -8290,17 +7942,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * Value * @description The string value @@ -8330,17 +7977,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * String Left * @description String Left @@ -8376,17 +8018,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * String Left * @description String Left @@ -8467,17 +8104,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * String * @description String to work on @@ -8525,17 +8157,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * String * @description String to split @@ -8571,17 +8198,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * String * @description String to split @@ -8616,17 +8238,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * A * @description The first number @@ -8694,17 +8311,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The IP-Adapter image prompt. */ image?: components["schemas"]["ImageField"]; /** @@ -8814,6 +8426,10 @@ export type components = { * @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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -8825,17 +8441,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to process */ image?: components["schemas"]["ImageField"]; /** @@ -8920,17 +8531,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** * VAE * @description VAE model to load @@ -8992,11 +8598,21 @@ export type components = { /** Error Type */ type: string; }; + /** + * WorkflowField + * @description Pydantic model for workflows with custom root of type dict[str, Any]. + * Workflows are stored without a strict schema. + */ + WorkflowField: Record; /** * 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; /** * Id * @description The id of this instance of an invocation. Must be unique among all instances of invocations. @@ -9008,17 +8624,12 @@ export type components = { * @default false */ is_intermediate?: boolean | null; - /** - * Workflow - * @description The workflow to save with the image - */ - workflow?: string | null; /** * Use Cache * @description Whether or not to use the cache * @default true */ - use_cache?: boolean | null; + use_cache?: boolean; /** @description The image to process */ image?: components["schemas"]["ImageField"]; /** @@ -9079,7 +8690,7 @@ export type components = { * If a field should be provided a data type that does not exactly match the python type of the field, use this to provide the type that should be used instead. See the node development docs for detail on adding a new field type, which involves client-side changes. * @enum {string} */ - UIType: "boolean" | "ColorField" | "ConditioningField" | "ControlField" | "float" | "ImageField" | "integer" | "LatentsField" | "string" | "BooleanCollection" | "ColorCollection" | "ConditioningCollection" | "ControlCollection" | "FloatCollection" | "ImageCollection" | "IntegerCollection" | "LatentsCollection" | "StringCollection" | "BooleanPolymorphic" | "ColorPolymorphic" | "ConditioningPolymorphic" | "ControlPolymorphic" | "FloatPolymorphic" | "ImagePolymorphic" | "IntegerPolymorphic" | "LatentsPolymorphic" | "StringPolymorphic" | "MainModelField" | "SDXLMainModelField" | "SDXLRefinerModelField" | "ONNXModelField" | "VaeModelField" | "LoRAModelField" | "ControlNetModelField" | "IPAdapterModelField" | "UNetField" | "VaeField" | "ClipField" | "Collection" | "CollectionItem" | "enum" | "Scheduler" | "WorkflowField" | "IsIntermediate" | "MetadataField" | "BoardField"; + UIType: "boolean" | "ColorField" | "ConditioningField" | "ControlField" | "float" | "ImageField" | "integer" | "LatentsField" | "string" | "BooleanCollection" | "ColorCollection" | "ConditioningCollection" | "ControlCollection" | "FloatCollection" | "ImageCollection" | "IntegerCollection" | "LatentsCollection" | "StringCollection" | "BooleanPolymorphic" | "ColorPolymorphic" | "ConditioningPolymorphic" | "ControlPolymorphic" | "FloatPolymorphic" | "ImagePolymorphic" | "IntegerPolymorphic" | "LatentsPolymorphic" | "StringPolymorphic" | "MainModelField" | "SDXLMainModelField" | "SDXLRefinerModelField" | "ONNXModelField" | "VaeModelField" | "LoRAModelField" | "ControlNetModelField" | "IPAdapterModelField" | "UNetField" | "VaeField" | "ClipField" | "Collection" | "CollectionItem" | "enum" | "Scheduler" | "WorkflowField" | "IsIntermediate" | "BoardField" | "Any" | "MetadataItem" | "MetadataItemCollection" | "MetadataItemPolymorphic" | "MetadataDict"; /** * _InputField * @description *DO NOT USE* @@ -9116,24 +8727,18 @@ export type components = { /** Ui Order */ ui_order: number | null; }; - /** - * StableDiffusion1ModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; - /** - * T2IAdapterModelFormat - * @description An enumeration. - * @enum {string} - */ - T2IAdapterModelFormat: "diffusers"; /** * ControlNetModelFormat * @description An enumeration. * @enum {string} */ ControlNetModelFormat: "checkpoint" | "diffusers"; + /** + * StableDiffusion2ModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; /** * IPAdapterModelFormat * @description An enumeration. @@ -9146,24 +8751,30 @@ export type components = { * @enum {string} */ CLIPVisionModelFormat: "diffusers"; + /** + * StableDiffusion1ModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; /** * StableDiffusionOnnxModelFormat * @description An enumeration. * @enum {string} */ StableDiffusionOnnxModelFormat: "olive" | "onnx"; - /** - * StableDiffusion2ModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; /** * StableDiffusionXLModelFormat * @description An enumeration. * @enum {string} */ StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; + /** + * T2IAdapterModelFormat + * @description An enumeration. + * @enum {string} + */ + T2IAdapterModelFormat: "diffusers"; }; responses: never; parameters: never; @@ -9724,7 +9335,7 @@ export type operations = { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["ImageMetadata"]; + "application/json": components["schemas"]["MetadataField"] | null; }; }; /** @description Validation Error */ @@ -10701,4 +10312,30 @@ export type operations = { }; }; }; + /** + * Get Workflow + * @description Gets a workflow + */ + get_workflow: { + parameters: { + path: { + /** @description The workflow to get */ + workflow_id: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["WorkflowField"]; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; }; diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 63617a4eb5..085ea65327 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -27,14 +27,6 @@ export type BatchConfig = export type EnqueueBatchResult = components['schemas']['EnqueueBatchResult']; -/** - * This is an unsafe type; the object inside is not guaranteed to be valid. - */ -export type UnsafeImageMetadata = { - metadata: s['CoreMetadata']; - graph: NonNullable; -}; - export type _InputField = s['_InputField']; export type _OutputField = s['_OutputField']; @@ -50,7 +42,6 @@ export type ImageChanges = s['ImageRecordChanges']; export type ImageCategory = s['ImageCategory']; export type ResourceOrigin = s['ResourceOrigin']; export type ImageField = s['ImageField']; -export type ImageMetadata = s['ImageMetadata']; export type OffsetPaginatedResults_BoardDTO_ = s['OffsetPaginatedResults_BoardDTO_']; export type OffsetPaginatedResults_ImageDTO_ = @@ -145,13 +136,19 @@ export type ImageCollectionInvocation = s['ImageCollectionInvocation']; export type MainModelLoaderInvocation = s['MainModelLoaderInvocation']; export type OnnxModelLoaderInvocation = s['OnnxModelLoaderInvocation']; export type LoraLoaderInvocation = s['LoraLoaderInvocation']; -export type MetadataAccumulatorInvocation = s['MetadataAccumulatorInvocation']; export type ESRGANInvocation = s['ESRGANInvocation']; export type DivideInvocation = s['DivideInvocation']; export type ImageNSFWBlurInvocation = s['ImageNSFWBlurInvocation']; export type ImageWatermarkInvocation = s['ImageWatermarkInvocation']; export type SeamlessModeInvocation = s['SeamlessModeInvocation']; export type SaveImageInvocation = s['SaveImageInvocation']; +export type MetadataInvocation = s['MetadataInvocation']; +export type CoreMetadataInvocation = s['CoreMetadataInvocation']; +export type MetadataItemInvocation = s['MetadataItemInvocation']; +export type MergeMetadataInvocation = s['MergeMetadataInvocation']; +export type IPAdapterMetadataField = s['IPAdapterMetadataField']; +export type T2IAdapterField = s['T2IAdapterField']; +export type LoRAMetadataField = s['LoRAMetadataField']; // ControlNet Nodes export type ControlNetInvocation = s['ControlNetInvocation']; diff --git a/tests/nodes/test_node_graph.py b/tests/nodes/test_node_graph.py index 3c965895f9..d1ece0336a 100644 --- a/tests/nodes/test_node_graph.py +++ b/tests/nodes/test_node_graph.py @@ -10,7 +10,12 @@ from invokeai.app.invocations.baseinvocation import ( ) from invokeai.app.invocations.image import ShowImageInvocation from invokeai.app.invocations.math import AddInvocation, SubtractInvocation -from invokeai.app.invocations.primitives import FloatInvocation, IntegerInvocation +from invokeai.app.invocations.primitives import ( + FloatCollectionInvocation, + FloatInvocation, + IntegerInvocation, + StringInvocation, +) from invokeai.app.invocations.upscale import ESRGANInvocation from invokeai.app.services.shared.default_graphs import create_text_to_image from invokeai.app.services.shared.graph import ( @@ -27,8 +32,11 @@ from invokeai.app.services.shared.graph import ( ) from .test_nodes import ( + AnyTypeTestInvocation, ImageToImageTestInvocation, ListPassThroughInvocation, + PolymorphicStringTestInvocation, + PromptCollectionTestInvocation, PromptTestInvocation, TextToImageTestInvocation, ) @@ -692,6 +700,144 @@ def test_ints_do_not_accept_floats(): g.add_edge(e) +def test_polymorphic_accepts_single(): + g = Graph() + n1 = StringInvocation(id="1", value="banana") + n2 = PolymorphicStringTestInvocation(id="2") + g.add_node(n1) + g.add_node(n2) + e1 = create_edge(n1.id, "value", n2.id, "value") + # Not throwing on this line is sufficient + g.add_edge(e1) + + +def test_polymorphic_accepts_collection_of_same_base_type(): + g = Graph() + n1 = PromptCollectionTestInvocation(id="1", collection=["banana", "sundae"]) + n2 = PolymorphicStringTestInvocation(id="2") + g.add_node(n1) + g.add_node(n2) + e1 = create_edge(n1.id, "collection", n2.id, "value") + # Not throwing on this line is sufficient + g.add_edge(e1) + + +def test_polymorphic_does_not_accept_collection_of_different_base_type(): + g = Graph() + n1 = FloatCollectionInvocation(id="1", collection=[1.0, 2.0, 3.0]) + n2 = PolymorphicStringTestInvocation(id="2") + g.add_node(n1) + g.add_node(n2) + e1 = create_edge(n1.id, "collection", n2.id, "value") + with pytest.raises(InvalidEdgeError): + g.add_edge(e1) + + +def test_polymorphic_does_not_accept_generic_collection(): + g = Graph() + n1 = IntegerInvocation(id="1", value=1) + n2 = IntegerInvocation(id="2", value=2) + n3 = CollectInvocation(id="3") + n4 = PolymorphicStringTestInvocation(id="4") + g.add_node(n1) + g.add_node(n2) + g.add_node(n3) + g.add_node(n4) + e1 = create_edge(n1.id, "value", n3.id, "item") + e2 = create_edge(n2.id, "value", n3.id, "item") + e3 = create_edge(n3.id, "collection", n4.id, "value") + g.add_edge(e1) + g.add_edge(e2) + with pytest.raises(InvalidEdgeError): + g.add_edge(e3) + + +def test_any_accepts_integer(): + g = Graph() + n1 = IntegerInvocation(id="1", value=1) + n2 = AnyTypeTestInvocation(id="2") + g.add_node(n1) + g.add_node(n2) + e = create_edge(n1.id, "value", n2.id, "value") + # Not throwing on this line is sufficient + g.add_edge(e) + + +def test_any_accepts_string(): + g = Graph() + n1 = StringInvocation(id="1", value="banana sundae") + n2 = AnyTypeTestInvocation(id="2") + g.add_node(n1) + g.add_node(n2) + e = create_edge(n1.id, "value", n2.id, "value") + # Not throwing on this line is sufficient + g.add_edge(e) + + +def test_any_accepts_generic_collection(): + g = Graph() + n1 = IntegerInvocation(id="1", value=1) + n2 = IntegerInvocation(id="2", value=2) + n3 = CollectInvocation(id="3") + n4 = AnyTypeTestInvocation(id="4") + g.add_node(n1) + g.add_node(n2) + g.add_node(n3) + g.add_node(n4) + e1 = create_edge(n1.id, "value", n3.id, "item") + e2 = create_edge(n2.id, "value", n3.id, "item") + e3 = create_edge(n3.id, "collection", n4.id, "value") + g.add_edge(e1) + g.add_edge(e2) + # Not throwing on this line is sufficient + g.add_edge(e3) + + +def test_any_accepts_prompt_collection(): + g = Graph() + n1 = PromptCollectionTestInvocation(id="1", collection=["banana", "sundae"]) + n2 = AnyTypeTestInvocation(id="2") + g.add_node(n1) + g.add_node(n2) + e = create_edge(n1.id, "collection", n2.id, "value") + # Not throwing on this line is sufficient + g.add_edge(e) + + +def test_any_accepts_any(): + g = Graph() + n1 = AnyTypeTestInvocation(id="1") + n2 = AnyTypeTestInvocation(id="2") + g.add_node(n1) + g.add_node(n2) + e = create_edge(n1.id, "value", n2.id, "value") + # Not throwing on this line is sufficient + g.add_edge(e) + + +def test_iterate_accepts_collection(): + """We need to update the validation for Collect -> Iterate to traverse to the Iterate + node's output and compare that against the item type of the Collect node's collection. Until + then, Collect nodes may not output into Iterate nodes.""" + g = Graph() + n1 = IntegerInvocation(id="1", value=1) + n2 = IntegerInvocation(id="2", value=2) + n3 = CollectInvocation(id="3") + n4 = IterateInvocation(id="4") + g.add_node(n1) + g.add_node(n2) + g.add_node(n3) + g.add_node(n4) + e1 = create_edge(n1.id, "value", n3.id, "item") + e2 = create_edge(n2.id, "value", n3.id, "item") + e3 = create_edge(n3.id, "collection", n4.id, "collection") + g.add_edge(e1) + g.add_edge(e2) + # Once we fix the validation logic as described, this should should not raise an error + with pytest.raises(InvalidEdgeError, match="Cannot connect collector to iterator"): + g.add_edge(e3) + + def test_graph_can_generate_schema(): # Not throwing on this line is sufficient # NOTE: if this test fails, it's PROBABLY because a new invocation type is breaking schema generation diff --git a/tests/nodes/test_nodes.py b/tests/nodes/test_nodes.py index 471c72a005..7807a56879 100644 --- a/tests/nodes/test_nodes.py +++ b/tests/nodes/test_nodes.py @@ -81,6 +81,29 @@ class PromptCollectionTestInvocation(BaseInvocation): return PromptCollectionTestInvocationOutput(collection=self.collection.copy()) +@invocation_output("test_any_output") +class AnyTypeTestInvocationOutput(BaseInvocationOutput): + value: Any = Field() + + +@invocation("test_any") +class AnyTypeTestInvocation(BaseInvocation): + value: Any = Field(default=None) + + def invoke(self, context: InvocationContext) -> AnyTypeTestInvocationOutput: + return AnyTypeTestInvocationOutput(value=self.value) + + +@invocation("test_polymorphic") +class PolymorphicStringTestInvocation(BaseInvocation): + value: Union[str, list[str]] = Field(default="") + + def invoke(self, context: InvocationContext) -> PromptCollectionTestInvocationOutput: + if isinstance(self.value, str): + return PromptCollectionTestInvocationOutput(collection=[self.value]) + return PromptCollectionTestInvocationOutput(collection=self.value) + + # Importing these must happen after test invocations are defined or they won't register from invokeai.app.services.events.events_base import EventServiceBase # noqa: E402 from invokeai.app.services.shared.graph import Edge, EdgeConnection # noqa: E402 From 5a163f02a66a75b373fe86bbf8b5f92c0de71ee3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 17 Oct 2023 19:08:01 +1100 Subject: [PATCH 03/27] fix(nodes): fix metadata/workflow serialization --- invokeai/app/invocations/baseinvocation.py | 3 --- .../app/services/workflow_records/workflow_records_common.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index 39df4971a6..162b22b28d 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -830,9 +830,6 @@ class MetadataField(RootModel): root: dict[str, Any] = Field(description="A dictionary of metadata, shape of which is arbitrary") - def model_dump(self, *args, **kwargs) -> dict[str, Any]: - return super().model_dump(*args, **kwargs)["root"] - type_adapter_MetadataField = TypeAdapter(MetadataField) diff --git a/invokeai/app/services/workflow_records/workflow_records_common.py b/invokeai/app/services/workflow_records/workflow_records_common.py index d548656dab..32046328bb 100644 --- a/invokeai/app/services/workflow_records/workflow_records_common.py +++ b/invokeai/app/services/workflow_records/workflow_records_common.py @@ -15,8 +15,5 @@ class WorkflowField(RootModel): root: dict[str, Any] = Field(description="Workflow dict") - def model_dump(self, *args, **kwargs) -> dict[str, Any]: - return super().model_dump(*args, **kwargs)["root"] - type_adapter_WorkflowField = TypeAdapter(WorkflowField) From 3c4f43314ccd9e552e6637ccdbe1f84b86ed52ce Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 17 Oct 2023 19:42:02 +1100 Subject: [PATCH 04/27] feat: move workflow/metadata models to `baseinvocation.py` needed to prevent circular imports --- invokeai/app/api/routers/images.py | 7 +++++-- invokeai/app/api/routers/workflows.py | 2 +- invokeai/app/invocations/baseinvocation.py | 20 ++++++++++++------- invokeai/app/invocations/metadata.py | 6 +++++- .../services/image_files/image_files_base.py | 3 +-- .../services/image_files/image_files_disk.py | 3 +-- invokeai/app/services/images/images_base.py | 3 +-- .../app/services/images/images_default.py | 3 +-- .../workflow_records/workflow_records_base.py | 2 +- .../workflow_records_common.py | 17 ---------------- .../workflow_records_sqlite.py | 7 ++----- 11 files changed, 31 insertions(+), 42 deletions(-) diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index f462437700..625fb3c43b 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -7,11 +7,14 @@ from fastapi.routing import APIRouter from PIL import Image from pydantic import BaseModel, Field, ValidationError -from invokeai.app.invocations.baseinvocation import MetadataField, type_adapter_MetadataField +from invokeai.app.invocations.baseinvocation import ( + MetadataField, + type_adapter_MetadataField, + type_adapter_WorkflowField, +) 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 type_adapter_WorkflowField from ..dependencies import ApiDependencies diff --git a/invokeai/app/api/routers/workflows.py b/invokeai/app/api/routers/workflows.py index 57a33fe73f..36de31fb51 100644 --- a/invokeai/app/api/routers/workflows.py +++ b/invokeai/app/api/routers/workflows.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, Path from invokeai.app.api.dependencies import ApiDependencies -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowField +from invokeai.app.invocations.baseinvocation import WorkflowField workflows_router = APIRouter(prefix="/v1/workflows", tags=["workflows"]) diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index 162b22b28d..50ce8de7d3 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -15,7 +15,6 @@ from pydantic.fields import _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 WorkflowField from invokeai.app.util.misc import uuid_string if TYPE_CHECKING: @@ -813,22 +812,29 @@ 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") + + +type_adapter_WorkflowField = TypeAdapter(WorkflowField) + + class WithWorkflow(BaseModel): workflow: Optional[WorkflowField] = InputField(default=None, description=FieldDescriptions.workflow) -class MetadataItemField(BaseModel): - label: str = Field(description=FieldDescriptions.metadata_item_label) - value: Any = Field(description=FieldDescriptions.metadata_item_value) - - class MetadataField(RootModel): """ Pydantic model for metadata with custom root of type dict[str, Any]. Metadata is stored without a strict schema. """ - root: dict[str, Any] = Field(description="A dictionary of metadata, shape of which is arbitrary") + root: dict[str, Any] = Field(description="The metadata") type_adapter_MetadataField = TypeAdapter(MetadataField) diff --git a/invokeai/app/invocations/metadata.py b/invokeai/app/invocations/metadata.py index 205dbef814..98f5f0e830 100644 --- a/invokeai/app/invocations/metadata.py +++ b/invokeai/app/invocations/metadata.py @@ -9,7 +9,6 @@ from invokeai.app.invocations.baseinvocation import ( InputField, InvocationContext, MetadataField, - MetadataItemField, OutputField, UIType, invocation, @@ -24,6 +23,11 @@ from invokeai.app.invocations.t2i_adapter import T2IAdapterField from ...version import __version__ +class MetadataItemField(BaseModel): + label: str = Field(description=FieldDescriptions.metadata_item_label) + value: Any = Field(description=FieldDescriptions.metadata_item_value) + + class LoRAMetadataField(BaseModel): """LoRA Metadata Field""" diff --git a/invokeai/app/services/image_files/image_files_base.py b/invokeai/app/services/image_files/image_files_base.py index 3f6e797225..91e18f30fc 100644 --- a/invokeai/app/services/image_files/image_files_base.py +++ b/invokeai/app/services/image_files/image_files_base.py @@ -4,8 +4,7 @@ from typing import Optional from PIL.Image import Image as PILImageType -from invokeai.app.invocations.metadata import MetadataField -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowField +from invokeai.app.invocations.baseinvocation import MetadataField, WorkflowField class ImageFileStorageBase(ABC): diff --git a/invokeai/app/services/image_files/image_files_disk.py b/invokeai/app/services/image_files/image_files_disk.py index 57c05562d5..e8a733d619 100644 --- a/invokeai/app/services/image_files/image_files_disk.py +++ b/invokeai/app/services/image_files/image_files_disk.py @@ -7,9 +7,8 @@ from PIL import Image, PngImagePlugin from PIL.Image import Image as PILImageType from send2trash import send2trash -from invokeai.app.invocations.metadata import MetadataField +from invokeai.app.invocations.baseinvocation import MetadataField, WorkflowField from invokeai.app.services.invoker import Invoker -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowField from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail from .image_files_base import ImageFileStorageBase diff --git a/invokeai/app/services/images/images_base.py b/invokeai/app/services/images/images_base.py index ebb40424bc..50a3a5fb82 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.metadata import MetadataField +from invokeai.app.invocations.baseinvocation import MetadataField, WorkflowField from invokeai.app.services.image_records.image_records_common import ( ImageCategory, ImageRecord, @@ -12,7 +12,6 @@ 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 WorkflowField class ImageServiceABC(ABC): diff --git a/invokeai/app/services/images/images_default.py b/invokeai/app/services/images/images_default.py index e466e809b1..a0d59470fc 100644 --- a/invokeai/app/services/images/images_default.py +++ b/invokeai/app/services/images/images_default.py @@ -2,10 +2,9 @@ from typing import Optional from PIL.Image import Image as PILImageType -from invokeai.app.invocations.metadata import MetadataField +from invokeai.app.invocations.baseinvocation import MetadataField, WorkflowField 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 WorkflowField from ..image_files.image_files_common import ( ImageFileDeleteException, diff --git a/invokeai/app/services/workflow_records/workflow_records_base.py b/invokeai/app/services/workflow_records/workflow_records_base.py index 97f7cfe3c0..d5a4b25ce4 100644 --- a/invokeai/app/services/workflow_records/workflow_records_base.py +++ b/invokeai/app/services/workflow_records/workflow_records_base.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowField +from invokeai.app.invocations.baseinvocation import WorkflowField class WorkflowRecordsStorageBase(ABC): diff --git a/invokeai/app/services/workflow_records/workflow_records_common.py b/invokeai/app/services/workflow_records/workflow_records_common.py index 32046328bb..3a2b13f565 100644 --- a/invokeai/app/services/workflow_records/workflow_records_common.py +++ b/invokeai/app/services/workflow_records/workflow_records_common.py @@ -1,19 +1,2 @@ -from typing import Any - -from pydantic import Field, RootModel, TypeAdapter - - class WorkflowNotFoundError(Exception): """Raised when a workflow is not found""" - - -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="Workflow dict") - - -type_adapter_WorkflowField = TypeAdapter(WorkflowField) diff --git a/invokeai/app/services/workflow_records/workflow_records_sqlite.py b/invokeai/app/services/workflow_records/workflow_records_sqlite.py index 2b284ac03f..e3c11cfa4b 100644 --- a/invokeai/app/services/workflow_records/workflow_records_sqlite.py +++ b/invokeai/app/services/workflow_records/workflow_records_sqlite.py @@ -1,14 +1,11 @@ import sqlite3 import threading +from invokeai.app.invocations.baseinvocation import WorkflowField, type_adapter_WorkflowField from invokeai.app.services.invoker import Invoker from invokeai.app.services.shared.sqlite import SqliteDatabase from invokeai.app.services.workflow_records.workflow_records_base import WorkflowRecordsStorageBase -from invokeai.app.services.workflow_records.workflow_records_common import ( - WorkflowField, - WorkflowNotFoundError, - type_adapter_WorkflowField, -) +from invokeai.app.services.workflow_records.workflow_records_common import WorkflowNotFoundError from invokeai.app.util.misc import uuid_string From 4012388f0aca3836714cbd6bba02f4ccda61b5ce Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 17 Oct 2023 19:46:37 +1100 Subject: [PATCH 05/27] feat: use `ModelValidator` naming convention for pydantic type adapters This is the naming convention in the docs and is also clear. --- invokeai/app/api/routers/images.py | 8 ++++---- invokeai/app/api/routers/models.py | 20 +++++++++---------- invokeai/app/invocations/baseinvocation.py | 4 ++-- .../image_records/image_records_sqlite.py | 4 ++-- .../item_storage/item_storage_sqlite.py | 10 +++++----- .../session_queue/session_queue_common.py | 8 ++++---- .../workflow_records_sqlite.py | 4 ++-- tests/nodes/test_node_graph.py | 4 ++-- tests/nodes/test_session_queue.py | 10 +++++----- 9 files changed, 36 insertions(+), 36 deletions(-) diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 625fb3c43b..c27ec1e0d9 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -9,8 +9,8 @@ from pydantic import BaseModel, Field, ValidationError from invokeai.app.invocations.baseinvocation import ( MetadataField, - type_adapter_MetadataField, - type_adapter_WorkflowField, + MetadataFieldValidator, + WorkflowFieldValidator, ) from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO @@ -66,7 +66,7 @@ async def upload_image( metadata_raw = pil_image.info.get("invokeai_metadata", None) if metadata_raw: try: - metadata = type_adapter_MetadataField.validate_json(metadata_raw) + metadata = MetadataFieldValidator.validate_json(metadata_raw) except ValidationError: ApiDependencies.invoker.services.logger.warn("Failed to parse metadata for uploaded image") pass @@ -75,7 +75,7 @@ async def upload_image( workflow_raw = pil_image.info.get("invokeai_workflow", None) if workflow_raw is not None: try: - workflow = type_adapter_WorkflowField.validate_json(workflow_raw) + workflow = WorkflowFieldValidator.validate_json(workflow_raw) except ValidationError: ApiDependencies.invoker.services.logger.warn("Failed to parse metadata for uploaded image") pass diff --git a/invokeai/app/api/routers/models.py b/invokeai/app/api/routers/models.py index 018f3af02b..afa7d8df82 100644 --- a/invokeai/app/api/routers/models.py +++ b/invokeai/app/api/routers/models.py @@ -23,13 +23,13 @@ from ..dependencies import ApiDependencies models_router = APIRouter(prefix="/v1/models", tags=["models"]) UpdateModelResponse = Union[tuple(OPENAPI_MODEL_CONFIGS)] -update_models_response_adapter = TypeAdapter(UpdateModelResponse) +UpdateModelResponseValidator = TypeAdapter(UpdateModelResponse) ImportModelResponse = Union[tuple(OPENAPI_MODEL_CONFIGS)] -import_models_response_adapter = TypeAdapter(ImportModelResponse) +ImportModelResponseValidator = TypeAdapter(ImportModelResponse) ConvertModelResponse = Union[tuple(OPENAPI_MODEL_CONFIGS)] -convert_models_response_adapter = TypeAdapter(ConvertModelResponse) +ConvertModelResponseValidator = TypeAdapter(ConvertModelResponse) MergeModelResponse = Union[tuple(OPENAPI_MODEL_CONFIGS)] ImportModelAttributes = Union[tuple(OPENAPI_MODEL_CONFIGS)] @@ -41,7 +41,7 @@ class ModelsList(BaseModel): model_config = ConfigDict(use_enum_values=True) -models_list_adapter = TypeAdapter(ModelsList) +ModelsListValidator = TypeAdapter(ModelsList) @models_router.get( @@ -60,7 +60,7 @@ async def list_models( models_raw.extend(ApiDependencies.invoker.services.model_manager.list_models(base_model, model_type)) else: models_raw = ApiDependencies.invoker.services.model_manager.list_models(None, model_type) - models = models_list_adapter.validate_python({"models": models_raw}) + models = ModelsListValidator.validate_python({"models": models_raw}) return models @@ -131,7 +131,7 @@ async def update_model( base_model=base_model, model_type=model_type, ) - model_response = update_models_response_adapter.validate_python(model_raw) + model_response = UpdateModelResponseValidator.validate_python(model_raw) except ModelNotFoundException as e: raise HTTPException(status_code=404, detail=str(e)) except ValueError as e: @@ -186,7 +186,7 @@ async def import_model( model_raw = ApiDependencies.invoker.services.model_manager.list_model( model_name=info.name, base_model=info.base_model, model_type=info.model_type ) - return import_models_response_adapter.validate_python(model_raw) + return ImportModelResponseValidator.validate_python(model_raw) except ModelNotFoundException as e: logger.error(str(e)) @@ -231,7 +231,7 @@ async def add_model( base_model=info.base_model, model_type=info.model_type, ) - return import_models_response_adapter.validate_python(model_raw) + return ImportModelResponseValidator.validate_python(model_raw) except ModelNotFoundException as e: logger.error(str(e)) raise HTTPException(status_code=404, detail=str(e)) @@ -302,7 +302,7 @@ async def convert_model( model_raw = ApiDependencies.invoker.services.model_manager.list_model( model_name, base_model=base_model, model_type=model_type ) - response = convert_models_response_adapter.validate_python(model_raw) + response = ConvertModelResponseValidator.validate_python(model_raw) except ModelNotFoundException as e: raise HTTPException(status_code=404, detail=f"Model '{model_name}' not found: {str(e)}") except ValueError as e: @@ -417,7 +417,7 @@ async def merge_models( base_model=base_model, model_type=ModelType.Main, ) - response = convert_models_response_adapter.validate_python(model_raw) + response = ConvertModelResponseValidator.validate_python(model_raw) except ModelNotFoundException: raise HTTPException( status_code=404, diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index 50ce8de7d3..5f1ff0395f 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -821,7 +821,7 @@ class WorkflowField(RootModel): root: dict[str, Any] = Field(description="The workflow") -type_adapter_WorkflowField = TypeAdapter(WorkflowField) +WorkflowFieldValidator = TypeAdapter(WorkflowField) class WithWorkflow(BaseModel): @@ -837,7 +837,7 @@ class MetadataField(RootModel): root: dict[str, Any] = Field(description="The metadata") -type_adapter_MetadataField = TypeAdapter(MetadataField) +MetadataFieldValidator = TypeAdapter(MetadataField) class WithMetadata(BaseModel): diff --git a/invokeai/app/services/image_records/image_records_sqlite.py b/invokeai/app/services/image_records/image_records_sqlite.py index 7b60ec3d5b..dcabe55829 100644 --- a/invokeai/app/services/image_records/image_records_sqlite.py +++ b/invokeai/app/services/image_records/image_records_sqlite.py @@ -3,7 +3,7 @@ import threading from datetime import datetime from typing import Optional, Union, cast -from invokeai.app.invocations.baseinvocation import MetadataField, type_adapter_MetadataField +from invokeai.app.invocations.baseinvocation import MetadataField, MetadataFieldValidator from invokeai.app.services.shared.pagination import OffsetPaginatedResults from invokeai.app.services.shared.sqlite import SqliteDatabase @@ -170,7 +170,7 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): as_dict = dict(result) metadata_raw = cast(Optional[str], as_dict.get("metadata", None)) - return type_adapter_MetadataField.validate_json(metadata_raw) if metadata_raw is not None else None + return MetadataFieldValidator.validate_json(metadata_raw) if metadata_raw is not None else None except sqlite3.Error as e: self._conn.rollback() raise ImageRecordNotFoundException from e diff --git a/invokeai/app/services/item_storage/item_storage_sqlite.py b/invokeai/app/services/item_storage/item_storage_sqlite.py index 1bb9429130..d0249ebfa6 100644 --- a/invokeai/app/services/item_storage/item_storage_sqlite.py +++ b/invokeai/app/services/item_storage/item_storage_sqlite.py @@ -18,7 +18,7 @@ class SqliteItemStorage(ItemStorageABC, Generic[T]): _cursor: sqlite3.Cursor _id_field: str _lock: threading.RLock - _adapter: Optional[TypeAdapter[T]] + _validator: Optional[TypeAdapter[T]] def __init__(self, db: SqliteDatabase, table_name: str, id_field: str = "id"): super().__init__() @@ -28,7 +28,7 @@ class SqliteItemStorage(ItemStorageABC, Generic[T]): self._table_name = table_name self._id_field = id_field # TODO: validate that T has this field self._cursor = self._conn.cursor() - self._adapter: Optional[TypeAdapter[T]] = None + self._validator: Optional[TypeAdapter[T]] = None self._create_table() @@ -47,14 +47,14 @@ class SqliteItemStorage(ItemStorageABC, Generic[T]): self._lock.release() def _parse_item(self, item: str) -> T: - if self._adapter is None: + if self._validator is None: """ We don't get access to `__orig_class__` in `__init__()`, and we need this before start(), so we can create it when it is first needed instead. __orig_class__ is technically an implementation detail of the typing module, not a supported API """ - self._adapter = TypeAdapter(get_args(self.__orig_class__)[0]) # type: ignore [attr-defined] - return self._adapter.validate_json(item) + self._validator = TypeAdapter(get_args(self.__orig_class__)[0]) # type: ignore [attr-defined] + return self._validator.validate_json(item) def set(self, item: T): try: diff --git a/invokeai/app/services/session_queue/session_queue_common.py b/invokeai/app/services/session_queue/session_queue_common.py index cbf2154b66..69e6a3ab87 100644 --- a/invokeai/app/services/session_queue/session_queue_common.py +++ b/invokeai/app/services/session_queue/session_queue_common.py @@ -147,20 +147,20 @@ DEFAULT_QUEUE_ID = "default" QUEUE_ITEM_STATUS = Literal["pending", "in_progress", "completed", "failed", "canceled"] -adapter_NodeFieldValue = TypeAdapter(list[NodeFieldValue]) +NodeFieldValueValidator = TypeAdapter(list[NodeFieldValue]) def get_field_values(queue_item_dict: dict) -> Optional[list[NodeFieldValue]]: field_values_raw = queue_item_dict.get("field_values", None) - return adapter_NodeFieldValue.validate_json(field_values_raw) if field_values_raw is not None else None + return NodeFieldValueValidator.validate_json(field_values_raw) if field_values_raw is not None else None -adapter_GraphExecutionState = TypeAdapter(GraphExecutionState) +GraphExecutionStateValidator = TypeAdapter(GraphExecutionState) def get_session(queue_item_dict: dict) -> GraphExecutionState: session_raw = queue_item_dict.get("session", "{}") - session = adapter_GraphExecutionState.validate_json(session_raw, strict=False) + session = GraphExecutionStateValidator.validate_json(session_raw, strict=False) return session diff --git a/invokeai/app/services/workflow_records/workflow_records_sqlite.py b/invokeai/app/services/workflow_records/workflow_records_sqlite.py index e3c11cfa4b..2d9e1f26e8 100644 --- a/invokeai/app/services/workflow_records/workflow_records_sqlite.py +++ b/invokeai/app/services/workflow_records/workflow_records_sqlite.py @@ -1,7 +1,7 @@ import sqlite3 import threading -from invokeai.app.invocations.baseinvocation import WorkflowField, type_adapter_WorkflowField +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.workflow_records.workflow_records_base import WorkflowRecordsStorageBase @@ -39,7 +39,7 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase): row = self._cursor.fetchone() if row is None: raise WorkflowNotFoundError(f"Workflow with id {workflow_id} not found") - return type_adapter_WorkflowField.validate_json(row[0]) + return WorkflowFieldValidator.validate_json(row[0]) except Exception: self._conn.rollback() raise diff --git a/tests/nodes/test_node_graph.py b/tests/nodes/test_node_graph.py index d1ece0336a..e2a50e61e5 100644 --- a/tests/nodes/test_node_graph.py +++ b/tests/nodes/test_node_graph.py @@ -615,8 +615,8 @@ def test_graph_can_deserialize(): g.add_edge(e) json = g.model_dump_json() - adapter_graph = TypeAdapter(Graph) - g2 = adapter_graph.validate_json(json) + GraphValidator = TypeAdapter(Graph) + g2 = GraphValidator.validate_json(json) assert g2 is not None assert g2.nodes["1"] is not None diff --git a/tests/nodes/test_session_queue.py b/tests/nodes/test_session_queue.py index 731316068c..cdab5729f8 100644 --- a/tests/nodes/test_session_queue.py +++ b/tests/nodes/test_session_queue.py @@ -150,9 +150,9 @@ def test_prepare_values_to_insert(batch_data_collection, batch_graph): values = prepare_values_to_insert(queue_id="default", batch=b, priority=0, max_new_queue_items=1000) assert len(values) == 8 - session_adapter = TypeAdapter(GraphExecutionState) + GraphExecutionStateValidator = TypeAdapter(GraphExecutionState) # graph should be serialized - ges = session_adapter.validate_json(values[0].session) + ges = GraphExecutionStateValidator.validate_json(values[0].session) # graph values should be populated assert ges.graph.get_node("1").prompt == "Banana sushi" @@ -161,16 +161,16 @@ def test_prepare_values_to_insert(batch_data_collection, batch_graph): assert ges.graph.get_node("4").prompt == "Nissan" # session ids should match deserialized graph - assert [v.session_id for v in values] == [session_adapter.validate_json(v.session).id for v in values] + assert [v.session_id for v in values] == [GraphExecutionStateValidator.validate_json(v.session).id for v in values] # should unique session ids sids = [v.session_id for v in values] assert len(sids) == len(set(sids)) - nfv_list_adapter = TypeAdapter(list[NodeFieldValue]) + NodeFieldValueValidator = TypeAdapter(list[NodeFieldValue]) # should have 3 node field values assert type(values[0].field_values) is str - assert len(nfv_list_adapter.validate_json(values[0].field_values)) == 3 + assert len(NodeFieldValueValidator.validate_json(values[0].field_values)) == 3 # should have batch id and priority assert all(v.batch_id == b.batch_id for v in values) From 8910e912c791e7c08b330204f4db5b64f1d07354 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 17 Oct 2023 19:48:47 +1100 Subject: [PATCH 06/27] chore(ui): regen types --- .../frontend/web/src/services/api/schema.d.ts | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.d.ts b/invokeai/frontend/web/src/services/api/schema.d.ts index 932891c862..6092e822d6 100644 --- a/invokeai/frontend/web/src/services/api/schema.d.ts +++ b/invokeai/frontend/web/src/services/api/schema.d.ts @@ -2942,7 +2942,7 @@ export type components = { * @description The nodes in this graph */ nodes?: { - [key: string]: components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["OnnxModelLoaderInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["ONNXLatentsToImageInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ONNXTextToLatentsInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["ONNXPromptInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["GraphInvocation"]; + [key: string]: components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["ONNXLatentsToImageInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["ONNXPromptInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["OnnxModelLoaderInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["ONNXTextToLatentsInvocation"] | components["schemas"]["InfillColorInvocation"]; }; /** * Edges @@ -2979,7 +2979,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["FaceOffOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ONNXModelLoaderOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["String2Output"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["VaeLoaderOutput"] | components["schemas"]["LatentsOutput"]; + [key: string]: components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["String2Output"] | components["schemas"]["ImageOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["VaeLoaderOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["ONNXModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["FaceMaskOutput"]; }; /** * Errors @@ -8728,23 +8728,11 @@ export type components = { ui_order: number | null; }; /** - * ControlNetModelFormat + * StableDiffusionOnnxModelFormat * @description An enumeration. * @enum {string} */ - ControlNetModelFormat: "checkpoint" | "diffusers"; - /** - * StableDiffusion2ModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; - /** - * IPAdapterModelFormat - * @description An enumeration. - * @enum {string} - */ - IPAdapterModelFormat: "invokeai"; + StableDiffusionOnnxModelFormat: "olive" | "onnx"; /** * CLIPVisionModelFormat * @description An enumeration. @@ -8752,17 +8740,23 @@ export type components = { */ CLIPVisionModelFormat: "diffusers"; /** - * StableDiffusion1ModelFormat + * ControlNetModelFormat * @description An enumeration. * @enum {string} */ - StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; + ControlNetModelFormat: "checkpoint" | "diffusers"; /** - * StableDiffusionOnnxModelFormat + * IPAdapterModelFormat * @description An enumeration. * @enum {string} */ - StableDiffusionOnnxModelFormat: "olive" | "onnx"; + IPAdapterModelFormat: "invokeai"; + /** + * StableDiffusion2ModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; /** * StableDiffusionXLModelFormat * @description An enumeration. @@ -8775,6 +8769,12 @@ export type components = { * @enum {string} */ T2IAdapterModelFormat: "diffusers"; + /** + * StableDiffusion1ModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; }; responses: never; parameters: never; From bbae4045c9f5e643d3ee11611991a141c682ea96 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 17 Oct 2023 23:22:30 +1100 Subject: [PATCH 07/27] fix(nodes): `GraphInvocation` should use `InputField` --- invokeai/app/services/shared/graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/services/shared/graph.py b/invokeai/app/services/shared/graph.py index 0f703db749..e9a4c73d4e 100644 --- a/invokeai/app/services/shared/graph.py +++ b/invokeai/app/services/shared/graph.py @@ -193,7 +193,7 @@ class GraphInvocation(BaseInvocation): """Execute a graph""" # TODO: figure out how to create a default here - graph: "Graph" = Field(description="The graph to run", default=None) + graph: Optional["Graph"] = InputField(description="The graph to run", default=None) def invoke(self, context: InvocationContext) -> GraphInvocationOutput: """Invoke with provided services and return outputs.""" From 7b6e2bc37fd1381a944768d19bb50b0cb27ee1ad Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 17 Oct 2023 23:23:17 +1100 Subject: [PATCH 08/27] feat(nodes): add field name validation Protect against using reserved field names --- invokeai/app/invocations/baseinvocation.py | 109 ++++++++++++++++++--- 1 file changed, 93 insertions(+), 16 deletions(-) diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index 5f1ff0395f..25589510a6 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -2,6 +2,7 @@ from __future__ import annotations +import inspect import re from abc import ABC, abstractmethod from enum import Enum @@ -11,7 +12,7 @@ from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterable, Literal, Op import semver from pydantic import BaseModel, ConfigDict, Field, RootModel, TypeAdapter, create_model -from pydantic.fields import _Unset +from pydantic.fields import FieldInfo, _Unset from pydantic_core import PydanticUndefined from invokeai.app.services.config.config_default import InvokeAIAppConfig @@ -25,6 +26,10 @@ class InvalidVersionError(ValueError): pass +class InvalidFieldError(TypeError): + pass + + class FieldDescriptions: denoising_start = "When to start denoising, expressed a percentage of total steps" denoising_end = "When to stop denoising, expressed a percentage of total steps" @@ -302,6 +307,7 @@ def InputField( ui_order=ui_order, item_default=item_default, ui_choice_labels=ui_choice_labels, + _field_kind="input", ) field_args = dict( @@ -444,6 +450,7 @@ def OutputField( ui_type=ui_type, ui_hidden=ui_hidden, ui_order=ui_order, + _field_kind="output", ), ) @@ -527,6 +534,7 @@ class BaseInvocationOutput(BaseModel): schema["required"].extend(["type"]) model_config = ConfigDict( + protected_namespaces=(), validate_assignment=True, json_schema_serialization_defaults_required=True, json_schema_extra=json_schema_extra, @@ -549,9 +557,6 @@ class MissingInputException(Exception): class BaseInvocation(ABC, BaseModel): """ - A node to process inputs and produce outputs. - May use dependency injection in __init__ to receive providers. - All invocations must use the `@invocation` decorator to provide their unique type. """ @@ -667,17 +672,21 @@ class BaseInvocation(ABC, BaseModel): id: str = Field( default_factory=uuid_string, description="The id of this instance of an invocation. Must be unique among all instances of invocations.", + json_schema_extra=dict(_field_kind="internal"), ) - is_intermediate: Optional[bool] = Field( + is_intermediate: bool = Field( default=False, description="Whether or not this is an intermediate invocation.", - json_schema_extra=dict(ui_type=UIType.IsIntermediate), + json_schema_extra=dict(ui_type=UIType.IsIntermediate, _field_kind="internal"), + ) + use_cache: bool = Field( + default=True, description="Whether or not to use the cache", json_schema_extra=dict(_field_kind="internal") ) - use_cache: bool = InputField(default=True, description="Whether or not to use the cache") UIConfig: ClassVar[Type[UIConfigBase]] model_config = ConfigDict( + protected_namespaces=(), validate_assignment=True, json_schema_extra=json_schema_extra, json_schema_serialization_defaults_required=True, @@ -688,6 +697,70 @@ class BaseInvocation(ABC, BaseModel): TBaseInvocation = TypeVar("TBaseInvocation", bound=BaseInvocation) +RESERVED_INPUT_FIELD_NAMES = { + "id", + "is_intermediate", + "use_cache", + "type", + "workflow", + "metadata", +} + +RESERVED_OUTPUT_FIELD_NAMES = {"type"} + + +class _Model(BaseModel): + pass + + +# Get all pydantic model attrs, methods, etc +RESERVED_PYDANTIC_FIELD_NAMES = set(map(lambda m: m[0], inspect.getmembers(_Model()))) + +print(RESERVED_PYDANTIC_FIELD_NAMES) + + +def validate_fields(model_fields: dict[str, FieldInfo], model_type: str) -> None: + """ + Validates the fields of an invocation or invocation output: + - must not override any pydantic reserved fields + - must be created via `InputField`, `OutputField`, or be an internal field defined in this file + """ + for name, field in model_fields.items(): + if name in RESERVED_PYDANTIC_FIELD_NAMES: + raise InvalidFieldError(f'Invalid field name "{name}" on "{model_type}" (reserved by pydantic)') + + field_kind = ( + # _field_kind is defined via InputField(), OutputField() or by one of the internal fields defined in this file + field.json_schema_extra.get("_field_kind", None) + if field.json_schema_extra + else None + ) + + # must have a field_kind + if field_kind is None or field_kind not in {"input", "output", "internal"}: + raise InvalidFieldError( + f'Invalid field definition for "{name}" on "{model_type}" (maybe it\'s not an InputField or OutputField?)' + ) + + if field_kind == "input" and name in RESERVED_INPUT_FIELD_NAMES: + raise InvalidFieldError(f'Invalid field name "{name}" on "{model_type}" (reserved input field name)') + + if field_kind == "output" and name in RESERVED_OUTPUT_FIELD_NAMES: + raise InvalidFieldError(f'Invalid field name "{name}" on "{model_type}" (reserved output field name)') + + # internal fields *must* be in the reserved list + if ( + field_kind == "internal" + and name not in RESERVED_INPUT_FIELD_NAMES + and name not in RESERVED_OUTPUT_FIELD_NAMES + ): + raise InvalidFieldError( + f'Invalid field name "{name}" on "{model_type}" (internal field without reserved name)' + ) + + return None + + def invocation( invocation_type: str, title: Optional[str] = None, @@ -697,7 +770,7 @@ def invocation( use_cache: Optional[bool] = True, ) -> Callable[[Type[TBaseInvocation]], Type[TBaseInvocation]]: """ - Adds metadata to an invocation. + Registers an invocation. :param str invocation_type: The type of the invocation. Must be unique among all invocations. :param Optional[str] title: Adds a title to the invocation. Use if the auto-generated title isn't quite right. Defaults to None. @@ -716,6 +789,8 @@ def invocation( if invocation_type in BaseInvocation.get_invocation_types(): raise ValueError(f'Invocation type "{invocation_type}" already exists') + validate_fields(cls.model_fields, invocation_type) + # Add OpenAPI schema extras uiconf_name = cls.__qualname__ + ".UIConfig" if not hasattr(cls, "UIConfig") or cls.UIConfig.__qualname__ != uiconf_name: @@ -746,8 +821,7 @@ def invocation( invocation_type_annotation = Literal[invocation_type] # type: ignore invocation_type_field = Field( - title="type", - default=invocation_type, + title="type", default=invocation_type, json_schema_extra=dict(_field_kind="internal") ) docstring = cls.__doc__ @@ -788,13 +862,12 @@ def invocation_output( if output_type in BaseInvocationOutput.get_output_types(): raise ValueError(f'Invocation type "{output_type}" already exists') + validate_fields(cls.model_fields, output_type) + # Add the output type to the model. output_type_annotation = Literal[output_type] # type: ignore - output_type_field = Field( - title="type", - default=output_type, - ) + output_type_field = Field(title="type", default=output_type, json_schema_extra=dict(_field_kind="internal")) docstring = cls.__doc__ cls = create_model( @@ -825,7 +898,9 @@ WorkflowFieldValidator = TypeAdapter(WorkflowField) class WithWorkflow(BaseModel): - workflow: Optional[WorkflowField] = InputField(default=None, description=FieldDescriptions.workflow) + workflow: Optional[WorkflowField] = Field( + default=None, description=FieldDescriptions.workflow, json_schema_extra=dict(_field_kind="internal") + ) class MetadataField(RootModel): @@ -841,4 +916,6 @@ MetadataFieldValidator = TypeAdapter(MetadataField) class WithMetadata(BaseModel): - metadata: Optional[MetadataField] = InputField(default=None, description=FieldDescriptions.metadata) + metadata: Optional[MetadataField] = Field( + default=None, description=FieldDescriptions.metadata, json_schema_extra=dict(_field_kind="internal") + ) From e3e8d8af0233ef9d2bdfc9fef889c864836fc2ff Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 18 Oct 2023 15:38:12 +1100 Subject: [PATCH 09/27] fix(ui): fix log message --- invokeai/frontend/web/src/services/api/endpoints/workflows.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/services/api/endpoints/workflows.ts b/invokeai/frontend/web/src/services/api/endpoints/workflows.ts index 4c69d2e286..7ddd9c5606 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/workflows.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/workflows.ts @@ -19,7 +19,7 @@ export const workflowsApi = api.injectEndpoints({ if (result.success) { return result.data; } else { - logger('images').warn('Problem parsing metadata'); + logger('images').warn('Problem parsing workflow'); } } return; From d32caf7cb1205475ffec054640da5beca959c9c2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 18 Oct 2023 15:38:27 +1100 Subject: [PATCH 10/27] fix(ui): remove references to metadata accumulator --- .../nodes/util/graphBuilders/buildLinearBatchConfig.ts | 5 +---- .../web/src/features/nodes/util/graphBuilders/constants.ts | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearBatchConfig.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearBatchConfig.ts index 313826452c..8bf9a2785a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearBatchConfig.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearBatchConfig.ts @@ -2,13 +2,12 @@ import { NUMPY_RAND_MAX } from 'app/constants'; import { RootState } from 'app/store/store'; import { generateSeeds } from 'common/util/generateSeeds'; import { NonNullableGraph } from 'features/nodes/types/types'; -import { range, unset } from 'lodash-es'; +import { range } from 'lodash-es'; import { components } from 'services/api/schema'; import { Batch, BatchConfig } from 'services/api/types'; import { CANVAS_COHERENCE_NOISE, METADATA, - METADATA_ACCUMULATOR, NOISE, POSITIVE_CONDITIONING, } from './constants'; @@ -149,8 +148,6 @@ export const prepareLinearUIBatch = ( }); if (shouldConcatSDXLStylePrompt && model?.base_model === 'sdxl') { - unset(graph.nodes[METADATA_ACCUMULATOR], 'positive_style_prompt'); - const stylePrompts = extendedPrompts.map((p) => [p, positiveStylePrompt].join(' ') ); diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/constants.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/constants.ts index e0dc52063b..51dc94769f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/constants.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/constants.ts @@ -63,7 +63,6 @@ export const BATCH_SEED = 'batch_seed'; export const BATCH_PROMPT = 'batch_prompt'; export const BATCH_STYLE_PROMPT = 'batch_style_prompt'; export const METADATA_COLLECT = 'metadata_collect'; -export const METADATA_ACCUMULATOR = 'metadata_accumulator'; export const MERGE_METADATA = 'merge_metadata'; export const REALESRGAN = 'esrgan'; export const DIVIDE = 'divide'; From 86c3acf18499db50839b85399b5e8cdfb96b3b7a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 18 Oct 2023 18:14:22 +1100 Subject: [PATCH 11/27] fix(nodes): revert optional graph --- invokeai/app/services/shared/graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/services/shared/graph.py b/invokeai/app/services/shared/graph.py index e9a4c73d4e..b84d456071 100644 --- a/invokeai/app/services/shared/graph.py +++ b/invokeai/app/services/shared/graph.py @@ -193,7 +193,7 @@ class GraphInvocation(BaseInvocation): """Execute a graph""" # TODO: figure out how to create a default here - graph: Optional["Graph"] = InputField(description="The graph to run", default=None) + graph: "Graph" = InputField(description="The graph to run", default=None) def invoke(self, context: InvocationContext) -> GraphInvocationOutput: """Invoke with provided services and return outputs.""" From 6d776bad7e8ff21d608651228c8bda27b238352f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 18 Oct 2023 18:15:01 +1100 Subject: [PATCH 12/27] fix(nodes): remove errant print --- invokeai/app/invocations/baseinvocation.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index 25589510a6..945df4bd83 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -716,8 +716,6 @@ class _Model(BaseModel): # Get all pydantic model attrs, methods, etc RESERVED_PYDANTIC_FIELD_NAMES = set(map(lambda m: m[0], inspect.getmembers(_Model()))) -print(RESERVED_PYDANTIC_FIELD_NAMES) - def validate_fields(model_fields: dict[str, FieldInfo], model_type: str) -> None: """ From 0cda7943fa1fd2f215e5238b753a817752a8761b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 18 Oct 2023 18:16:36 +1100 Subject: [PATCH 13/27] feat(api): add workflow_images junction table similar to boards, images and workflows may be associated via junction table --- invokeai/app/api/dependencies.py | 3 + invokeai/app/api/routers/images.py | 3 + .../image_records/image_records_base.py | 1 - .../image_records/image_records_common.py | 8 -- .../image_records/image_records_sqlite.py | 15 +-- invokeai/app/services/images/images_common.py | 7 + .../app/services/images/images_default.py | 34 +++-- invokeai/app/services/invocation_services.py | 4 + .../workflow_image_records/__init__.py | 0 .../workflow_image_records_base.py | 23 ++++ .../workflow_image_records_sqlite.py | 123 ++++++++++++++++++ 11 files changed, 189 insertions(+), 32 deletions(-) create mode 100644 invokeai/app/services/workflow_image_records/__init__.py create mode 100644 invokeai/app/services/workflow_image_records/workflow_image_records_base.py create mode 100644 invokeai/app/services/workflow_image_records/workflow_image_records_sqlite.py diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index ae4882c0d0..4746eeae3f 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -1,6 +1,7 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) 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__ @@ -91,6 +92,7 @@ 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( @@ -116,6 +118,7 @@ class ApiDependencies: session_processor=session_processor, session_queue=session_queue, urls=urls, + workflow_image_records=workflow_image_records, workflow_records=workflow_records, ) diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index c27ec1e0d9..429eaef37c 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -1,4 +1,5 @@ import io +import traceback from typing import Optional from fastapi import Body, HTTPException, Path, Query, Request, Response, UploadFile @@ -60,6 +61,7 @@ async def upload_image( pil_image = pil_image.crop(bbox) except Exception: # Error opening the image + ApiDependencies.invoker.services.logger.error(traceback.format_exc()) raise HTTPException(status_code=415, detail="Failed to read image") # attempt to parse metadata from image @@ -97,6 +99,7 @@ async def upload_image( return image_dto except Exception: + ApiDependencies.invoker.services.logger.error(traceback.format_exc()) raise HTTPException(status_code=500, detail="Failed to create image") diff --git a/invokeai/app/services/image_records/image_records_base.py b/invokeai/app/services/image_records/image_records_base.py index cd1db81857..655e4b4fb8 100644 --- a/invokeai/app/services/image_records/image_records_base.py +++ b/invokeai/app/services/image_records/image_records_base.py @@ -80,7 +80,6 @@ class ImageRecordStorageBase(ABC): session_id: Optional[str] = None, node_id: Optional[str] = None, metadata: Optional[MetadataField] = None, - workflow_id: Optional[str] = None, ) -> datetime: """Saves an image record.""" pass diff --git a/invokeai/app/services/image_records/image_records_common.py b/invokeai/app/services/image_records/image_records_common.py index 6576fb9647..5a6e5652c9 100644 --- a/invokeai/app/services/image_records/image_records_common.py +++ b/invokeai/app/services/image_records/image_records_common.py @@ -100,7 +100,6 @@ IMAGE_DTO_COLS = ", ".join( "width", "height", "session_id", - "workflow_id", "node_id", "is_intermediate", "created_at", @@ -141,11 +140,6 @@ class ImageRecord(BaseModelExcludeNull): description="The session ID that generated this image, if it is a generated image.", ) """The session ID that generated this image, if it is a generated image.""" - workflow_id: Optional[str] = Field( - default=None, - description="The workflow that generated this image.", - ) - """The workflow that generated this image.""" node_id: Optional[str] = Field( default=None, description="The node ID that generated this image, if it is a generated image.", @@ -190,7 +184,6 @@ def deserialize_image_record(image_dict: dict) -> ImageRecord: width = image_dict.get("width", 0) height = image_dict.get("height", 0) session_id = image_dict.get("session_id", None) - workflow_id = image_dict.get("workflow_id", None) node_id = image_dict.get("node_id", None) created_at = image_dict.get("created_at", get_iso_timestamp()) updated_at = image_dict.get("updated_at", get_iso_timestamp()) @@ -205,7 +198,6 @@ def deserialize_image_record(image_dict: dict) -> ImageRecord: width=width, height=height, session_id=session_id, - workflow_id=workflow_id, node_id=node_id, created_at=created_at, updated_at=updated_at, diff --git a/invokeai/app/services/image_records/image_records_sqlite.py b/invokeai/app/services/image_records/image_records_sqlite.py index dcabe55829..239917b728 100644 --- a/invokeai/app/services/image_records/image_records_sqlite.py +++ b/invokeai/app/services/image_records/image_records_sqlite.py @@ -76,16 +76,6 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): """ ) - if "workflow_id" not in columns: - self._cursor.execute( - """--sql - ALTER TABLE images - ADD COLUMN workflow_id TEXT; - -- TODO: This requires a migration: - -- FOREIGN KEY (workflow_id) REFERENCES workflows (workflow_id) ON DELETE SET NULL; - """ - ) - # Create the `images` table indices. self._cursor.execute( """--sql @@ -423,7 +413,6 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): session_id: Optional[str] = None, node_id: Optional[str] = None, metadata: Optional[MetadataField] = None, - workflow_id: Optional[str] = None, ) -> datetime: try: metadata_json = metadata.model_dump_json() if metadata is not None else None @@ -439,11 +428,10 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): node_id, session_id, metadata, - workflow_id, is_intermediate, starred ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); """, ( image_name, @@ -454,7 +442,6 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): node_id, session_id, metadata_json, - workflow_id, is_intermediate, starred, ), diff --git a/invokeai/app/services/images/images_common.py b/invokeai/app/services/images/images_common.py index 0464244b94..198c26c3a2 100644 --- a/invokeai/app/services/images/images_common.py +++ b/invokeai/app/services/images/images_common.py @@ -24,6 +24,11 @@ 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( @@ -31,6 +36,7 @@ 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( @@ -38,4 +44,5 @@ 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 a0d59470fc..8eb768a1b9 100644 --- a/invokeai/app/services/images/images_default.py +++ b/invokeai/app/services/images/images_default.py @@ -74,11 +74,12 @@ class ImageService(ImageServiceABC): # Nullable fields node_id=node_id, metadata=metadata, - workflow_id=workflow_id, session_id=session_id, ) 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 ) @@ -138,10 +139,11 @@ class ImageService(ImageServiceABC): image_record = self.__invoker.services.image_records.get(image_name) image_dto = image_record_to_dto( - image_record, - self.__invoker.services.urls.get_image_url(image_name), - self.__invoker.services.urls.get_image_url(image_name, True), - self.__invoker.services.board_image_records.get_board_for_image(image_name), + image_record=image_record, + 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 @@ -162,6 +164,19 @@ class ImageService(ImageServiceABC): self.__invoker.services.logger.error("Problem getting image DTO") raise e + def get_workflow(self, image_name: str) -> Optional[WorkflowField]: + 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") + 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: return str(self.__invoker.services.image_files.get_path(image_name, thumbnail)) @@ -205,10 +220,11 @@ class ImageService(ImageServiceABC): image_dtos = list( map( lambda r: image_record_to_dto( - r, - self.__invoker.services.urls.get_image_url(r.image_name), - self.__invoker.services.urls.get_image_url(r.image_name, True), - self.__invoker.services.board_image_records.get_board_for_image(r.image_name), + image_record=r, + 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), ), results.items, ) diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index 94db75d810..804b1b6884 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -28,6 +28,7 @@ if TYPE_CHECKING: from .shared.graph import GraphExecutionState, LibraryGraph from .urls.urls_base import UrlServiceBase from .workflow_records.workflow_records_base import WorkflowRecordsStorageBase + from .workflow_image_records.workflow_image_records_base import WorkflowImageRecordsStorageBase class InvocationServices: @@ -56,6 +57,7 @@ class InvocationServices: invocation_cache: "InvocationCacheBase" names: "NameServiceBase" urls: "UrlServiceBase" + workflow_image_records: "WorkflowImageRecordsStorageBase" workflow_records: "WorkflowRecordsStorageBase" def __init__( @@ -82,6 +84,7 @@ class InvocationServices: invocation_cache: "InvocationCacheBase", names: "NameServiceBase", urls: "UrlServiceBase", + workflow_image_records: "WorkflowImageRecordsStorageBase", workflow_records: "WorkflowRecordsStorageBase", ): self.board_images = board_images @@ -106,4 +109,5 @@ 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/workflow_image_records/__init__.py b/invokeai/app/services/workflow_image_records/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 new file mode 100644 index 0000000000..d99a2ba106 --- /dev/null +++ b/invokeai/app/services/workflow_image_records/workflow_image_records_base.py @@ -0,0 +1,23 @@ +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 new file mode 100644 index 0000000000..1a5de672bc --- /dev/null +++ b/invokeai/app/services/workflow_image_records/workflow_image_records_sqlite.py @@ -0,0 +1,123 @@ +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 (?, ?) + ON CONFLICT (image_name) DO UPDATE SET workflow_id = ?; + """, + (workflow_id, image_name, workflow_id), + ) + 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() From 23fa2e560aaa0625936faea8c073623f29f0dc2e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 18 Oct 2023 18:27:29 +1100 Subject: [PATCH 14/27] fix: fix tests --- tests/nodes/test_graph_execution_state.py | 1 + tests/nodes/test_invoker.py | 1 + tests/nodes/test_nodes.py | 31 ++++++++++++----------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/tests/nodes/test_graph_execution_state.py b/tests/nodes/test_graph_execution_state.py index e2d435e621..171cdfdb6f 100644 --- a/tests/nodes/test_graph_execution_state.py +++ b/tests/nodes/test_graph_execution_state.py @@ -76,6 +76,7 @@ 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_invoker.py b/tests/nodes/test_invoker.py index 9774f07fdd..25b02955b0 100644 --- a/tests/nodes/test_invoker.py +++ b/tests/nodes/test_invoker.py @@ -81,6 +81,7 @@ 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_nodes.py b/tests/nodes/test_nodes.py index 7807a56879..1d7f2e4194 100644 --- a/tests/nodes/test_nodes.py +++ b/tests/nodes/test_nodes.py @@ -1,11 +1,12 @@ from typing import Any, Callable, Union -from pydantic import Field from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, + InputField, InvocationContext, + OutputField, invocation, invocation_output, ) @@ -15,12 +16,12 @@ from invokeai.app.invocations.image import ImageField # Define test invocations before importing anything that uses invocations @invocation_output("test_list_output") class ListPassThroughInvocationOutput(BaseInvocationOutput): - collection: list[ImageField] = Field(default_factory=list) + collection: list[ImageField] = OutputField(default_factory=list) @invocation("test_list") class ListPassThroughInvocation(BaseInvocation): - collection: list[ImageField] = Field(default_factory=list) + collection: list[ImageField] = InputField(default_factory=list) def invoke(self, context: InvocationContext) -> ListPassThroughInvocationOutput: return ListPassThroughInvocationOutput(collection=self.collection) @@ -28,12 +29,12 @@ class ListPassThroughInvocation(BaseInvocation): @invocation_output("test_prompt_output") class PromptTestInvocationOutput(BaseInvocationOutput): - prompt: str = Field(default="") + prompt: str = OutputField(default="") @invocation("test_prompt") class PromptTestInvocation(BaseInvocation): - prompt: str = Field(default="") + prompt: str = InputField(default="") def invoke(self, context: InvocationContext) -> PromptTestInvocationOutput: return PromptTestInvocationOutput(prompt=self.prompt) @@ -47,13 +48,13 @@ class ErrorInvocation(BaseInvocation): @invocation_output("test_image_output") class ImageTestInvocationOutput(BaseInvocationOutput): - image: ImageField = Field() + image: ImageField = OutputField() @invocation("test_text_to_image") class TextToImageTestInvocation(BaseInvocation): - prompt: str = Field(default="") - prompt2: str = Field(default="") + prompt: str = InputField(default="") + prompt2: str = InputField(default="") def invoke(self, context: InvocationContext) -> ImageTestInvocationOutput: return ImageTestInvocationOutput(image=ImageField(image_name=self.id)) @@ -61,8 +62,8 @@ class TextToImageTestInvocation(BaseInvocation): @invocation("test_image_to_image") class ImageToImageTestInvocation(BaseInvocation): - prompt: str = Field(default="") - image: Union[ImageField, None] = Field(default=None) + prompt: str = InputField(default="") + image: Union[ImageField, None] = InputField(default=None) def invoke(self, context: InvocationContext) -> ImageTestInvocationOutput: return ImageTestInvocationOutput(image=ImageField(image_name=self.id)) @@ -70,12 +71,12 @@ class ImageToImageTestInvocation(BaseInvocation): @invocation_output("test_prompt_collection_output") class PromptCollectionTestInvocationOutput(BaseInvocationOutput): - collection: list[str] = Field(default_factory=list) + collection: list[str] = OutputField(default_factory=list) @invocation("test_prompt_collection") class PromptCollectionTestInvocation(BaseInvocation): - collection: list[str] = Field() + collection: list[str] = InputField() def invoke(self, context: InvocationContext) -> PromptCollectionTestInvocationOutput: return PromptCollectionTestInvocationOutput(collection=self.collection.copy()) @@ -83,12 +84,12 @@ class PromptCollectionTestInvocation(BaseInvocation): @invocation_output("test_any_output") class AnyTypeTestInvocationOutput(BaseInvocationOutput): - value: Any = Field() + value: Any = OutputField() @invocation("test_any") class AnyTypeTestInvocation(BaseInvocation): - value: Any = Field(default=None) + value: Any = InputField(default=None) def invoke(self, context: InvocationContext) -> AnyTypeTestInvocationOutput: return AnyTypeTestInvocationOutput(value=self.value) @@ -96,7 +97,7 @@ class AnyTypeTestInvocation(BaseInvocation): @invocation("test_polymorphic") class PolymorphicStringTestInvocation(BaseInvocation): - value: Union[str, list[str]] = Field(default="") + value: Union[str, list[str]] = InputField(default="") def invoke(self, context: InvocationContext) -> PromptCollectionTestInvocationOutput: if isinstance(self.value, str): From 2faed653d75547ac40b955971b6ca481b82586d7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 18 Oct 2023 20:10:17 +1100 Subject: [PATCH 15/27] fix(api): deduplicate metadata/workflow extraction logic --- invokeai/app/api/routers/images.py | 2 +- .../services/image_files/image_files_disk.py | 18 ++++-------------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 429eaef37c..a57414e17f 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -60,10 +60,10 @@ async def upload_image( bbox = pil_image.getbbox() pil_image = pil_image.crop(bbox) except Exception: - # Error opening the image ApiDependencies.invoker.services.logger.error(traceback.format_exc()) raise HTTPException(status_code=415, detail="Failed to read image") + # TODO: retain non-invokeai metadata on upload? # attempt to parse metadata from image metadata_raw = pil_image.info.get("invokeai_metadata", None) if metadata_raw: diff --git a/invokeai/app/services/image_files/image_files_disk.py b/invokeai/app/services/image_files/image_files_disk.py index e8a733d619..91c1e14789 100644 --- a/invokeai/app/services/image_files/image_files_disk.py +++ b/invokeai/app/services/image_files/image_files_disk.py @@ -65,20 +65,10 @@ class DiskImageFileStorage(ImageFileStorageBase): pnginfo = PngImagePlugin.PngInfo() - if metadata is not None or workflow is not None: - if metadata is not None: - pnginfo.add_text("invokeai_metadata", metadata.model_dump_json()) - if workflow is not None: - pnginfo.add_text("invokeai_workflow", workflow.model_dump_json()) - else: - # For uploaded images, we want to retain metadata. PIL strips it on save; manually add it back - # TODO: retain non-invokeai metadata on save... - original_metadata = image.info.get("invokeai_metadata", None) - if original_metadata is not None: - pnginfo.add_text("invokeai_metadata", original_metadata) - original_workflow = image.info.get("invokeai_workflow", None) - if original_workflow is not None: - pnginfo.add_text("invokeai_workflow", original_workflow) + if metadata is not None: + pnginfo.add_text("invokeai_metadata", metadata.model_dump_json()) + if workflow is not None: + pnginfo.add_text("invokeai_workflow", workflow.model_dump_json()) image.save( image_path, From f04462973b091f02ef15046cecfaf266de7ab0bb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 18 Oct 2023 21:23:09 +1100 Subject: [PATCH 16/27] feat(ui): create debounced metadata/workflow query hooks Also added config options for metadata and workflow debounce times (`metadataFetchDebounce` & `workflowFetchDebounce`). Falls back to 0 if not provided. In OSS, because we have no major latency concerns, the debounce is 0. But in other environments, it may be desirable to set this to something like 300ms. --- .../frontend/web/src/app/types/invokeai.ts | 2 ++ .../CurrentImage/CurrentImageButtons.tsx | 22 ++++++------------- .../SingleSelectionMenuItems.tsx | 19 ++++++---------- .../ImageMetadataViewer.tsx | 18 ++++----------- .../src/services/api/endpoints/workflows.ts | 1 - .../api/hooks/useDebouncedMetadata.ts | 21 ++++++++++++++++++ .../api/hooks/useDebouncedWorkflow.ts | 21 ++++++++++++++++++ 7 files changed, 62 insertions(+), 42 deletions(-) create mode 100644 invokeai/frontend/web/src/services/api/hooks/useDebouncedMetadata.ts create mode 100644 invokeai/frontend/web/src/services/api/hooks/useDebouncedWorkflow.ts diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index 39e4ffd27a..0fe7a36052 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -59,6 +59,8 @@ export type AppConfig = { nodesAllowlist: string[] | undefined; nodesDenylist: string[] | undefined; maxUpscalePixels?: number; + metadataFetchDebounce?: number; + workflowFetchDebounce?: number; sd: { defaultModel?: string; disabledControlNetModels: string[]; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx index 4c0aa5e0e8..36a251952c 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx @@ -38,15 +38,12 @@ import { FaSeedling, } from 'react-icons/fa'; import { FaCircleNodes, FaEllipsis } from 'react-icons/fa6'; -import { - useGetImageDTOQuery, - useGetImageMetadataQuery, -} from 'services/api/endpoints/images'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; +import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata'; +import { useDebouncedWorkflow } from 'services/api/hooks/useDebouncedWorkflow'; import { menuListMotionProps } from 'theme/components/menu'; -import { useDebounce } from 'use-debounce'; import { sentImageToImg2Img } from '../../store/actions'; import SingleSelectionMenuItems from '../ImageContextMenu/SingleSelectionMenuItems'; -import { useGetWorkflowQuery } from 'services/api/endpoints/workflows'; const currentImageButtonsSelector = createSelector( [stateSelector, activeTabNameSelector], @@ -105,17 +102,12 @@ const CurrentImageButtons = () => { lastSelectedImage?.image_name ?? skipToken ); - const [debouncedImageName] = useDebounce(lastSelectedImage?.image_name, 300); - const [debouncedWorkflowId] = useDebounce( - lastSelectedImage?.workflow_id, - 300 + const { metadata, isLoading: isLoadingMetadata } = useDebouncedMetadata( + lastSelectedImage?.image_name ); - const { data: metadata, isLoading: isLoadingMetadata } = - useGetImageMetadataQuery(debouncedImageName ?? skipToken); - - const { data: workflow, isLoading: isLoadingWorkflow } = useGetWorkflowQuery( - debouncedWorkflowId ?? skipToken + const { workflow, isLoading: isLoadingWorkflow } = useDebouncedWorkflow( + lastSelectedImage?.workflow_id ); const handleLoadWorkflow = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index 38de235e38..ed12eb5ff4 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -1,6 +1,5 @@ import { Flex, MenuItem, Spinner } from '@chakra-ui/react'; import { useStore } from '@nanostores/react'; -import { skipToken } from '@reduxjs/toolkit/dist/query'; import { useAppToaster } from 'app/components/Toaster'; import { $customStarUI } from 'app/store/nanostores/customStarUI'; import { useAppDispatch } from 'app/store/storeHooks'; @@ -33,13 +32,12 @@ import { import { FaCircleNodes } from 'react-icons/fa6'; import { MdStar, MdStarBorder } from 'react-icons/md'; import { - useGetImageMetadataQuery, useStarImagesMutation, useUnstarImagesMutation, } from 'services/api/endpoints/images'; -import { useGetWorkflowQuery } from 'services/api/endpoints/workflows'; +import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata'; +import { useDebouncedWorkflow } from 'services/api/hooks/useDebouncedWorkflow'; import { ImageDTO } from 'services/api/types'; -import { useDebounce } from 'use-debounce'; import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions'; type SingleSelectionMenuItemsProps = { @@ -57,14 +55,11 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled; const customStarUi = useStore($customStarUI); - const [debouncedImageName] = useDebounce(imageDTO?.image_name, 300); - const [debouncedWorkflowId] = useDebounce(imageDTO?.workflow_id, 300); - - const { data: metadata, isLoading: isLoadingMetadata } = - useGetImageMetadataQuery(debouncedImageName ?? skipToken); - - const { data: workflow, isLoading: isLoadingWorkflow } = useGetWorkflowQuery( - debouncedWorkflowId ?? skipToken + const { metadata, isLoading: isLoadingMetadata } = useDebouncedMetadata( + imageDTO?.image_name + ); + const { workflow, isLoading: isLoadingWorkflow } = useDebouncedWorkflow( + imageDTO?.workflow_id ); const [starImages] = useStarImagesMutation(); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx index f6820b9d20..29637e4a3c 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx @@ -9,15 +9,13 @@ import { Tabs, Text, } from '@chakra-ui/react'; -import { skipToken } from '@reduxjs/toolkit/dist/query'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useGetImageMetadataQuery } from 'services/api/endpoints/images'; -import { useGetWorkflowQuery } from 'services/api/endpoints/workflows'; +import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata'; +import { useDebouncedWorkflow } from 'services/api/hooks/useDebouncedWorkflow'; import { ImageDTO } from 'services/api/types'; -import { useDebounce } from 'use-debounce'; import DataViewer from './DataViewer'; import ImageMetadataActions from './ImageMetadataActions'; @@ -33,16 +31,8 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => { // }); const { t } = useTranslation(); - const [debouncedImageName] = useDebounce(image.image_name, 300); - const [debouncedWorkflowId] = useDebounce(image.workflow_id, 300); - - const { data: metadata } = useGetImageMetadataQuery( - debouncedImageName ?? skipToken - ); - - const { data: workflow } = useGetWorkflowQuery( - debouncedWorkflowId ?? skipToken - ); + const { metadata } = useDebouncedMetadata(image.image_name); + const { workflow } = useDebouncedWorkflow(image.workflow_id); return ( ({ getWorkflow: build.query({ query: (workflow_id) => `workflows/i/${workflow_id}`, - keepUnusedDataFor: 86400, // 24 hours providesTags: (result, error, workflow_id) => [ { type: 'Workflow', id: workflow_id }, ], diff --git a/invokeai/frontend/web/src/services/api/hooks/useDebouncedMetadata.ts b/invokeai/frontend/web/src/services/api/hooks/useDebouncedMetadata.ts new file mode 100644 index 0000000000..023b3c140c --- /dev/null +++ b/invokeai/frontend/web/src/services/api/hooks/useDebouncedMetadata.ts @@ -0,0 +1,21 @@ +import { skipToken } from '@reduxjs/toolkit/query'; +import { useDebounce } from 'use-debounce'; +import { useGetImageMetadataQuery } from '../endpoints/images'; +import { useAppSelector } from 'app/store/storeHooks'; + +export const useDebouncedMetadata = (imageName?: string | null) => { + const metadataFetchDebounce = useAppSelector( + (state) => state.config.metadataFetchDebounce + ); + + const [debouncedImageName] = useDebounce( + imageName, + metadataFetchDebounce ?? 0 + ); + + const { data: metadata, isLoading } = useGetImageMetadataQuery( + debouncedImageName ?? skipToken + ); + + return { metadata, isLoading }; +}; diff --git a/invokeai/frontend/web/src/services/api/hooks/useDebouncedWorkflow.ts b/invokeai/frontend/web/src/services/api/hooks/useDebouncedWorkflow.ts new file mode 100644 index 0000000000..2731597b2c --- /dev/null +++ b/invokeai/frontend/web/src/services/api/hooks/useDebouncedWorkflow.ts @@ -0,0 +1,21 @@ +import { skipToken } from '@reduxjs/toolkit/query'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useDebounce } from 'use-debounce'; +import { useGetWorkflowQuery } from '../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 }; +}; From 91049799430b67cb3c86d42a5e33b4dc586bc357 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 18 Oct 2023 21:24:07 +1100 Subject: [PATCH 17/27] chore(ui): regen types --- .../frontend/web/src/services/api/schema.d.ts | 298 +++++++++--------- 1 file changed, 149 insertions(+), 149 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.d.ts b/invokeai/frontend/web/src/services/api/schema.d.ts index 6092e822d6..5541fa20e9 100644 --- a/invokeai/frontend/web/src/services/api/schema.d.ts +++ b/invokeai/frontend/web/src/services/api/schema.d.ts @@ -419,7 +419,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -588,7 +588,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -646,7 +646,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -894,7 +894,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -945,7 +945,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -1033,7 +1033,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -1079,7 +1079,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -1150,7 +1150,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -1207,7 +1207,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -1282,7 +1282,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -1349,7 +1349,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -1392,7 +1392,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -1443,7 +1443,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -1483,7 +1483,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -1545,7 +1545,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -1594,7 +1594,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -1694,7 +1694,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -1832,7 +1832,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -2019,7 +2019,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -2088,7 +2088,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -2144,7 +2144,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -2261,7 +2261,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -2302,11 +2302,11 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache - * @default true + * @default false */ use_cache?: boolean; /** @@ -2359,7 +2359,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -2452,7 +2452,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -2499,7 +2499,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -2596,7 +2596,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -2699,7 +2699,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -2750,7 +2750,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -2785,7 +2785,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -2832,7 +2832,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -2897,7 +2897,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -2942,7 +2942,7 @@ export type components = { * @description The nodes in this graph */ nodes?: { - [key: string]: components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["ONNXLatentsToImageInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["ONNXPromptInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["OnnxModelLoaderInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["ONNXTextToLatentsInvocation"] | components["schemas"]["InfillColorInvocation"]; + [key: string]: components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["OpenposeImageProcessorInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["OnnxModelLoaderInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ONNXTextToLatentsInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["ONNXPromptInvocation"] | components["schemas"]["ONNXLatentsToImageInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ImageMultiplyInvocation"]; }; /** * Edges @@ -2979,7 +2979,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["String2Output"] | components["schemas"]["ImageOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["VaeLoaderOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["ONNXModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["FaceMaskOutput"]; + [key: string]: components["schemas"]["FaceOffOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["ONNXModelLoaderOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["String2Output"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["VaeLoaderOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["FaceMaskOutput"]; }; /** * Errors @@ -3018,7 +3018,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -3067,7 +3067,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -3146,7 +3146,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -3281,7 +3281,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -3341,7 +3341,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -3383,7 +3383,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -3436,7 +3436,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -3479,7 +3479,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -3534,7 +3534,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -3576,7 +3576,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -3675,11 +3675,6 @@ export type components = { * @description The session ID that generated this image, if it is a generated image. */ session_id?: string | null; - /** - * Workflow Id - * @description The workflow that generated this image. - */ - workflow_id?: string | null; /** * Node Id * @description The node ID that generated this image, if it is a generated image. @@ -3695,6 +3690,11 @@ export type components = { * @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 @@ -3726,7 +3726,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -3767,7 +3767,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -3810,7 +3810,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -3845,7 +3845,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -3892,7 +3892,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -3929,7 +3929,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -3988,7 +3988,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -4075,7 +4075,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -4129,7 +4129,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -4173,7 +4173,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -4243,7 +4243,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -4300,7 +4300,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -4345,7 +4345,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -4393,7 +4393,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -4435,7 +4435,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -4486,7 +4486,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -4521,7 +4521,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -4614,7 +4614,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -4675,7 +4675,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -4706,7 +4706,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -4773,7 +4773,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -4832,7 +4832,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -4881,7 +4881,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -4946,7 +4946,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -4993,7 +4993,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -5115,7 +5115,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -5202,7 +5202,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -5237,7 +5237,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -5274,7 +5274,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -5329,7 +5329,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -5370,7 +5370,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -5418,7 +5418,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -5490,7 +5490,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -5537,7 +5537,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -5605,7 +5605,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -5652,7 +5652,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -5768,7 +5768,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -5827,7 +5827,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -5907,7 +5907,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -5954,7 +5954,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -6016,7 +6016,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -6102,7 +6102,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -6231,7 +6231,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -6266,7 +6266,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -6319,7 +6319,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -6374,7 +6374,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -6441,11 +6441,11 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache - * @default true + * @default false */ use_cache?: boolean; /** @@ -6488,11 +6488,11 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache - * @default true + * @default false */ use_cache?: boolean; /** @@ -6529,11 +6529,11 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache - * @default true + * @default false */ use_cache?: boolean; /** @@ -6581,7 +6581,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -6628,7 +6628,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -6683,7 +6683,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -6747,7 +6747,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -6788,7 +6788,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -6869,7 +6869,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -6951,7 +6951,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -7014,7 +7014,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -7077,7 +7077,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -7139,11 +7139,11 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache - * @default true + * @default false */ use_cache?: boolean; /** @description The image to process */ @@ -7172,7 +7172,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -7221,7 +7221,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -7277,7 +7277,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -7354,7 +7354,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -7593,7 +7593,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -7780,7 +7780,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -7890,7 +7890,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -7941,7 +7941,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -7976,7 +7976,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -8017,7 +8017,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -8103,7 +8103,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -8156,7 +8156,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -8197,7 +8197,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -8237,7 +8237,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -8310,7 +8310,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -8440,7 +8440,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -8530,7 +8530,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -8623,7 +8623,7 @@ export type components = { * @description Whether or not this is an intermediate invocation. * @default false */ - is_intermediate?: boolean | null; + is_intermediate?: boolean; /** * Use Cache * @description Whether or not to use the cache @@ -8728,11 +8728,11 @@ export type components = { ui_order: number | null; }; /** - * StableDiffusionOnnxModelFormat + * StableDiffusion2ModelFormat * @description An enumeration. * @enum {string} */ - StableDiffusionOnnxModelFormat: "olive" | "onnx"; + StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; /** * CLIPVisionModelFormat * @description An enumeration. @@ -8740,11 +8740,23 @@ export type components = { */ CLIPVisionModelFormat: "diffusers"; /** - * ControlNetModelFormat + * StableDiffusionXLModelFormat * @description An enumeration. * @enum {string} */ - ControlNetModelFormat: "checkpoint" | "diffusers"; + StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; + /** + * StableDiffusion1ModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; + /** + * StableDiffusionOnnxModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusionOnnxModelFormat: "olive" | "onnx"; /** * IPAdapterModelFormat * @description An enumeration. @@ -8752,29 +8764,17 @@ export type components = { */ IPAdapterModelFormat: "invokeai"; /** - * StableDiffusion2ModelFormat + * ControlNetModelFormat * @description An enumeration. * @enum {string} */ - StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; - /** - * StableDiffusionXLModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; + ControlNetModelFormat: "checkpoint" | "diffusers"; /** * T2IAdapterModelFormat * @description An enumeration. * @enum {string} */ T2IAdapterModelFormat: "diffusers"; - /** - * StableDiffusion1ModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; }; responses: never; parameters: never; From b5940039f3051c3b9ed0474b6428ac1981e06893 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 18 Oct 2023 21:24:21 +1100 Subject: [PATCH 18/27] chore: lint --- invokeai/app/api/dependencies.py | 2 +- invokeai/app/api/routers/images.py | 6 +----- invokeai/app/services/invocation_services.py | 2 +- .../workflow_image_records/workflow_image_records_sqlite.py | 2 +- tests/nodes/test_nodes.py | 1 - 5 files changed, 4 insertions(+), 9 deletions(-) diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index 4746eeae3f..e7c8fa7fae 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -1,8 +1,8 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) from logging import Logger -from invokeai.app.services.workflow_image_records.workflow_image_records_sqlite import SqliteWorkflowImageRecordsStorage +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__ diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index a57414e17f..e8c8c693b3 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -8,11 +8,7 @@ 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, WorkflowFieldValidator 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 diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index 804b1b6884..d405201f4e 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -27,8 +27,8 @@ 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_records.workflow_records_base import WorkflowRecordsStorageBase from .workflow_image_records.workflow_image_records_base import WorkflowImageRecordsStorageBase + from .workflow_records.workflow_records_base import WorkflowRecordsStorageBase class InvocationServices: 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 index 1a5de672bc..912d80cbf6 100644 --- a/invokeai/app/services/workflow_image_records/workflow_image_records_sqlite.py +++ b/invokeai/app/services/workflow_image_records/workflow_image_records_sqlite.py @@ -1,8 +1,8 @@ import sqlite3 import threading from typing import Optional, cast -from invokeai.app.services.shared.sqlite import SqliteDatabase +from invokeai.app.services.shared.sqlite import SqliteDatabase from invokeai.app.services.workflow_image_records.workflow_image_records_base import WorkflowImageRecordsStorageBase diff --git a/tests/nodes/test_nodes.py b/tests/nodes/test_nodes.py index 1d7f2e4194..51b33dd4c7 100644 --- a/tests/nodes/test_nodes.py +++ b/tests/nodes/test_nodes.py @@ -1,6 +1,5 @@ from typing import Any, Callable, Union - from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, From 16dacb5f430072a2b86feb7983d8055264fda82a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 18 Oct 2023 21:36:29 +1100 Subject: [PATCH 19/27] fix(nodes): remove constraints on ip adapter metadata fields --- invokeai/app/invocations/metadata.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/invokeai/app/invocations/metadata.py b/invokeai/app/invocations/metadata.py index 98f5f0e830..1ed399873b 100644 --- a/invokeai/app/invocations/metadata.py +++ b/invokeai/app/invocations/metadata.py @@ -43,16 +43,10 @@ class IPAdapterMetadataField(BaseModel): description="The IP-Adapter model.", ) weight: Union[float, list[float]] = Field( - default=1, - ge=0, description="The weight given to the IP-Adapter", ) - begin_step_percent: float = Field( - default=0, ge=-1, le=2, description="When the IP-Adapter is first applied (% of total steps)" - ) - end_step_percent: float = Field( - default=1, ge=0, le=1, description="When the IP-Adapter is last applied (% of total steps)" - ) + begin_step_percent: float = Field(description="When the IP-Adapter is first applied (% of total steps)") + end_step_percent: float = Field(description="When the IP-Adapter is last applied (% of total steps)") @invocation_output("metadata_item_output") From 52fbd1b222866ba151a7c6b4044d17e063e2fd59 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 18 Oct 2023 22:18:06 +1100 Subject: [PATCH 20/27] fix(ui): remove errant comment --- .../src/features/nodes/util/graphBuilders/buildNodesGraph.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildNodesGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildNodesGraph.ts index 4437e14f66..eb782f456a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildNodesGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildNodesGraph.ts @@ -35,7 +35,6 @@ export const buildNodesGraph = (nodesState: NodesState): Graph => { const { nodes, edges } = nodesState; const filteredNodes = nodes.filter(isInvocationNode); - // const workflowJSON = JSON.stringify(buildWorkflow(nodesState)); // Reduce the node editor nodes into invocation graph nodes const parsedNodes = filteredNodes.reduce>( @@ -68,7 +67,6 @@ export const buildNodesGraph = (nodesState: NodesState): Graph => { if (embedWorkflow) { // add the workflow to the node - // Object.assign(graphNode, { workflow: workflowJSON }); Object.assign(graphNode, { workflow: buildWorkflow(nodesState) }); } From 301a8fef92d15d485be5b20b9c8d4a6b65729624 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 18 Oct 2023 22:33:17 +1100 Subject: [PATCH 21/27] fix(ui): fix batch metadata logic when graph has no metadata On canvas, images have no metadata yet, so this needs to be handled --- .../graphBuilders/buildLinearBatchConfig.ts | 74 +++++++++++-------- .../nodes/util/graphBuilders/metadata.ts | 8 ++ 2 files changed, 50 insertions(+), 32 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearBatchConfig.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearBatchConfig.ts index 8bf9a2785a..59f8d4123f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearBatchConfig.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearBatchConfig.ts @@ -11,7 +11,7 @@ import { NOISE, POSITIVE_CONDITIONING, } from './constants'; -import { removeMetadata } from './metadata'; +import { getHasMetadata, removeMetadata } from './metadata'; export const prepareLinearUIBatch = ( state: RootState, @@ -40,13 +40,15 @@ export const prepareLinearUIBatch = ( }); } - // add to metadata - removeMetadata(graph, 'seed'); - zipped.push({ - node_path: METADATA, - field_name: 'seed', - items: seeds, - }); + if (getHasMetadata(graph)) { + // add to metadata + removeMetadata(graph, 'seed'); + zipped.push({ + node_path: METADATA, + field_name: 'seed', + items: seeds, + }); + } if (graph.nodes[CANVAS_COHERENCE_NOISE]) { zipped.push({ @@ -78,12 +80,14 @@ export const prepareLinearUIBatch = ( } // add to metadata - removeMetadata(graph, 'seed'); - firstBatchDatumList.push({ - node_path: METADATA, - field_name: 'seed', - items: seeds, - }); + if (getHasMetadata(graph)) { + removeMetadata(graph, 'seed'); + firstBatchDatumList.push({ + node_path: METADATA, + field_name: 'seed', + items: seeds, + }); + } if (graph.nodes[CANVAS_COHERENCE_NOISE]) { firstBatchDatumList.push({ @@ -108,12 +112,14 @@ export const prepareLinearUIBatch = ( } // add to metadata - removeMetadata(graph, 'seed'); - secondBatchDatumList.push({ - node_path: METADATA, - field_name: 'seed', - items: seeds, - }); + if (getHasMetadata(graph)) { + removeMetadata(graph, 'seed'); + secondBatchDatumList.push({ + node_path: METADATA, + field_name: 'seed', + items: seeds, + }); + } if (graph.nodes[CANVAS_COHERENCE_NOISE]) { secondBatchDatumList.push({ @@ -140,12 +146,14 @@ export const prepareLinearUIBatch = ( } // add to metadata - removeMetadata(graph, 'positive_prompt'); - firstBatchDatumList.push({ - node_path: METADATA, - field_name: 'positive_prompt', - items: extendedPrompts, - }); + if (getHasMetadata(graph)) { + removeMetadata(graph, 'positive_prompt'); + firstBatchDatumList.push({ + node_path: METADATA, + field_name: 'positive_prompt', + items: extendedPrompts, + }); + } if (shouldConcatSDXLStylePrompt && model?.base_model === 'sdxl') { const stylePrompts = extendedPrompts.map((p) => @@ -161,12 +169,14 @@ export const prepareLinearUIBatch = ( } // add to metadata - removeMetadata(graph, 'positive_style_prompt'); - firstBatchDatumList.push({ - node_path: METADATA, - field_name: 'positive_style_prompt', - items: extendedPrompts, - }); + if (getHasMetadata(graph)) { + removeMetadata(graph, 'positive_style_prompt'); + firstBatchDatumList.push({ + node_path: METADATA, + field_name: 'positive_style_prompt', + items: extendedPrompts, + }); + } } data.push(firstBatchDatumList); diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/metadata.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/metadata.ts index 547c45addf..5cc397ce68 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/metadata.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/metadata.ts @@ -56,3 +56,11 @@ export const removeMetadata = ( delete metadataNode[key]; }; + +export const getHasMetadata = (graph: NonNullableGraph): boolean => { + const metadataNode = graph.nodes[METADATA] as + | CoreMetadataInvocation + | undefined; + + return Boolean(metadataNode); +}; From 2f4f83280b7240a1aeeddc09a3395b32c19185f3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 19 Oct 2023 07:37:28 +1100 Subject: [PATCH 22/27] fix(db): remove extraneous conflict handling in workflow image records --- .../workflow_image_records/workflow_image_records_sqlite.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 index 912d80cbf6..ec7a73f1d5 100644 --- a/invokeai/app/services/workflow_image_records/workflow_image_records_sqlite.py +++ b/invokeai/app/services/workflow_image_records/workflow_image_records_sqlite.py @@ -85,10 +85,9 @@ class SqliteWorkflowImageRecordsStorage(WorkflowImageRecordsStorageBase): self._cursor.execute( """--sql INSERT INTO workflow_images (workflow_id, image_name) - VALUES (?, ?) - ON CONFLICT (image_name) DO UPDATE SET workflow_id = ?; + VALUES (?, ?); """, - (workflow_id, image_name, workflow_id), + (workflow_id, image_name), ) self._conn.commit() except sqlite3.Error as e: From c071262c20b7912e6d262a54097806e16ad226f9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 19 Oct 2023 08:16:28 +1100 Subject: [PATCH 23/27] fix(ui): remove getMetadataFromFile query & util This will all be handled by python going forward --- .../getMetadataAndWorkflowFromImageBlob.ts | 45 ------------ .../web/src/services/api/endpoints/images.ts | 72 +------------------ 2 files changed, 1 insertion(+), 116 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/nodes/util/getMetadataAndWorkflowFromImageBlob.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/getMetadataAndWorkflowFromImageBlob.ts b/invokeai/frontend/web/src/features/nodes/util/getMetadataAndWorkflowFromImageBlob.ts deleted file mode 100644 index b46a701757..0000000000 --- a/invokeai/frontend/web/src/features/nodes/util/getMetadataAndWorkflowFromImageBlob.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as png from '@stevebel/png'; -import { logger } from 'app/logging/logger'; -import { parseify } from 'common/util/serialize'; -import { - ImageMetadataAndWorkflow, - zCoreMetadata, - zWorkflow, -} from 'features/nodes/types/types'; -import { get } from 'lodash-es'; - -export const getMetadataAndWorkflowFromImageBlob = async ( - image: Blob -): Promise => { - const data: ImageMetadataAndWorkflow = {}; - const buffer = await image.arrayBuffer(); - const text = png.decode(buffer).text; - - const rawMetadata = get(text, 'invokeai_metadata'); - if (rawMetadata) { - const metadataResult = zCoreMetadata.safeParse(JSON.parse(rawMetadata)); - if (metadataResult.success) { - data.metadata = metadataResult.data; - } else { - logger('system').error( - { error: parseify(metadataResult.error) }, - 'Problem reading metadata from image' - ); - } - } - - const rawWorkflow = get(text, 'invokeai_workflow'); - if (rawWorkflow) { - const workflowResult = zWorkflow.safeParse(JSON.parse(rawWorkflow)); - if (workflowResult.success) { - data.workflow = workflowResult.data; - } else { - logger('system').error( - { error: parseify(workflowResult.error) }, - 'Problem reading workflow from image' - ); - } - } - - return data; -}; diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts index 36c00ee1c9..166d00a3db 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/images.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts @@ -1,5 +1,4 @@ import { EntityState, Update } from '@reduxjs/toolkit'; -import { fetchBaseQuery } from '@reduxjs/toolkit/dist/query'; import { PatchCollection } from '@reduxjs/toolkit/dist/query/core/buildThunks'; import { logger } from 'app/logging/logger'; import { @@ -8,15 +7,9 @@ import { IMAGE_CATEGORIES, IMAGE_LIMIT, } from 'features/gallery/store/types'; -import { - CoreMetadata, - ImageMetadataAndWorkflow, - zCoreMetadata, -} from 'features/nodes/types/types'; -import { getMetadataAndWorkflowFromImageBlob } from 'features/nodes/util/getMetadataAndWorkflowFromImageBlob'; +import { CoreMetadata, zCoreMetadata } from 'features/nodes/types/types'; import { keyBy } from 'lodash-es'; import { ApiTagDescription, LIST_TAG, api } from '..'; -import { $authToken, $projectId } from '../client'; import { components, paths } from '../schema'; import { DeleteBoardResult, @@ -135,68 +128,6 @@ export const imagesApi = api.injectEndpoints({ }, keepUnusedDataFor: 86400, // 24 hours }), - getImageMetadataFromFile: build.query< - ImageMetadataAndWorkflow, - { image: ImageDTO; shouldFetchMetadataFromApi: boolean } - >({ - queryFn: async ( - args: { image: ImageDTO; shouldFetchMetadataFromApi: boolean }, - api, - extraOptions, - fetchWithBaseQuery - ) => { - if (args.shouldFetchMetadataFromApi) { - let metadata; - const metadataResponse = await fetchWithBaseQuery( - `images/i/${args.image.image_name}/metadata` - ); - if (metadataResponse.data) { - const metadataResult = zCoreMetadata.safeParse( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (metadataResponse.data as any)?.metadata - ); - if (metadataResult.success) { - metadata = metadataResult.data; - } - } - return { data: { metadata } }; - } else { - const authToken = $authToken.get(); - const projectId = $projectId.get(); - const customBaseQuery = fetchBaseQuery({ - baseUrl: '', - prepareHeaders: (headers) => { - if (authToken) { - headers.set('Authorization', `Bearer ${authToken}`); - } - if (projectId) { - headers.set('project-id', projectId); - } - - return headers; - }, - responseHandler: async (res) => { - return await res.blob(); - }, - }); - - const response = await customBaseQuery( - args.image.image_url, - api, - extraOptions - ); - const data = await getMetadataAndWorkflowFromImageBlob( - response.data as Blob - ); - - return { data }; - } - }, - providesTags: (result, error, { image }) => [ - { type: 'ImageMetadataFromFile', id: image.image_name }, - ], - keepUnusedDataFor: 86400, // 24 hours - }), deleteImage: build.mutation({ query: ({ image_name }) => ({ url: `images/i/${image_name}`, @@ -1643,6 +1574,5 @@ export const { useDeleteBoardMutation, useStarImagesMutation, useUnstarImagesMutation, - useGetImageMetadataFromFileQuery, useBulkDownloadImagesMutation, } = imagesApi; From dcd11327c1105715b52b6ccb2e13ff1edf2dd956 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 20 Oct 2023 11:41:05 +1100 Subject: [PATCH 24/27] fix(db): remove unused, commented out methods --- .../workflow_records_sqlite.py | 43 ------------------- 1 file changed, 43 deletions(-) diff --git a/invokeai/app/services/workflow_records/workflow_records_sqlite.py b/invokeai/app/services/workflow_records/workflow_records_sqlite.py index 2d9e1f26e8..e9e2bdca3a 100644 --- a/invokeai/app/services/workflow_records/workflow_records_sqlite.py +++ b/invokeai/app/services/workflow_records/workflow_records_sqlite.py @@ -100,46 +100,3 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase): raise finally: self._lock.release() - - # def update(self, workflow_id: str, workflow: Workflow) -> Workflow: - # """Updates a workflow record.""" - # try: - # workflow_id = workflow.get("id", None) - # if type(workflow_id) is not str: - # raise WorkflowNotFoundError(f"Workflow does not have a valid id, got {workflow_id}") - # self._lock.acquire() - # self._cursor.execute( - # """--sql - # UPDATE workflows - # SET workflow = ? - # WHERE workflow_id = ? - # """, - # (workflow, 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) -> Workflow: - # """Updates a workflow record.""" - # workflow = self.get(workflow_id) - # try: - # self._lock.acquire() - # self._cursor.execute( - # """--sql - # DELETE FROM workflows - # WHERE workflow_id = ? - # """, - # (workflow_id,), - # ) - # self._conn.commit() - # except Exception: - # self._conn.rollback() - # raise - # finally: - # self._lock.release() - # return workflow From b7f63a40653bb92ab4dd128e154fac18aa8a30e9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 20 Oct 2023 13:58:53 +1100 Subject: [PATCH 25/27] fix(ui): fix canvas color picker when value is zero good ol' zero is false-y --- .../web/src/features/canvas/hooks/useColorUnderCursor.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useColorUnderCursor.ts b/invokeai/frontend/web/src/features/canvas/hooks/useColorUnderCursor.ts index 0ade036987..5bdc59d345 100644 --- a/invokeai/frontend/web/src/features/canvas/hooks/useColorUnderCursor.ts +++ b/invokeai/frontend/web/src/features/canvas/hooks/useColorUnderCursor.ts @@ -37,7 +37,12 @@ const useColorPicker = () => { 1 ).data; - if (!(a && r && g && b)) { + if ( + r === undefined || + g === undefined || + b === undefined || + a === undefined + ) { return; } From 8604943e89fb26c2c74ef9c4b4501e4b1a4a4dbd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 19 Oct 2023 17:51:55 +1100 Subject: [PATCH 26/27] feat(nodes): simple custom nodes Custom nodes may be places in `$INVOKEAI_ROOT/nodes/` (configurable with `custom_nodes_dir` option). On app startup, an `__init__.py` is copied into the custom nodes dir, which recursively loads all python files in the directory as modules (files starting with `_` are ignored). The custom nodes dir is now a python module itself. When we `from invocations import *` to load init all invocations, we load the custom nodes dir, registering all custom nodes. --- invokeai/app/invocations/__init__.py | 29 ++++++++++++--- .../app/invocations/_custom_nodes_init.py | 37 +++++++++++++++++++ .../app/services/config/config_default.py | 8 ++++ 3 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 invokeai/app/invocations/_custom_nodes_init.py diff --git a/invokeai/app/invocations/__init__.py b/invokeai/app/invocations/__init__.py index 6407a1cdee..91a2edc680 100644 --- a/invokeai/app/invocations/__init__.py +++ b/invokeai/app/invocations/__init__.py @@ -1,8 +1,25 @@ -import os +import shutil +import sys +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path -__all__ = [] +from invokeai.app.services.config.config_default import InvokeAIAppConfig -dirname = os.path.dirname(os.path.abspath(__file__)) -for f in os.listdir(dirname): - if f != "__init__.py" and os.path.isfile("%s/%s" % (dirname, f)) and f[-3:] == ".py": - __all__.append(f[:-3]) +custom_nodes_path = Path(InvokeAIAppConfig.get_config().custom_nodes_path.absolute()) +custom_nodes_path.mkdir(parents=True, exist_ok=True) +custom_nodes_init_path = str(custom_nodes_path / "__init__.py") + +# copy our custom nodes __init__.py to the custom nodes directory +shutil.copy(Path(__file__).parent / "_custom_nodes_init.py", custom_nodes_init_path) + +# Import custom nodes, see https://docs.python.org/3/library/importlib.html#importing-programmatically +spec = spec_from_file_location("custom_nodes", custom_nodes_init_path) +if spec is None or spec.loader is None: + raise RuntimeError(f"Could not load custom nodes from {custom_nodes_init_path}") +module = module_from_spec(spec) +sys.modules[spec.name] = module +spec.loader.exec_module(module) + +# add core nodes to __all__ +python_files = filter(lambda f: not f.name.startswith("_"), Path(__file__).parent.rglob("*.py")) +__all__ = list(f.stem for f in python_files) # type: ignore diff --git a/invokeai/app/invocations/_custom_nodes_init.py b/invokeai/app/invocations/_custom_nodes_init.py new file mode 100644 index 0000000000..561f6de382 --- /dev/null +++ b/invokeai/app/invocations/_custom_nodes_init.py @@ -0,0 +1,37 @@ +""" +InvokeAI custom nodes initialization + +This file is responsible for loading all custom nodes from this directory. + +All python files are loaded on app startup. Custom nodes will be initialized and available for use +in workflows. + +The app must be restarted for changes to be picked up. + +This file is overwritten on launch. Do not edit this file directly. +""" +import sys +from importlib import import_module +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path + +from invokeai.backend.util.logging import InvokeAILogger + +logger = InvokeAILogger.get_logger() +count = 0 +for f in Path(__file__).parent.rglob("*.py"): + module_name = f.stem + if (not module_name.startswith("_")) and (module_name not in globals()): + spec = spec_from_file_location(module_name, f.absolute()) + if spec is None or spec.loader is None: + logger.warn(f"Could not load {f}") + continue + module = module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + count += 1 + del f, module_name + +logger.info(f"Loaded {count} modules from {Path(__file__).parent}") + +del import_module, Path diff --git a/invokeai/app/services/config/config_default.py b/invokeai/app/services/config/config_default.py index df01b65882..a877c465d2 100644 --- a/invokeai/app/services/config/config_default.py +++ b/invokeai/app/services/config/config_default.py @@ -243,6 +243,7 @@ class InvokeAIAppConfig(InvokeAISettings): db_dir : Optional[Path] = Field(default=Path('databases'), description='Path to InvokeAI databases directory', json_schema_extra=Categories.Paths) outdir : Optional[Path] = Field(default=Path('outputs'), description='Default folder for output images', json_schema_extra=Categories.Paths) use_memory_db : bool = Field(default=False, description='Use in-memory database for storing image metadata', json_schema_extra=Categories.Paths) + custom_nodes_dir : Path = Field(default=Path('nodes'), description='Path to directory for custom nodes', json_schema_extra=Categories.Paths) from_file : Optional[Path] = Field(default=None, description='Take command input from the indicated file (command-line client only)', json_schema_extra=Categories.Paths) # LOGGING @@ -410,6 +411,13 @@ class InvokeAIAppConfig(InvokeAISettings): """ return self._resolve(self.models_dir) + @property + def custom_nodes_path(self) -> Path: + """ + Path to the custom nodes directory + """ + return self._resolve(self.custom_nodes_dir) + # the following methods support legacy calls leftover from the Globals era @property def full_precision(self) -> bool: From 824702de99a09d18e5fae065bda2cc67568908f2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 20 Oct 2023 12:50:55 +1100 Subject: [PATCH 27/27] feat(nodes): change expected structure for custom nodes --- invokeai/app/invocations/__init__.py | 7 ++- .../app/invocations/_custom_nodes_init.py | 37 -------------- .../app/invocations/custom_nodes/README.md | 51 +++++++++++++++++++ invokeai/app/invocations/custom_nodes/init.py | 51 +++++++++++++++++++ 4 files changed, 107 insertions(+), 39 deletions(-) delete mode 100644 invokeai/app/invocations/_custom_nodes_init.py create mode 100644 invokeai/app/invocations/custom_nodes/README.md create mode 100644 invokeai/app/invocations/custom_nodes/init.py diff --git a/invokeai/app/invocations/__init__.py b/invokeai/app/invocations/__init__.py index 91a2edc680..32cf73d215 100644 --- a/invokeai/app/invocations/__init__.py +++ b/invokeai/app/invocations/__init__.py @@ -7,10 +7,13 @@ from invokeai.app.services.config.config_default import InvokeAIAppConfig custom_nodes_path = Path(InvokeAIAppConfig.get_config().custom_nodes_path.absolute()) custom_nodes_path.mkdir(parents=True, exist_ok=True) + custom_nodes_init_path = str(custom_nodes_path / "__init__.py") +custom_nodes_readme_path = str(custom_nodes_path / "README.md") # copy our custom nodes __init__.py to the custom nodes directory -shutil.copy(Path(__file__).parent / "_custom_nodes_init.py", custom_nodes_init_path) +shutil.copy(Path(__file__).parent / "custom_nodes/init.py", custom_nodes_init_path) +shutil.copy(Path(__file__).parent / "custom_nodes/README.md", custom_nodes_readme_path) # Import custom nodes, see https://docs.python.org/3/library/importlib.html#importing-programmatically spec = spec_from_file_location("custom_nodes", custom_nodes_init_path) @@ -21,5 +24,5 @@ sys.modules[spec.name] = module spec.loader.exec_module(module) # add core nodes to __all__ -python_files = filter(lambda f: not f.name.startswith("_"), Path(__file__).parent.rglob("*.py")) +python_files = filter(lambda f: not f.name.startswith("_"), Path(__file__).parent.glob("*.py")) __all__ = list(f.stem for f in python_files) # type: ignore diff --git a/invokeai/app/invocations/_custom_nodes_init.py b/invokeai/app/invocations/_custom_nodes_init.py deleted file mode 100644 index 561f6de382..0000000000 --- a/invokeai/app/invocations/_custom_nodes_init.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -InvokeAI custom nodes initialization - -This file is responsible for loading all custom nodes from this directory. - -All python files are loaded on app startup. Custom nodes will be initialized and available for use -in workflows. - -The app must be restarted for changes to be picked up. - -This file is overwritten on launch. Do not edit this file directly. -""" -import sys -from importlib import import_module -from importlib.util import module_from_spec, spec_from_file_location -from pathlib import Path - -from invokeai.backend.util.logging import InvokeAILogger - -logger = InvokeAILogger.get_logger() -count = 0 -for f in Path(__file__).parent.rglob("*.py"): - module_name = f.stem - if (not module_name.startswith("_")) and (module_name not in globals()): - spec = spec_from_file_location(module_name, f.absolute()) - if spec is None or spec.loader is None: - logger.warn(f"Could not load {f}") - continue - module = module_from_spec(spec) - sys.modules[spec.name] = module - spec.loader.exec_module(module) - count += 1 - del f, module_name - -logger.info(f"Loaded {count} modules from {Path(__file__).parent}") - -del import_module, Path diff --git a/invokeai/app/invocations/custom_nodes/README.md b/invokeai/app/invocations/custom_nodes/README.md new file mode 100644 index 0000000000..d93bb65539 --- /dev/null +++ b/invokeai/app/invocations/custom_nodes/README.md @@ -0,0 +1,51 @@ +# Custom Nodes / Node Packs + +Copy your node packs to this directory. + +When nodes are added or changed, you must restart the app to see the changes. + +## Directory Structure + +For a node pack to be loaded, it must be placed in a directory alongside this +file. Here's an example structure: + +```py +. +├── __init__.py # Invoke-managed custom node loader +│ +├── cool_node +│ ├── __init__.py # see example below +│ └── cool_node.py +│ +└── my_node_pack + ├── __init__.py # see example below + ├── tasty_node.py + ├── bodacious_node.py + ├── utils.py + └── extra_nodes + └── fancy_node.py +``` + +## Node Pack `__init__.py` + +Each node pack must have an `__init__.py` file that imports its nodes. + +The structure of each node or node pack is otherwise not important. + +Here are examples, based on the example directory structure. + +### `cool_node/__init__.py` + +```py +from .cool_node import CoolInvocation +``` + +### `my_node_pack/__init__.py` + +```py +from .tasty_node import TastyInvocation +from .bodacious_node import BodaciousInvocation +from .extra_nodes.fancy_node import FancyInvocation +``` + +Only nodes imported in the `__init__.py` file are loaded. diff --git a/invokeai/app/invocations/custom_nodes/init.py b/invokeai/app/invocations/custom_nodes/init.py new file mode 100644 index 0000000000..c6708e95a7 --- /dev/null +++ b/invokeai/app/invocations/custom_nodes/init.py @@ -0,0 +1,51 @@ +""" +Invoke-managed custom node loader. See README.md for more information. +""" + +import sys +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path + +from invokeai.backend.util.logging import InvokeAILogger + +logger = InvokeAILogger.get_logger() +loaded_count = 0 + + +for d in Path(__file__).parent.iterdir(): + # skip files + if not d.is_dir(): + continue + + # skip hidden directories + if d.name.startswith("_") or d.name.startswith("."): + continue + + # skip directories without an `__init__.py` + init = d / "__init__.py" + if not init.exists(): + continue + + module_name = init.parent.stem + + # skip if already imported + if module_name in globals(): + continue + + # we have a legit module to import + 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}") + continue + + module = module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + + loaded_count += 1 + + del init, module_name + + +logger.info(f"Loaded {loaded_count} modules from {Path(__file__).parent}")