From 454ba9b8930a4fc0260c474a9c5665caa14e3f6a Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Tue, 23 May 2023 10:51:00 -0400 Subject: [PATCH 01/72] add crossOrigin = anonymous attribute to konva image --- .../web/src/features/canvas/components/IAICanvasImage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx index 8229f8552f..b8757eff0c 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx @@ -8,7 +8,7 @@ type IAICanvasImageProps = { }; const IAICanvasImage = (props: IAICanvasImageProps) => { const { url, x, y } = props; - const [image] = useImage(url); + const [image] = useImage(url, 'anonymous'); return ; }; From 19da795274847411c18618e9d6200663882d8a28 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 18 May 2023 11:15:59 +1000 Subject: [PATCH 02/72] fix(ui): send to canvas in currentimagebuttons not working --- .../src/features/gallery/components/CurrentImageButtons.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx index 8193dbe1ce..f7932db0c4 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx @@ -65,6 +65,7 @@ import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/U import { allParametersSet } from 'features/parameters/store/generationSlice'; import DeleteImageButton from './ImageActionButtons/DeleteImageButton'; import { useAppToaster } from 'app/components/Toaster'; +import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; const currentImageButtonsSelector = createSelector( [ @@ -335,7 +336,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { dispatch(sentImageToCanvas()); if (isLightboxOpen) dispatch(setIsLightboxOpen(false)); - // dispatch(setInitialCanvasImage(selectedImage)); + dispatch(setInitialCanvasImage(image)); dispatch(requestCanvasRescale()); if (activeTabName !== 'unifiedCanvas') { From d6efb989532f8b93b7f323eee32c72b796f01344 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 24 May 2023 10:35:42 +1000 Subject: [PATCH 03/72] build: fix test-invoke-pip.yml - Restore conditional which ensures tests are only run on `main` - Fix `yaml` syntax error --- .github/workflows/test-invoke-pip.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-invoke-pip.yml b/.github/workflows/test-invoke-pip.yml index 21fda2d191..a8916d0bfd 100644 --- a/.github/workflows/test-invoke-pip.yml +++ b/.github/workflows/test-invoke-pip.yml @@ -80,7 +80,8 @@ jobs: uses: actions/checkout@v3 - name: set test prompt to main branch validation - run:echo "TEST_PROMPTS=tests/validate_pr_prompt.txt" >> ${{ matrix.github-env }} + if: ${{ github.ref == 'refs/heads/main' }} + run: echo "TEST_PROMPTS=tests/validate_pr_prompt.txt" >> ${{ matrix.github-env }} - name: setup python uses: actions/setup-python@v4 From 670c79f2c73923a890c945253a2b526759d4a784 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 25 May 2023 00:31:48 +1000 Subject: [PATCH 04/72] fix: attempt to fix actions i think this conditional needs to be removed. --- .github/workflows/test-invoke-pip.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test-invoke-pip.yml b/.github/workflows/test-invoke-pip.yml index a8916d0bfd..17673de937 100644 --- a/.github/workflows/test-invoke-pip.yml +++ b/.github/workflows/test-invoke-pip.yml @@ -80,7 +80,6 @@ jobs: uses: actions/checkout@v3 - name: set test prompt to main branch validation - if: ${{ github.ref == 'refs/heads/main' }} run: echo "TEST_PROMPTS=tests/validate_pr_prompt.txt" >> ${{ matrix.github-env }} - name: setup python From fb0b63c580b0dacb9cc36ece933ac64f9fcb5518 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 24 May 2023 00:03:51 +1000 Subject: [PATCH 05/72] fix(nodes): fix seam painting The problem was the same seed was getting used for the seam painting pass, causing the fried look. Same issue as if you do img2img on a txt2img with the same seed/prompt. Thanks to @hipsterusername for teaming up to debug this. We got pretty deep into the weeds. --- invokeai/backend/generator/inpaint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/backend/generator/inpaint.py b/invokeai/backend/generator/inpaint.py index 8c471d025d..a7fec83eb7 100644 --- a/invokeai/backend/generator/inpaint.py +++ b/invokeai/backend/generator/inpaint.py @@ -196,7 +196,7 @@ class Inpaint(Img2Img): seam_noise = self.get_noise(im.width, im.height) - result = make_image(seam_noise, seed) + result = make_image(seam_noise, seed=None) return result From 9c89d3452ca97e18391871c06193e3c94b5dc46e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 May 2023 19:13:53 +1000 Subject: [PATCH 06/72] feat(nodes): add high-level images service feat(nodes): add ResultsServiceABC & SqliteResultsService **Doesn't actually work bc of circular imports. Can't even test it.** - add a base class for ResultsService and SQLite implementation - use `graph_execution_manager` `on_changed` callback to keep `results` table in sync fix(nodes): fix results service bugs chore(ui): regen api fix(ui): fix type guards feat(nodes): add `result_type` to results table, fix types fix(nodes): do not shadow `list` builtin feat(nodes): add results router It doesn't work due to circular imports still fix(nodes): Result class should use outputs classes, not fields feat(ui): crude results router fix(ui): send to canvas in currentimagebuttons not working feat(nodes): add core metadata builder feat(nodes): add design doc feat(nodes): wip latents db stuff feat(nodes): images_db_service and resources router feat(nodes): wip images db & router feat(nodes): update image related names feat(nodes): update urlservice feat(nodes): add high-level images service --- invokeai/app/api/dependencies.py | 32 +- invokeai/app/api/routers/image_resources.py | 74 ++ invokeai/app/api/routers/images.py | 36 +- invokeai/app/api/routers/results.py | 42 ++ invokeai/app/api_app.py | 13 +- invokeai/app/invocations/baseinvocation.py | 7 +- invokeai/app/invocations/generate.py | 12 + invokeai/app/invocations/image.py | 2 +- invokeai/app/models/image.py | 16 +- invokeai/app/models/metadata.py | 70 ++ invokeai/app/models/resources.py | 28 + invokeai/app/services/db.ipynb | 578 +++++++++++++++ invokeai/app/services/image_db.py | 329 +++++++++ invokeai/app/services/image_storage.py | 111 +-- invokeai/app/services/images.py | 219 ++++++ invokeai/app/services/invocation_services.py | 47 +- invokeai/app/services/metadata.py | 17 +- invokeai/app/services/models/image_record.py | 29 + invokeai/app/services/proposeddesign.py | 657 ++++++++++++++++++ invokeai/app/services/results.py | 466 +++++++++++++ invokeai/app/services/urls.py | 32 + .../app/services/util/create_enum_table.py | 39 ++ .../services/util/deserialize_image_record.py | 33 + invokeai/app/util/enum.py | 12 + invokeai/app/util/misc.py | 8 + .../src/services/api/models/ImageOutput.ts | 2 +- .../api/models/RandomIntInvocation.ts | 8 + .../api/schemas/$RandomIntInvocation.ts | 8 + .../frontend/web/src/services/types/guards.ts | 7 +- 29 files changed, 2851 insertions(+), 83 deletions(-) create mode 100644 invokeai/app/api/routers/image_resources.py create mode 100644 invokeai/app/api/routers/results.py create mode 100644 invokeai/app/models/metadata.py create mode 100644 invokeai/app/models/resources.py create mode 100644 invokeai/app/services/db.ipynb create mode 100644 invokeai/app/services/image_db.py create mode 100644 invokeai/app/services/images.py create mode 100644 invokeai/app/services/models/image_record.py create mode 100644 invokeai/app/services/proposeddesign.py create mode 100644 invokeai/app/services/results.py create mode 100644 invokeai/app/services/urls.py create mode 100644 invokeai/app/services/util/create_enum_table.py create mode 100644 invokeai/app/services/util/deserialize_image_record.py create mode 100644 invokeai/app/util/enum.py diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index 517e174b68..7494d24324 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -1,9 +1,13 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) import os +from types import ModuleType +from invokeai.app.services.database.images.sqlite_images_db_service import ( + SqliteImageDb, +) +from invokeai.app.services.urls import LocalUrlService import invokeai.backend.util.logging as logger -from typing import types from ..services.default_graphs import create_system_graphs from ..services.latent_storage import DiskLatentsStorage, ForwardCacheLatentsStorage @@ -17,6 +21,7 @@ from ..services.invoker import Invoker from ..services.processor import DefaultInvocationProcessor from ..services.sqlite import SqliteItemStorage from ..services.metadata import PngMetadataService +from ..services.results import SqliteResultsService from .events import FastAPIEventService @@ -50,28 +55,41 @@ class ApiDependencies: os.path.join(os.path.dirname(__file__), "../../../../outputs") ) - latents = ForwardCacheLatentsStorage(DiskLatentsStorage(f'{output_folder}/latents')) + latents = ForwardCacheLatentsStorage( + DiskLatentsStorage(f"{output_folder}/latents") + ) metadata = PngMetadataService() - images = DiskImageStorage(f'{output_folder}/images', metadata_service=metadata) + urls = LocalUrlService() + + images = DiskImageStorage(f"{output_folder}/images", metadata_service=metadata) # TODO: build a file/path manager? db_location = os.path.join(output_folder, "invokeai.db") + graph_execution_manager = SqliteItemStorage[GraphExecutionState]( + filename=db_location, table_name="graph_executions" + ) + + images_db = SqliteImageDb(filename=db_location) + + # register event handler to update the `results` table when a graph execution state is inserted or updated + # graph_execution_manager.on_changed(results.handle_graph_execution_state_change) + services = InvocationServices( - model_manager=get_model_manager(config,logger), + model_manager=get_model_manager(config, logger), events=events, latents=latents, images=images, metadata=metadata, + images_db=images_db, + urls=urls, queue=MemoryInvocationQueue(), graph_library=SqliteItemStorage[LibraryGraph]( filename=db_location, table_name="graphs" ), - graph_execution_manager=SqliteItemStorage[GraphExecutionState]( - filename=db_location, table_name="graph_executions" - ), + graph_execution_manager=graph_execution_manager, processor=DefaultInvocationProcessor(), restoration=RestorationServices(config,logger), configuration=config, diff --git a/invokeai/app/api/routers/image_resources.py b/invokeai/app/api/routers/image_resources.py new file mode 100644 index 0000000000..56fcdcb2d1 --- /dev/null +++ b/invokeai/app/api/routers/image_resources.py @@ -0,0 +1,74 @@ +from fastapi import HTTPException, Path, Query +from fastapi.routing import APIRouter +from invokeai.app.models.image import ( + ImageCategory, + ImageType, +) +from invokeai.app.services.image_db import ImageRecordServiceBase +from invokeai.app.services.image_storage import ImageStorageBase +from invokeai.app.services.models.image_record import ImageRecord +from invokeai.app.services.item_storage import PaginatedResults + +from ..dependencies import ApiDependencies + +image_records_router = APIRouter(prefix="/v1/records/images", tags=["records"]) + + +@image_records_router.get("/{image_type}/{image_name}", operation_id="get_image_record") +async def get_image_record( + image_type: ImageType = Path(description="The type of the image record to get"), + image_name: str = Path(description="The id of the image record to get"), +) -> ImageRecord: + """Gets an image record by id""" + + try: + return ApiDependencies.invoker.services.images_new.get_record( + image_type=image_type, image_name=image_name + ) + except ImageRecordServiceBase.ImageRecordNotFoundException: + raise HTTPException(status_code=404) + + +@image_records_router.get( + "/", + operation_id="list_image_records", +) +async def list_image_records( + image_type: ImageType = Query(description="The type of image records to get"), + image_category: ImageCategory = Query( + description="The kind of image records to get" + ), + page: int = Query(default=0, description="The page of image records to get"), + per_page: int = Query( + default=10, description="The number of image records per page" + ), +) -> PaginatedResults[ImageRecord]: + """Gets a list of image records by type and category""" + + images = ApiDependencies.invoker.services.images_new.get_many( + image_type=image_type, + image_category=image_category, + page=page, + per_page=per_page, + ) + + return images + + +@image_records_router.delete("/{image_type}/{image_name}", operation_id="delete_image") +async def delete_image_record( + image_type: ImageType = Query(description="The type of image records to get"), + image_name: str = Path(description="The name of the image to delete"), +) -> None: + """Deletes an image record""" + + try: + ApiDependencies.invoker.services.images_new.delete( + image_type=image_type, image_name=image_name + ) + except ImageStorageBase.ImageFileDeleteException: + # TODO: log this + pass + except ImageRecordServiceBase.ImageRecordDeleteException: + # TODO: log this + pass diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 0b7891e0f2..41ba00ef7a 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -14,23 +14,39 @@ from invokeai.app.api.models.images import ( ImageResponse, ImageResponseMetadata, ) +from invokeai.app.models.image import ImageType from invokeai.app.services.item_storage import PaginatedResults -from ...services.image_storage import ImageType from ..dependencies import ApiDependencies images_router = APIRouter(prefix="/v1/images", tags=["images"]) -@images_router.get("/{image_type}/{image_name}", operation_id="get_image") +# @images_router.get("/{image_type}/{image_name}", operation_id="get_image") +# async def get_image( +# image_type: ImageType = Path(description="The type of image to get"), +# image_name: str = Path(description="The name of the image to get"), +# ) -> FileResponse: +# """Gets an image""" + +# path = ApiDependencies.invoker.services.images.get_path( +# image_type=image_type, image_name=image_name +# ) + +# if ApiDependencies.invoker.services.images.validate_path(path): +# return FileResponse(path) +# else: +# raise HTTPException(status_code=404) + +@images_router.get("/{image_type}/{image_id}", operation_id="get_image") async def get_image( - image_type: ImageType = Path(description="The type of image to get"), - image_name: str = Path(description="The name of the image to get"), + image_type: ImageType = Path(description="The type of the image to get"), + image_id: str = Path(description="The id of the image to get"), ) -> FileResponse: """Gets an image""" path = ApiDependencies.invoker.services.images.get_path( - image_type=image_type, image_name=image_name + image_type=image_type, image_id=image_id ) if ApiDependencies.invoker.services.images.validate_path(path): @@ -41,7 +57,7 @@ async def get_image( @images_router.delete("/{image_type}/{image_name}", operation_id="delete_image") async def delete_image( - image_type: ImageType = Path(description="The type of image to delete"), + image_type: ImageType = Path(description="The type of the image to delete"), image_name: str = Path(description="The name of the image to delete"), ) -> None: """Deletes an image and its thumbnail""" @@ -52,16 +68,16 @@ async def delete_image( @images_router.get( - "/{thumbnail_type}/thumbnails/{thumbnail_name}", operation_id="get_thumbnail" + "/{image_type}/thumbnails/{thumbnail_id}", operation_id="get_thumbnail" ) async def get_thumbnail( - thumbnail_type: ImageType = Path(description="The type of thumbnail to get"), - thumbnail_name: str = Path(description="The name of the thumbnail to get"), + image_type: ImageType = Path(description="The type of the thumbnail to get"), + thumbnail_id: str = Path(description="The id of the thumbnail to get"), ) -> FileResponse | Response: """Gets a thumbnail""" path = ApiDependencies.invoker.services.images.get_path( - image_type=thumbnail_type, image_name=thumbnail_name, is_thumbnail=True + image_type=image_type, image_id=thumbnail_id, is_thumbnail=True ) if ApiDependencies.invoker.services.images.validate_path(path): diff --git a/invokeai/app/api/routers/results.py b/invokeai/app/api/routers/results.py new file mode 100644 index 0000000000..4190e5bd27 --- /dev/null +++ b/invokeai/app/api/routers/results.py @@ -0,0 +1,42 @@ +from fastapi import HTTPException, Path, Query +from fastapi.routing import APIRouter +from invokeai.app.services.results import ResultType, ResultWithSession +from invokeai.app.services.item_storage import PaginatedResults + +from ..dependencies import ApiDependencies + +results_router = APIRouter(prefix="/v1/results", tags=["results"]) + + +@results_router.get("/{result_type}/{result_name}", operation_id="get_result") +async def get_result( + result_type: ResultType = Path(description="The type of result to get"), + result_name: str = Path(description="The name of the result to get"), +) -> ResultWithSession: + """Gets a result""" + + result = ApiDependencies.invoker.services.results.get( + result_id=result_name, result_type=result_type + ) + + if result is not None: + return result + else: + raise HTTPException(status_code=404) + + +@results_router.get( + "/", + operation_id="list_results", + responses={200: {"model": PaginatedResults[ResultWithSession]}}, +) +async def list_results( + result_type: ResultType = Query(description="The type of results to get"), + page: int = Query(default=0, description="The page of results to get"), + per_page: int = Query(default=10, description="The number of results per page"), +) -> PaginatedResults[ResultWithSession]: + """Gets a list of results""" + results = ApiDependencies.invoker.services.results.get_many( + result_type=result_type, page=page, per_page=per_page + ) + return results diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index 33714f1057..a67f36edd3 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -3,6 +3,7 @@ import asyncio from inspect import signature import uvicorn +from invokeai.app.models import resources import invokeai.backend.util.logging as logger from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -14,11 +15,12 @@ from fastapi_events.middleware import EventHandlerASGIMiddleware from pydantic.schema import schema from .api.dependencies import ApiDependencies -from .api.routers import images, sessions, models +from .api.routers import image_resources, images, sessions, models from .api.sockets import SocketIO from .invocations.baseinvocation import BaseInvocation from .services.config import InvokeAIAppConfig + # Create the app # TODO: create this all in a method so configuration/etc. can be passed in? app = FastAPI(title="Invoke AI", docs_url=None, redoc_url=None) @@ -73,6 +75,8 @@ app.include_router(images.images_router, prefix="/api") app.include_router(models.models_router, prefix="/api") +app.include_router(image_resources.image_resources_router, prefix="/api") + # Build a custom OpenAPI to include all outputs # TODO: can outputs be included on metadata of invocation schemas somehow? @@ -121,6 +125,7 @@ app.openapi = custom_openapi # Override API doc favicons app.mount("/static", StaticFiles(directory="static/dream_web"), name="static") + @app.get("/docs", include_in_schema=False) def overridden_swagger(): return get_swagger_ui_html( @@ -138,8 +143,12 @@ def overridden_redoc(): redoc_favicon_url="/static/favicon.ico", ) + # Must mount *after* the other routes else it borks em -app.mount("/", StaticFiles(directory="invokeai/frontend/web/dist", html=True), name="ui") +app.mount( + "/", StaticFiles(directory="invokeai/frontend/web/dist", html=True), name="ui" +) + def invoke_api(): # Start our own event loop for eventing usage diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index 7daaa588b1..da61641105 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -1,12 +1,15 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) +from __future__ import annotations + from abc import ABC, abstractmethod from inspect import signature -from typing import get_args, get_type_hints, Dict, List, Literal, TypedDict +from typing import get_args, get_type_hints, Dict, List, Literal, TypedDict, TYPE_CHECKING from pydantic import BaseModel, Field -from ..services.invocation_services import InvocationServices +if TYPE_CHECKING: + from ..services.invocation_services import InvocationServices class InvocationContext: diff --git a/invokeai/app/invocations/generate.py b/invokeai/app/invocations/generate.py index bc72bbe2b3..525be128e4 100644 --- a/invokeai/app/invocations/generate.py +++ b/invokeai/app/invocations/generate.py @@ -10,6 +10,8 @@ from pydantic import BaseModel, Field from invokeai.app.models.image import ColorField, ImageField, ImageType from invokeai.app.invocations.util.choose_model import choose_model +from invokeai.app.models.metadata import GeneratedImageOrLatentsMetadata +from invokeai.app.models.image import ImageCategory, ImageType from invokeai.app.util.misc import SEED_MAX, get_random_seed from invokeai.backend.generator.inpaint import infill_methods from .baseinvocation import BaseInvocation, InvocationContext, InvocationConfig @@ -106,6 +108,16 @@ class TextToImageInvocation(BaseInvocation, SDImageInvocation): context.services.images.save( image_type, image_name, generate_output.image, metadata ) + + context.services.images_db.set( + id=image_name, + image_type=ImageType.RESULT, + image_category=ImageCategory.IMAGE, + session_id=context.graph_execution_state_id, + node_id=self.id, + metadata=GeneratedImageOrLatentsMetadata(), + ) + return build_image_output( image_type=image_type, image_name=image_name, diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 8b4163c4c6..56141cbb0e 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -31,7 +31,7 @@ class ImageOutput(BaseInvocationOutput): """Base class for invocations that output an image""" # fmt: off - type: Literal["image"] = "image" + type: Literal["image_output"] = "image_output" image: ImageField = Field(default=None, description="The output image") width: int = Field(description="The width of the image in pixels") height: int = Field(description="The height of the image in pixels") diff --git a/invokeai/app/models/image.py b/invokeai/app/models/image.py index f6813c6d96..f364abdb71 100644 --- a/invokeai/app/models/image.py +++ b/invokeai/app/models/image.py @@ -2,11 +2,23 @@ from enum import Enum from typing import Optional, Tuple from pydantic import BaseModel, Field +from invokeai.app.util.enum import MetaEnum + + +class ImageType(str, Enum, metaclass=MetaEnum): + """The type of an image.""" -class ImageType(str, Enum): RESULT = "results" - INTERMEDIATE = "intermediates" UPLOAD = "uploads" + INTERMEDIATE = "intermediates" + + +class ImageCategory(str, Enum, metaclass=MetaEnum): + """The category of an image. Use ImageCategory.OTHER for non-default categories.""" + + IMAGE = "image" + CONTROL_IMAGE = "control_image" + OTHER = "other" def is_image_type(obj): diff --git a/invokeai/app/models/metadata.py b/invokeai/app/models/metadata.py new file mode 100644 index 0000000000..aae3337266 --- /dev/null +++ b/invokeai/app/models/metadata.py @@ -0,0 +1,70 @@ +from typing import Optional +from pydantic import BaseModel, Field, StrictFloat, StrictInt, StrictStr + + +class GeneratedImageOrLatentsMetadata(BaseModel): + """Core generation metadata for an image/tensor generated in InvokeAI. + + Generated by traversing the execution graph, collecting the parameters of the nearest ancestors of a given node. + + Full metadata may be accessed by querying for the session in the `graph_executions` table. + """ + + positive_conditioning: Optional[StrictStr] = Field( + default=None, description="The positive conditioning." + ) + negative_conditioning: Optional[StrictStr] = Field( + default=None, description="The negative conditioning." + ) + width: Optional[StrictInt] = Field( + default=None, description="Width of the image/tensor in pixels." + ) + height: Optional[StrictInt] = Field( + default=None, description="Height of the image/tensor in pixels." + ) + seed: Optional[StrictInt] = Field( + default=None, description="The seed used for noise generation." + ) + cfg_scale: Optional[StrictFloat] = Field( + default=None, description="The classifier-free guidance scale." + ) + steps: Optional[StrictInt] = Field( + default=None, description="The number of steps used for inference." + ) + scheduler: Optional[StrictStr] = Field( + default=None, description="The scheduler used for inference." + ) + model: Optional[StrictStr] = Field( + default=None, description="The model used for inference." + ) + strength: Optional[StrictFloat] = Field( + default=None, + description="The strength used for image-to-image/tensor-to-tensor.", + ) + image: Optional[StrictStr] = Field( + default=None, description="The ID of the initial image." + ) + tensor: Optional[StrictStr] = Field( + default=None, description="The ID of the initial tensor." + ) + # Pending model refactor: + # vae: Optional[str] = Field(default=None,description="The VAE used for decoding.") + # unet: Optional[str] = Field(default=None,description="The UNet used dor inference.") + # clip: Optional[str] = Field(default=None,description="The CLIP Encoder used for conditioning.") + + +class UploadedImageOrLatentsMetadata(BaseModel): + """Limited metadata for an uploaded image/tensor.""" + + width: Optional[StrictInt] = Field( + default=None, description="Width of the image/tensor in pixels." + ) + height: Optional[StrictInt] = Field( + default=None, description="Height of the image/tensor in pixels." + ) + # The extra field will be the contents of the PNG file's tEXt chunk. It may have come + # from another SD application or InvokeAI, so it needs to be flexible. + # If the upload is a not an image or `image_latents` tensor, this will be omitted. + extra: Optional[StrictStr] = Field( + default=None, description="Extra metadata, extracted from the PNG tEXt chunk." + ) diff --git a/invokeai/app/models/resources.py b/invokeai/app/models/resources.py new file mode 100644 index 0000000000..1cd22e4550 --- /dev/null +++ b/invokeai/app/models/resources.py @@ -0,0 +1,28 @@ +# TODO: Make a new model for this +from enum import Enum + +from invokeai.app.util.enum import MetaEnum + + +class ResourceType(str, Enum, metaclass=MetaEnum): + """The type of a resource.""" + + IMAGES = "images" + TENSORS = "tensors" + + +# class ResourceOrigin(str, Enum, metaclass=MetaEnum): +# """The origin of a resource (eg image or tensor).""" + +# RESULTS = "results" +# UPLOADS = "uploads" +# INTERMEDIATES = "intermediates" + + + +class TensorKind(str, Enum, metaclass=MetaEnum): + """The kind of a tensor. Use TensorKind.OTHER for non-default kinds.""" + + IMAGE_LATENTS = "image_latents" + CONDITIONING = "conditioning" + OTHER = "other" diff --git a/invokeai/app/services/db.ipynb b/invokeai/app/services/db.ipynb new file mode 100644 index 0000000000..67dfe22128 --- /dev/null +++ b/invokeai/app/services/db.ipynb @@ -0,0 +1,578 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "from abc import ABC, abstractmethod\n", + "from enum import Enum\n", + "import enum\n", + "import sqlite3\n", + "import threading\n", + "from typing import Optional, Type, TypeVar, Union\n", + "from PIL.Image import Image as PILImage\n", + "from pydantic import BaseModel, Field\n", + "from torch import Tensor" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "class ResourceOrigin(str, Enum):\n", + " \"\"\"The origin of a resource (eg image or tensor).\"\"\"\n", + "\n", + " RESULTS = \"results\"\n", + " UPLOADS = \"uploads\"\n", + " INTERMEDIATES = \"intermediates\"\n", + "\n", + "\n", + "class ImageKind(str, Enum):\n", + " \"\"\"The kind of an image. Use ImageKind.OTHER for non-default kinds.\"\"\"\n", + "\n", + " IMAGE = \"image\"\n", + " CONTROL_IMAGE = \"control_image\"\n", + " OTHER = \"other\"\n", + "\n", + "\n", + "class TensorKind(str, Enum):\n", + " \"\"\"The kind of a tensor. Use TensorKind.OTHER for non-default kinds.\"\"\"\n", + "\n", + " IMAGE_LATENTS = \"image_latents\"\n", + " CONDITIONING = \"conditioning\"\n", + " OTHER = \"other\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "def create_sql_values_string_from_string_enum(enum: Type[Enum]):\n", + " \"\"\"\n", + " Creates a string of the form \"('value1'), ('value2'), ..., ('valueN')\" from a StrEnum.\n", + " \"\"\"\n", + "\n", + " delimiter = \", \"\n", + " values = [f\"('{e.value}')\" for e in enum]\n", + " return delimiter.join(values)\n", + "\n", + "\n", + "def create_sql_table_from_enum(\n", + " enum: Type[Enum],\n", + " table_name: str,\n", + " primary_key_name: str,\n", + " conn: sqlite3.Connection,\n", + " cursor: sqlite3.Cursor,\n", + " lock: threading.Lock,\n", + "):\n", + " \"\"\"\n", + " Creates and populates a table to be used as a functional enum.\n", + " \"\"\"\n", + "\n", + " try:\n", + " lock.acquire()\n", + "\n", + " values_string = create_sql_values_string_from_string_enum(enum)\n", + "\n", + " cursor.execute(\n", + " f\"\"\"--sql\n", + " CREATE TABLE IF NOT EXISTS {table_name} (\n", + " {primary_key_name} TEXT PRIMARY KEY\n", + " );\n", + " \"\"\"\n", + " )\n", + " cursor.execute(\n", + " f\"\"\"--sql\n", + " INSERT OR IGNORE INTO {table_name} ({primary_key_name}) VALUES {values_string};\n", + " \"\"\"\n", + " )\n", + " conn.commit()\n", + " finally:\n", + " lock.release()\n", + "\n", + "\n", + "\"\"\"\n", + "`resource_origins` functions as an enum for the ResourceOrigin model.\n", + "\"\"\"\n", + "\n", + "\n", + "# def create_resource_origins_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", + "# create_sql_table_from_enum(\n", + "# enum=ResourceOrigin,\n", + "# table_name=\"resource_origins\",\n", + "# primary_key_name=\"origin_name\",\n", + "# conn=conn,\n", + "# cursor=cursor,\n", + "# lock=lock,\n", + "# )\n", + "\n", + "\n", + "\"\"\"\n", + "`image_kinds` functions as an enum for the ImageType model.\n", + "\"\"\"\n", + "\n", + "\n", + "# def create_image_kinds_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", + " # create_sql_table_from_enum(\n", + " # enum=ImageKind,\n", + " # table_name=\"image_kinds\",\n", + " # primary_key_name=\"kind_name\",\n", + " # conn=conn,\n", + " # cursor=cursor,\n", + " # lock=lock,\n", + " # )\n", + "\n", + "\n", + "\"\"\"\n", + "`tensor_kinds` functions as an enum for the TensorType model.\n", + "\"\"\"\n", + "\n", + "\n", + "# def create_tensor_kinds_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", + " # create_sql_table_from_enum(\n", + " # enum=TensorKind,\n", + " # table_name=\"tensor_kinds\",\n", + " # primary_key_name=\"kind_name\",\n", + " # conn=conn,\n", + " # cursor=cursor,\n", + " # lock=lock,\n", + " # )\n", + "\n", + "\n", + "\"\"\"\n", + "`images` stores all images, regardless of type\n", + "\"\"\"\n", + "\n", + "\n", + "def create_images_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", + " try:\n", + " lock.acquire()\n", + "\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE TABLE IF NOT EXISTS images (\n", + " id TEXT PRIMARY KEY,\n", + " origin TEXT,\n", + " image_kind TEXT,\n", + " created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n", + " FOREIGN KEY(origin) REFERENCES resource_origins(origin_name),\n", + " FOREIGN KEY(image_kind) REFERENCES image_kinds(kind_name)\n", + " );\n", + " \"\"\"\n", + " )\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE UNIQUE INDEX IF NOT EXISTS idx_images_id ON images(id);\n", + " \"\"\"\n", + " )\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE INDEX IF NOT EXISTS idx_images_origin ON images(origin);\n", + " \"\"\"\n", + " )\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE INDEX IF NOT EXISTS idx_images_image_kind ON images(image_kind);\n", + " \"\"\"\n", + " )\n", + " conn.commit()\n", + " finally:\n", + " lock.release()\n", + "\n", + "\n", + "\"\"\"\n", + "`images_results` stores additional data specific to `results` images.\n", + "\"\"\"\n", + "\n", + "\n", + "def create_images_results_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", + " try:\n", + " lock.acquire()\n", + "\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE TABLE IF NOT EXISTS images_results (\n", + " images_id TEXT PRIMARY KEY,\n", + " session_id TEXT NOT NULL,\n", + " node_id TEXT NOT NULL,\n", + " FOREIGN KEY(images_id) REFERENCES images(id) ON DELETE CASCADE\n", + " );\n", + " \"\"\"\n", + " )\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE UNIQUE INDEX IF NOT EXISTS idx_images_results_images_id ON images_results(images_id);\n", + " \"\"\"\n", + " )\n", + " conn.commit()\n", + " finally:\n", + " lock.release()\n", + "\n", + "\n", + "\"\"\"\n", + "`images_intermediates` stores additional data specific to `intermediates` images\n", + "\"\"\"\n", + "\n", + "\n", + "def create_images_intermediates_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", + " try:\n", + " lock.acquire()\n", + "\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE TABLE IF NOT EXISTS images_intermediates (\n", + " images_id TEXT PRIMARY KEY,\n", + " session_id TEXT NOT NULL,\n", + " node_id TEXT NOT NULL,\n", + " FOREIGN KEY(images_id) REFERENCES images(id) ON DELETE CASCADE\n", + " );\n", + " \"\"\"\n", + " )\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE UNIQUE INDEX IF NOT EXISTS idx_images_intermediates_images_id ON images_intermediates(images_id);\n", + " \"\"\"\n", + " )\n", + " conn.commit()\n", + " finally:\n", + " lock.release()\n", + "\n", + "\n", + "\"\"\"\n", + "`images_metadata` stores basic metadata for any image type\n", + "\"\"\"\n", + "\n", + "\n", + "def create_images_metadata_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", + " try:\n", + " lock.acquire()\n", + "\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE TABLE IF NOT EXISTS images_metadata (\n", + " images_id TEXT PRIMARY KEY,\n", + " metadata TEXT,\n", + " FOREIGN KEY(images_id) REFERENCES images(id) ON DELETE CASCADE\n", + " );\n", + " \"\"\"\n", + " )\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE UNIQUE INDEX IF NOT EXISTS idx_images_metadata_images_id ON images_metadata(images_id);\n", + " \"\"\"\n", + " )\n", + " conn.commit()\n", + " finally:\n", + " lock.release()\n", + "\n", + "\n", + "# `tensors` table: stores references to tensor\n", + "\n", + "\n", + "def create_tensors_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", + " try:\n", + " lock.acquire()\n", + "\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE TABLE IF NOT EXISTS tensors (\n", + " id TEXT PRIMARY KEY,\n", + " origin TEXT,\n", + " tensor_kind TEXT,\n", + " created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n", + " FOREIGN KEY(origin) REFERENCES resource_origins(origin_name),\n", + " FOREIGN KEY(tensor_kind) REFERENCES tensor_kinds(kind_name)\n", + " );\n", + " \"\"\"\n", + " )\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE UNIQUE INDEX IF NOT EXISTS idx_tensors_id ON tensors(id);\n", + " \"\"\"\n", + " )\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE INDEX IF NOT EXISTS idx_tensors_origin ON tensors(origin);\n", + " \"\"\"\n", + " )\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE INDEX IF NOT EXISTS idx_tensors_tensor_kind ON tensors(tensor_kind);\n", + " \"\"\"\n", + " )\n", + " conn.commit()\n", + " finally:\n", + " lock.release()\n", + "\n", + "\n", + "# `tensors_results` stores additional data specific to `result` tensor\n", + "\n", + "\n", + "def create_tensors_results_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", + " try:\n", + " lock.acquire()\n", + "\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE TABLE IF NOT EXISTS tensors_results (\n", + " tensors_id TEXT PRIMARY KEY,\n", + " session_id TEXT NOT NULL,\n", + " node_id TEXT NOT NULL,\n", + " FOREIGN KEY(tensors_id) REFERENCES tensors(id) ON DELETE CASCADE\n", + " );\n", + " \"\"\"\n", + " )\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE UNIQUE INDEX IF NOT EXISTS idx_tensors_results_tensors_id ON tensors_results(tensors_id);\n", + " \"\"\"\n", + " )\n", + " conn.commit()\n", + " finally:\n", + " lock.release()\n", + "\n", + "\n", + "# `tensors_intermediates` stores additional data specific to `intermediate` tensor\n", + "\n", + "\n", + "def create_tensors_intermediates_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", + " try:\n", + " lock.acquire()\n", + "\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE TABLE IF NOT EXISTS tensors_intermediates (\n", + " tensors_id TEXT PRIMARY KEY,\n", + " session_id TEXT NOT NULL,\n", + " node_id TEXT NOT NULL,\n", + " FOREIGN KEY(tensors_id) REFERENCES tensors(id) ON DELETE CASCADE\n", + " );\n", + " \"\"\"\n", + " )\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE UNIQUE INDEX IF NOT EXISTS idx_tensors_intermediates_tensors_id ON tensors_intermediates(tensors_id);\n", + " \"\"\"\n", + " )\n", + " conn.commit()\n", + " finally:\n", + " lock.release()\n", + "\n", + "\n", + "# `tensors_metadata` table: stores generated/transformed metadata for tensor\n", + "\n", + "\n", + "def create_tensors_metadata_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", + " try:\n", + " lock.acquire()\n", + "\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE TABLE IF NOT EXISTS tensors_metadata (\n", + " tensors_id TEXT PRIMARY KEY,\n", + " metadata TEXT,\n", + " FOREIGN KEY(tensors_id) REFERENCES tensors(id) ON DELETE CASCADE\n", + " );\n", + " \"\"\"\n", + " )\n", + " cursor.execute(\n", + " \"\"\"--sql\n", + " CREATE UNIQUE INDEX IF NOT EXISTS idx_tensors_metadata_tensors_id ON tensors_metadata(tensors_id);\n", + " \"\"\"\n", + " )\n", + " conn.commit()\n", + " finally:\n", + " lock.release()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "db_path = '/home/bat/Documents/Code/outputs/test.db'\n", + "if (os.path.exists(db_path)):\n", + " os.remove(db_path)\n", + "\n", + "conn = sqlite3.connect(\n", + " db_path, check_same_thread=False\n", + ")\n", + "cursor = conn.cursor()\n", + "lock = threading.Lock()" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [], + "source": [ + "create_sql_table_from_enum(\n", + " enum=ResourceOrigin,\n", + " table_name=\"resource_origins\",\n", + " primary_key_name=\"origin_name\",\n", + " conn=conn,\n", + " cursor=cursor,\n", + " lock=lock,\n", + ")\n", + "\n", + "create_sql_table_from_enum(\n", + " enum=ImageKind,\n", + " table_name=\"image_kinds\",\n", + " primary_key_name=\"kind_name\",\n", + " conn=conn,\n", + " cursor=cursor,\n", + " lock=lock,\n", + ")\n", + "\n", + "create_sql_table_from_enum(\n", + " enum=TensorKind,\n", + " table_name=\"tensor_kinds\",\n", + " primary_key_name=\"kind_name\",\n", + " conn=conn,\n", + " cursor=cursor,\n", + " lock=lock,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [], + "source": [ + "create_images_table(conn, cursor, lock)\n", + "create_images_results_table(conn, cursor, lock)\n", + "create_images_intermediates_table(conn, cursor, lock)\n", + "create_images_metadata_table(conn, cursor, lock)" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [], + "source": [ + "create_tensors_table(conn, cursor, lock)\n", + "create_tensors_results_table(conn, cursor, lock)\n", + "create_tensors_intermediates_table(conn, cursor, lock)\n", + "create_tensors_metadata_table(conn, cursor, lock)" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "from pydantic import StrictStr\n", + "\n", + "\n", + "class GeneratedImageOrLatentsMetadata(BaseModel):\n", + " \"\"\"Core generation metadata for an image/tensor generated in InvokeAI.\n", + "\n", + " Generated by traversing the execution graph, collecting the parameters of the nearest ancestors of a given node.\n", + "\n", + " Full metadata may be accessed by querying for the session in the `graph_executions` table.\n", + " \"\"\"\n", + "\n", + " positive_conditioning: Optional[StrictStr] = Field(\n", + " default=None, description=\"The positive conditioning.\"\n", + " )\n", + " negative_conditioning: Optional[str] = Field(\n", + " default=None, description=\"The negative conditioning.\"\n", + " )\n", + " width: Optional[int] = Field(\n", + " default=None, description=\"Width of the image/tensor in pixels.\"\n", + " )\n", + " height: Optional[int] = Field(\n", + " default=None, description=\"Height of the image/tensor in pixels.\"\n", + " )\n", + " seed: Optional[int] = Field(\n", + " default=None, description=\"The seed used for noise generation.\"\n", + " )\n", + " cfg_scale: Optional[float] = Field(\n", + " default=None, description=\"The classifier-free guidance scale.\"\n", + " )\n", + " steps: Optional[int] = Field(\n", + " default=None, description=\"The number of steps used for inference.\"\n", + " )\n", + " scheduler: Optional[str] = Field(\n", + " default=None, description=\"The scheduler used for inference.\"\n", + " )\n", + " model: Optional[str] = Field(\n", + " default=None, description=\"The model used for inference.\"\n", + " )\n", + " strength: Optional[float] = Field(\n", + " default=None,\n", + " description=\"The strength used for image-to-image/tensor-to-tensor.\",\n", + " )\n", + " image: Optional[str] = Field(\n", + " default=None, description=\"The ID of the initial image.\"\n", + " )\n", + " tensor: Optional[str] = Field(\n", + " default=None, description=\"The ID of the initial tensor.\"\n", + " )\n", + " # Pending model refactor:\n", + " # vae: Optional[str] = Field(default=None,description=\"The VAE used for decoding.\")\n", + " # unet: Optional[str] = Field(default=None,description=\"The UNet used dor inference.\")\n", + " # clip: Optional[str] = Field(default=None,description=\"The CLIP Encoder used for conditioning.\")\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "GeneratedImageOrLatentsMetadata(positive_conditioning='123', negative_conditioning=None, width=None, height=None, seed=None, cfg_scale=None, steps=None, scheduler=None, model=None, strength=None, image=None, tensor=None)" + ] + }, + "execution_count": 61, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "GeneratedImageOrLatentsMetadata(positive_conditioning='123')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/invokeai/app/services/image_db.py b/invokeai/app/services/image_db.py new file mode 100644 index 0000000000..73984c6685 --- /dev/null +++ b/invokeai/app/services/image_db.py @@ -0,0 +1,329 @@ +from abc import ABC, abstractmethod +import datetime +from typing import Optional +from invokeai.app.models.metadata import ( + GeneratedImageOrLatentsMetadata, + UploadedImageOrLatentsMetadata, +) + +import sqlite3 +import threading +from typing import Optional, Union +from invokeai.app.models.metadata import ( + GeneratedImageOrLatentsMetadata, + UploadedImageOrLatentsMetadata, +) +from invokeai.app.models.image import ( + ImageCategory, + ImageType, +) +from invokeai.app.services.util.create_enum_table import create_enum_table +from invokeai.app.services.models.image_record import ImageRecord +from invokeai.app.services.util.deserialize_image_record import ( + deserialize_image_record, +) + +from invokeai.app.services.item_storage import PaginatedResults + + +class ImageRecordServiceBase(ABC): + """Low-level service responsible for interfacing with the image record store.""" + + class ImageRecordNotFoundException(Exception): + """Raised when an image record is not found.""" + + def __init__(self, message="Image record not found"): + super().__init__(message) + + class ImageRecordSaveException(Exception): + """Raised when an image record cannot be saved.""" + + def __init__(self, message="Image record not saved"): + super().__init__(message) + + class ImageRecordDeleteException(Exception): + """Raised when an image record cannot be deleted.""" + + def __init__(self, message="Image record not deleted"): + super().__init__(message) + + @abstractmethod + def get(self, image_type: ImageType, image_name: str) -> ImageRecord: + """Gets an image record.""" + pass + + @abstractmethod + def get_many( + self, + image_type: ImageType, + image_category: ImageCategory, + page: int = 0, + per_page: int = 10, + ) -> PaginatedResults[ImageRecord]: + """Gets a page of image records.""" + pass + + @abstractmethod + def delete(self, image_type: ImageType, image_name: str) -> None: + """Deletes an image record.""" + pass + + @abstractmethod + def save( + self, + image_name: str, + image_type: ImageType, + image_category: ImageCategory, + session_id: Optional[str], + node_id: Optional[str], + metadata: Optional[ + GeneratedImageOrLatentsMetadata | UploadedImageOrLatentsMetadata + ], + created_at: str = datetime.datetime.utcnow().isoformat(), + ) -> None: + """Saves an image record.""" + pass + + +class SqliteImageRecordService(ImageRecordServiceBase): + _filename: str + _conn: sqlite3.Connection + _cursor: sqlite3.Cursor + _lock: threading.Lock + + def __init__(self, filename: str) -> None: + super().__init__() + + self._filename = filename + self._conn = sqlite3.connect(filename, check_same_thread=False) + # Enable row factory to get rows as dictionaries (must be done before making the cursor!) + self._conn.row_factory = sqlite3.Row + self._cursor = self._conn.cursor() + self._lock = threading.Lock() + + try: + self._lock.acquire() + # Enable foreign keys + self._conn.execute("PRAGMA foreign_keys = ON;") + self._create_tables() + self._conn.commit() + finally: + self._lock.release() + + def _create_tables(self) -> None: + """Creates the tables for the `images` database.""" + + # Create the `images` table. + self._cursor.execute( + f"""--sql + CREATE TABLE IF NOT EXISTS images ( + id TEXT PRIMARY KEY, + image_type TEXT, -- non-nullable via foreign key constraint + image_category TEXT, -- non-nullable via foreign key constraint + session_id TEXT, -- nullable + node_id TEXT, -- nullable + metadata TEXT, -- nullable + created_at TEXT NOT NULL, + FOREIGN KEY(image_type) REFERENCES image_types(type_name), + FOREIGN KEY(image_category) REFERENCES image_categories(category_name) + ); + """ + ) + + # Create the `images` table indices. + self._cursor.execute( + """--sql + CREATE UNIQUE INDEX IF NOT EXISTS idx_images_id ON images(id); + """ + ) + self._cursor.execute( + """--sql + CREATE INDEX IF NOT EXISTS idx_images_image_type ON images(image_type); + """ + ) + self._cursor.execute( + """--sql + CREATE INDEX IF NOT EXISTS idx_images_image_category ON images(image_category); + """ + ) + + # Create the tables for image-related enums + create_enum_table( + enum=ImageType, + table_name="image_types", + primary_key_name="type_name", + cursor=self._cursor, + ) + + create_enum_table( + enum=ImageCategory, + table_name="image_categories", + primary_key_name="category_name", + cursor=self._cursor, + ) + + # Create the `tags` table. TODO: do this elsewhere, shouldn't be in images db service + self._cursor.execute( + """--sql + CREATE TABLE IF NOT EXISTS tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tag_name TEXT UNIQUE NOT NULL + ); + """ + ) + + # Create the `images_tags` junction table. + self._cursor.execute( + """--sql + CREATE TABLE IF NOT EXISTS images_tags ( + image_id TEXT, + tag_id INTEGER, + PRIMARY KEY (image_id, tag_id), + FOREIGN KEY(image_id) REFERENCES images(id) ON DELETE CASCADE, + FOREIGN KEY(tag_id) REFERENCES tags(id) ON DELETE CASCADE + ); + """ + ) + + # Create the `images_favorites` table. + self._cursor.execute( + """--sql + CREATE TABLE IF NOT EXISTS images_favorites ( + image_id TEXT PRIMARY KEY, + favorited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(image_id) REFERENCES images(id) ON DELETE CASCADE + ); + """ + ) + + def get(self, image_type: ImageType, image_name: str) -> Union[ImageRecord, None]: + try: + self._lock.acquire() + + self._cursor.execute( + f"""--sql + SELECT * FROM images + WHERE id = ?; + """, + (image_name,), + ) + + result = self._cursor.fetchone() + except sqlite3.Error as e: + self._conn.rollback() + raise self.ImageRecordNotFoundException from e + finally: + self._lock.release() + + if not result: + raise self.ImageRecordNotFoundException + + return deserialize_image_record(result) + + def get_many( + self, + image_type: ImageType, + image_category: ImageCategory, + page: int = 0, + per_page: int = 10, + ) -> PaginatedResults[ImageRecord]: + try: + self._lock.acquire() + + self._cursor.execute( + f"""--sql + SELECT * FROM images + WHERE image_type = ? AND image_category = ? + LIMIT ? OFFSET ?; + """, + (image_type.value, image_category.value, per_page, page * per_page), + ) + + result = self._cursor.fetchall() + + images = list(map(lambda r: deserialize_image_record(r), result)) + + self._cursor.execute( + """--sql + SELECT count(*) FROM images + WHERE image_type = ? AND image_category = ? + """, + (image_type.value, image_category.value), + ) + + count = self._cursor.fetchone()[0] + except sqlite3.Error as e: + self._conn.rollback() + raise e + finally: + self._lock.release() + + pageCount = int(count / per_page) + 1 + + return PaginatedResults( + items=images, page=page, pages=pageCount, per_page=per_page, total=count + ) + + def delete(self, image_type: ImageType, image_name: str) -> None: + try: + self._lock.acquire() + self._cursor.execute( + """--sql + DELETE FROM images + WHERE id = ?; + """, + (image_name,), + ) + self._conn.commit() + except sqlite3.Error as e: + self._conn.rollback() + raise ImageRecordServiceBase.ImageRecordDeleteException from e + finally: + self._lock.release() + + def save( + self, + image_name: str, + image_type: ImageType, + image_category: ImageCategory, + session_id: Optional[str], + node_id: Optional[str], + metadata: Union[ + GeneratedImageOrLatentsMetadata, UploadedImageOrLatentsMetadata, None + ], + created_at: str, + ) -> None: + try: + metadata_json = ( + None if metadata is None else metadata.json(exclude_none=True) + ) + self._lock.acquire() + self._cursor.execute( + """--sql + INSERT OR IGNORE INTO images ( + id, + image_type, + image_category, + node_id, + session_id, + metadata + created_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?); + """, + ( + image_name, + image_type.value, + image_category.value, + node_id, + session_id, + metadata_json, + created_at, + ), + ) + self._conn.commit() + except sqlite3.Error as e: + self._conn.rollback() + raise ImageRecordServiceBase.ImageRecordNotFoundException from e + finally: + self._lock.release() diff --git a/invokeai/app/services/image_storage.py b/invokeai/app/services/image_storage.py index e2593dd473..7610ac62bf 100644 --- a/invokeai/app/services/image_storage.py +++ b/invokeai/app/services/image_storage.py @@ -27,7 +27,25 @@ from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail class ImageStorageBase(ABC): - """Responsible for storing and retrieving images.""" + """Low-level service responsible for storing and retrieving images.""" + + class ImageFileNotFoundException(Exception): + """Raised when an image file is not found in storage.""" + + def __init__(self, message="Image file not found"): + super().__init__(message) + + class ImageFileSaveException(Exception): + """Raised when an image cannot be saved.""" + + def __init__(self, message="Image file not saved"): + super().__init__(message) + + class ImageFileDeleteException(Exception): + """Raised when an image cannot be deleted.""" + + def __init__(self, message="Image file not deleted"): + super().__init__(message) @abstractmethod def get(self, image_type: ImageType, image_name: str) -> Image: @@ -136,7 +154,7 @@ class DiskImageStorage(ImageStorageBase): page_of_images.append( ImageResponse( - image_type=image_type.value, + image_type=image_type, image_name=filename, # TODO: DiskImageStorage should not be building URLs...? image_url=self.get_uri(image_type, filename), @@ -164,14 +182,17 @@ class DiskImageStorage(ImageStorageBase): ) def get(self, image_type: ImageType, image_name: str) -> Image: - image_path = self.get_path(image_type, image_name) - cache_item = self.__get_cache(image_path) - if cache_item: - return cache_item + try: + image_path = self.get_path(image_type, image_name) + cache_item = self.__get_cache(image_path) + if cache_item: + return cache_item - image = PILImage.open(image_path) - self.__set_cache(image_path, image) - return image + image = PILImage.open(image_path) + self.__set_cache(image_path, image) + return image + except Exception as e: + raise ImageStorageBase.ImageFileNotFoundException from e # TODO: make this a bit more flexible for e.g. cloud storage def get_path( @@ -209,8 +230,10 @@ class DiskImageStorage(ImageStorageBase): try: os.stat(path) return True - except Exception: + except FileNotFoundError: return False + except Exception as e: + raise e def save( self, @@ -219,45 +242,53 @@ class DiskImageStorage(ImageStorageBase): image: Image, metadata: InvokeAIMetadata | None = None, ) -> SavedImage: - image_path = self.get_path(image_type, image_name) + try: + image_path = self.get_path(image_type, image_name) - # TODO: Reading the image and then saving it strips the metadata... - if metadata: - pnginfo = build_invokeai_metadata_pnginfo(metadata=metadata) - image.save(image_path, "PNG", pnginfo=pnginfo) - else: - image.save(image_path) # this saved image has an empty info + # TODO: Reading the image and then saving it strips the metadata... + if metadata: + pnginfo = build_invokeai_metadata_pnginfo(metadata=metadata) + image.save(image_path, "PNG", pnginfo=pnginfo) + else: + image.save(image_path) # this saved image has an empty info - thumbnail_name = get_thumbnail_name(image_name) - thumbnail_path = self.get_path(image_type, thumbnail_name, is_thumbnail=True) - thumbnail_image = make_thumbnail(image) - thumbnail_image.save(thumbnail_path) + thumbnail_name = get_thumbnail_name(image_name) + thumbnail_path = self.get_path( + image_type, thumbnail_name, is_thumbnail=True + ) + thumbnail_image = make_thumbnail(image) + thumbnail_image.save(thumbnail_path) - self.__set_cache(image_path, image) - self.__set_cache(thumbnail_path, thumbnail_image) + self.__set_cache(image_path, image) + self.__set_cache(thumbnail_path, thumbnail_image) - return SavedImage( - image_name=image_name, - thumbnail_name=thumbnail_name, - created=int(os.path.getctime(image_path)), - ) + return SavedImage( + image_name=image_name, + thumbnail_name=thumbnail_name, + created=int(os.path.getctime(image_path)), + ) + except Exception as e: + raise ImageStorageBase.ImageFileSaveException from e def delete(self, image_type: ImageType, image_name: str) -> None: - basename = os.path.basename(image_name) - image_path = self.get_path(image_type, basename) + try: + basename = os.path.basename(image_name) + image_path = self.get_path(image_type, basename) - if os.path.exists(image_path): - send2trash(image_path) - if image_path in self.__cache: - del self.__cache[image_path] + if os.path.exists(image_path): + send2trash(image_path) + if image_path in self.__cache: + del self.__cache[image_path] - thumbnail_name = get_thumbnail_name(image_name) - thumbnail_path = self.get_path(image_type, thumbnail_name, True) + thumbnail_name = get_thumbnail_name(image_name) + thumbnail_path = self.get_path(image_type, thumbnail_name, True) - if os.path.exists(thumbnail_path): - send2trash(thumbnail_path) - if thumbnail_path in self.__cache: - del self.__cache[thumbnail_path] + if os.path.exists(thumbnail_path): + send2trash(thumbnail_path) + if thumbnail_path in self.__cache: + del self.__cache[thumbnail_path] + except Exception as e: + raise ImageStorageBase.ImageFileDeleteException from e def __get_cache(self, image_name: str) -> Image | None: return None if image_name not in self.__cache else self.__cache[image_name] diff --git a/invokeai/app/services/images.py b/invokeai/app/services/images.py new file mode 100644 index 0000000000..190ddaa8d6 --- /dev/null +++ b/invokeai/app/services/images.py @@ -0,0 +1,219 @@ +from typing import Union +import uuid +from PIL.Image import Image as PILImageType +from invokeai.app.models.image import ImageCategory, ImageType +from invokeai.app.models.metadata import ( + GeneratedImageOrLatentsMetadata, + UploadedImageOrLatentsMetadata, +) +from invokeai.app.services.image_db import ( + ImageRecordServiceBase, +) +from invokeai.app.services.models.image_record import ImageRecord +from invokeai.app.services.image_storage import ImageStorageBase +from invokeai.app.services.item_storage import PaginatedResults +from invokeai.app.services.metadata import MetadataServiceBase +from invokeai.app.services.urls import UrlServiceBase +from invokeai.app.util.misc import get_iso_timestamp + + +class ImageServiceDependencies: + """Service dependencies for the ImageManagementService.""" + + db: ImageRecordServiceBase + storage: ImageStorageBase + metadata: MetadataServiceBase + urls: UrlServiceBase + + def __init__( + self, + image_db_service: ImageRecordServiceBase, + image_storage_service: ImageStorageBase, + image_metadata_service: MetadataServiceBase, + url_service: UrlServiceBase, + ): + self.db = image_db_service + self.storage = image_storage_service + self.metadata = image_metadata_service + self.url = url_service + + +class ImageService: + """High-level service for image management.""" + + _services: ImageServiceDependencies + + def __init__( + self, + image_db_service: ImageRecordServiceBase, + image_storage_service: ImageStorageBase, + image_metadata_service: MetadataServiceBase, + url_service: UrlServiceBase, + ): + self._services = ImageServiceDependencies( + image_db_service=image_db_service, + image_storage_service=image_storage_service, + image_metadata_service=image_metadata_service, + url_service=url_service, + ) + + def _create_image_name( + self, + image_type: ImageType, + image_category: ImageCategory, + node_id: Union[str, None], + session_id: Union[str, None], + ) -> str: + """Creates an image name.""" + uuid_str = str(uuid.uuid4()) + + if node_id is not None and session_id is not None: + return f"{image_type.value}_{image_category.value}_{session_id}_{node_id}_{uuid_str}.png" + + return f"{image_type.value}_{image_category.value}_{uuid_str}.png" + + def create( + self, + image: PILImageType, + image_type: ImageType, + image_category: ImageCategory, + node_id: Union[str, None], + session_id: Union[str, None], + metadata: Union[ + GeneratedImageOrLatentsMetadata, UploadedImageOrLatentsMetadata, None + ], + ) -> ImageRecord: + """Creates an image, storing the file and its metadata.""" + image_name = self._create_image_name( + image_type=image_type, + image_category=image_category, + node_id=node_id, + session_id=session_id, + ) + + timestamp = get_iso_timestamp() + + try: + # TODO: Consider using a transaction here to ensure consistency between storage and database + self._services.storage.save( + image_type=image_type, + image_name=image_name, + image=image, + metadata=metadata, + ) + + self._services.db.save( + image_name=image_name, + image_type=image_type, + image_category=image_category, + node_id=node_id, + session_id=session_id, + metadata=metadata, + created_at=timestamp, + ) + + image_url = self._services.url.get_image_url( + image_type=image_type, image_name=image_name + ) + + thumbnail_url = self._services.url.get_thumbnail_url( + image_type=image_type, image_name=image_name + ) + + return ImageRecord( + image_name=image_name, + image_type=image_type, + image_category=image_category, + node_id=node_id, + session_id=session_id, + metadata=metadata, + created_at=timestamp, + image_url=image_url, + thumbnail_url=thumbnail_url, + ) + except ImageRecordServiceBase.ImageRecordSaveException: + # TODO: log this + raise + except ImageStorageBase.ImageFileSaveException: + # TODO: log this + raise + + def get_pil_image(self, image_type: ImageType, image_name: str) -> PILImageType: + """Gets an image as a PIL image.""" + try: + pil_image = self._services.storage.get( + image_type=image_type, image_name=image_name + ) + return pil_image + except ImageStorageBase.ImageFileNotFoundException: + # TODO: log this + raise + + def get_record(self, image_type: ImageType, image_name: str) -> ImageRecord: + """Gets an image record.""" + try: + image_record = self._services.db.get( + image_type=image_type, image_name=image_name + ) + return image_record + except ImageRecordServiceBase.ImageRecordNotFoundException: + # TODO: log this + raise + + def delete(self, image_type: ImageType, image_name: str): + """Deletes an image.""" + # TODO: Consider using a transaction here to ensure consistency between storage and database + try: + self._services.storage.delete(image_type=image_type, image_name=image_name) + self._services.db.delete(image_type=image_type, image_name=image_name) + except ImageRecordServiceBase.ImageRecordDeleteException: + # TODO: log this + raise + except ImageStorageBase.ImageFileDeleteException: + # TODO: log this + raise + + def get_many( + self, + image_type: ImageType, + image_category: ImageCategory, + page: int = 0, + per_page: int = 10, + ) -> PaginatedResults[ImageRecord]: + """Gets a paginated list of image records.""" + try: + results = self._services.db.get_many( + image_type=image_type, + image_category=image_category, + page=page, + per_page=per_page, + ) + + for r in results.items: + r.image_url = self._services.url.get_image_url( + image_type=image_type, image_name=r.image_name + ) + + r.thumbnail_url = self._services.url.get_thumbnail_url( + image_type=image_type, image_name=r.image_name + ) + + return results + except Exception as e: + raise e + + def add_tag(self, image_type: ImageType, image_id: str, tag: str) -> None: + """Adds a tag to an image.""" + raise NotImplementedError("The 'add_tag' method is not implemented yet.") + + def remove_tag(self, image_type: ImageType, image_id: str, tag: str) -> None: + """Removes a tag from an image.""" + raise NotImplementedError("The 'remove_tag' method is not implemented yet.") + + def favorite(self, image_type: ImageType, image_id: str) -> None: + """Favorites an image.""" + raise NotImplementedError("The 'favorite' method is not implemented yet.") + + def unfavorite(self, image_type: ImageType, image_id: str) -> None: + """Unfavorites an image.""" + raise NotImplementedError("The 'unfavorite' method is not implemented yet.") diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index d4c0c06b65..74fb7accff 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -1,7 +1,12 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI Team -from typing import types +from types import ModuleType +from invokeai.app.services.image_db import ( + ImageRecordServiceBase, +) +from invokeai.app.services.images import ImageService from invokeai.app.services.metadata import MetadataServiceBase +from invokeai.app.services.urls import UrlServiceBase from invokeai.backend import ModelManager from .events import EventServiceBase @@ -12,6 +17,7 @@ from .invocation_queue import InvocationQueueABC from .item_storage import ItemStorageABC from .config import InvokeAISettings + class InvocationServices: """Services that can be used by invocations""" @@ -23,26 +29,32 @@ class InvocationServices: model_manager: ModelManager restoration: RestorationServices configuration: InvokeAISettings - + images_db: ImageRecordServiceBase + urls: UrlServiceBase + images_new: ImageService + # NOTE: we must forward-declare any types that include invocations, since invocations can use services graph_library: ItemStorageABC["LibraryGraph"] graph_execution_manager: ItemStorageABC["GraphExecutionState"] processor: "InvocationProcessorABC" def __init__( - self, - model_manager: ModelManager, - events: EventServiceBase, - logger: types.ModuleType, - latents: LatentsStorageBase, - images: ImageStorageBase, - metadata: MetadataServiceBase, - queue: InvocationQueueABC, - graph_library: ItemStorageABC["LibraryGraph"], - graph_execution_manager: ItemStorageABC["GraphExecutionState"], - processor: "InvocationProcessorABC", - restoration: RestorationServices, - configuration: InvokeAISettings=None, + self, + model_manager: ModelManager, + events: EventServiceBase, + logger: ModuleType, + latents: LatentsStorageBase, + images: ImageStorageBase, + metadata: MetadataServiceBase, + queue: InvocationQueueABC, + images_db: ImageRecordServiceBase, + images_new: ImageService, + urls: UrlServiceBase, + graph_library: ItemStorageABC["LibraryGraph"], + graph_execution_manager: ItemStorageABC["GraphExecutionState"], + processor: "InvocationProcessorABC", + restoration: RestorationServices, + configuration: InvokeAISettings=None, ): self.model_manager = model_manager self.events = events @@ -51,8 +63,13 @@ class InvocationServices: self.images = images self.metadata = metadata self.queue = queue + self.images_db = images_db + self.images_new = images_new + self.urls = urls self.graph_library = graph_library self.graph_execution_manager = graph_execution_manager self.processor = processor self.restoration = restoration self.configuration = configuration + + diff --git a/invokeai/app/services/metadata.py b/invokeai/app/services/metadata.py index a7f2378ab1..bc1cfdb063 100644 --- a/invokeai/app/services/metadata.py +++ b/invokeai/app/services/metadata.py @@ -22,16 +22,24 @@ class MetadataLatentsField(TypedDict): class MetadataColorField(TypedDict): """Pydantic-less ColorField, used for metadata parsing""" + r: int g: int b: int a: int - # TODO: This is a placeholder for `InvocationsUnion` pending resolution of circular imports NodeMetadata = Dict[ - str, None | str | int | float | bool | MetadataImageField | MetadataLatentsField | MetadataColorField + str, + None + | str + | int + | float + | bool + | MetadataImageField + | MetadataLatentsField + | MetadataColorField, ] @@ -67,6 +75,11 @@ class MetadataServiceBase(ABC): """Builds an InvokeAIMetadata object""" pass + @abstractmethod + def create_metadata(self, session_id: str, node_id: str) -> dict: + """Creates metadata for a result""" + pass + class PngMetadataService(MetadataServiceBase): """Handles loading and building metadata for images.""" diff --git a/invokeai/app/services/models/image_record.py b/invokeai/app/services/models/image_record.py new file mode 100644 index 0000000000..600508e57f --- /dev/null +++ b/invokeai/app/services/models/image_record.py @@ -0,0 +1,29 @@ +import datetime +from typing import Literal, Optional, Union +from pydantic import BaseModel, Field +from invokeai.app.models.metadata import ( + GeneratedImageOrLatentsMetadata, + UploadedImageOrLatentsMetadata, +) +from invokeai.app.models.image import ImageCategory, ImageType +from invokeai.app.models.resources import ResourceType + + +class ImageRecord(BaseModel): + """Deserialized image record.""" + + image_name: str = Field(description="The name of the image.") + image_type: ImageType = Field(description="The type of the image.") + image_category: ImageCategory = Field(description="The category of the image.") + created_at: Union[datetime.datetime, str] = Field( + description="The created timestamp of the image." + ) + session_id: Optional[str] = Field(default=None, description="The session ID.") + node_id: Optional[str] = Field(default=None, description="The node ID.") + metadata: Optional[ + Union[GeneratedImageOrLatentsMetadata, UploadedImageOrLatentsMetadata] + ] = Field(default=None, description="The image's metadata.") + image_url: Optional[str] = Field(default=None, description="The URL of the image.") + thumbnail_url: Optional[str] = Field( + default=None, description="The thumbnail URL of the image." + ) diff --git a/invokeai/app/services/proposeddesign.py b/invokeai/app/services/proposeddesign.py new file mode 100644 index 0000000000..712d7224e9 --- /dev/null +++ b/invokeai/app/services/proposeddesign.py @@ -0,0 +1,657 @@ +from abc import ABC, abstractmethod +from enum import Enum +import enum +import sqlite3 +import threading +from typing import Optional, Type, TypeVar, Union +from PIL.Image import Image as PILImage +from pydantic import BaseModel, Field +from torch import Tensor + +from invokeai.app.services.item_storage import PaginatedResults + + +""" +Substantial proposed changes to the management of images and tensor. + +tl;dr: +With the upcoming move to latents-only nodes, we need to handle metadata differently. After struggling with this unsuccessfully - trying to smoosh it in to the existing setup - I believe we need to expand the scope of the refactor to include the management of images and latents - and make `latents` a special case of `tensor`. + +full story: +The consensus for tensor-only nodes' metadata was to traverse the execution graph and grab the core parameters to write to the image. This was straightforward, and I've written functions to find the nearest t2l/l2l, noise, and compel nodes and build the metadata from those. + +But struggling to integrate this and the associated edge cases this brought up a number of issues deeper in the system (some of which I had previously implemented). The ImageStorageService is doing way too much, and we have a need to be able to retrieve sessions the session given image/latents id, which is not currently feasible due to SQLite's JSON parsing performance. + +I made a new ResultsService and `results` table in the db to facilitate this. This first attempt failed because it doesn't handle uploads and leaves the codebase messy. + +So I've spent the day trying to figure out to handle this in a sane way and think I've got something decent. I've described some changes to service bases and the database below. + +The gist of it is to store the core parameters for an image in its metadata when the image is saved, but never to read from it. Instead, the same metadata is stored in the database, which will be set up for efficient access. So when a page of images is requested, the metadata comes from the db instead of a filesystem operation. + +The URL generation responsibilities have been split off the image storage service in to a URL service. New database services/tables for images and tensor are added. These services will provide paginated images/tensors for the API to serve. This also paves the way for handling tensors as first-class outputs. +""" + + +# TODO: Make a new model for this +class ResourceOrigin(str, Enum): + """The origin of a resource (eg image or tensor).""" + + RESULTS = "results" + UPLOADS = "uploads" + INTERMEDIATES = "intermediates" + + +class ImageKind(str, Enum): + """The kind of an image.""" + + IMAGE = "image" + CONTROL_IMAGE = "control_image" + + +class TensorKind(str, Enum): + """The kind of a tensor.""" + + IMAGE_TENSOR = "tensor" + CONDITIONING = "conditioning" + + +""" +Core Generation Metadata Pydantic Model + +I've already implemented the code to traverse a session to build this object. +""" + + +class CoreGenerationMetadata(BaseModel): + """Core generation metadata for an image/tensor generated in InvokeAI. + + Generated by traversing the execution graph, collecting the parameters of the nearest ancestors of a given node. + + Full metadata may be accessed by querying for the session in the `graph_executions` table. + """ + + positive_conditioning: Optional[str] = Field( + description="The positive conditioning." + ) + negative_conditioning: Optional[str] = Field( + description="The negative conditioning." + ) + width: Optional[int] = Field(description="Width of the image/tensor in pixels.") + height: Optional[int] = Field(description="Height of the image/tensor in pixels.") + seed: Optional[int] = Field(description="The seed used for noise generation.") + cfg_scale: Optional[float] = Field( + description="The classifier-free guidance scale." + ) + steps: Optional[int] = Field(description="The number of steps used for inference.") + scheduler: Optional[str] = Field(description="The scheduler used for inference.") + model: Optional[str] = Field(description="The model used for inference.") + strength: Optional[float] = Field( + description="The strength used for image-to-image/tensor-to-tensor." + ) + image: Optional[str] = Field(description="The ID of the initial image.") + tensor: Optional[str] = Field(description="The ID of the initial tensor.") + # Pending model refactor: + # vae: Optional[str] = Field(description="The VAE used for decoding.") + # unet: Optional[str] = Field(description="The UNet used dor inference.") + # clip: Optional[str] = Field(description="The CLIP Encoder used for conditioning.") + + +""" +Minimal Uploads Metadata Model +""" + + +class UploadsMetadata(BaseModel): + """Limited metadata for an uploaded image/tensor.""" + + width: Optional[int] = Field(description="Width of the image/tensor in pixels.") + height: Optional[int] = Field(description="Height of the image/tensor in pixels.") + # The extra field will be the contents of the PNG file's tEXt chunk. It may have come + # from another SD application or InvokeAI, so we need to make it very flexible. I think it's + # best to just store it as a string and let the frontend parse it. + # If the upload is a tensor type, this will be omitted. + extra: Optional[str] = Field( + description="Extra metadata, extracted from the PNG tEXt chunk." + ) + + +""" +Slimmed-down Image Storage Service Base + - No longer lists images or generates URLs - only stores and retrieves images. + - OSS implementation for disk storage +""" + + +class ImageStorageBase(ABC): + """Responsible for storing and retrieving images.""" + + @abstractmethod + def save( + self, + image: PILImage, + image_kind: ImageKind, + origin: ResourceOrigin, + context_id: str, + node_id: str, + metadata: CoreGenerationMetadata, + ) -> str: + """Saves an image and its thumbnail, returning its unique identifier.""" + pass + + @abstractmethod + def get(self, id: str, thumbnail: bool = False) -> Union[PILImage, None]: + """Retrieves an image as a PIL Image.""" + pass + + @abstractmethod + def delete(self, id: str) -> None: + """Deletes an image.""" + pass + + +class TensorStorageBase(ABC): + """Responsible for storing and retrieving tensors.""" + + @abstractmethod + def save( + self, + tensor: Tensor, + tensor_kind: TensorKind, + origin: ResourceOrigin, + context_id: str, + node_id: str, + metadata: CoreGenerationMetadata, + ) -> str: + """Saves a tensor, returning its unique identifier.""" + pass + + @abstractmethod + def get(self, id: str, thumbnail: bool = False) -> Union[Tensor, None]: + """Retrieves a tensor as a torch Tensor.""" + pass + + @abstractmethod + def delete(self, id: str) -> None: + """Deletes a tensor.""" + pass + + +""" +New Url Service Base + - Abstracts the logic for generating URLs out of the storage service + - OSS implementation for locally-hosted URLs + - Also provides a method to get the internal path to a resource (for OSS, the FS path) +""" + + +class ResourceLocationServiceBase(ABC): + """Responsible for locating resources (eg images or tensors).""" + + @abstractmethod + def get_url(self, id: str) -> str: + """Gets the URL for a resource.""" + pass + + @abstractmethod + def get_path(self, id: str) -> str: + """Gets the path for a resource.""" + pass + + +""" +New Images Database Service Base + +This is a new service that will be responsible for the new `images` table(s): + - Storing images in the table + - Retrieving individual images and pages of images + - Deleting individual images + +Operations will typically use joins with the various `images` tables. +""" + + +class ImagesDbServiceBase(ABC): + """Responsible for interfacing with `images` table.""" + + class GeneratedImageEntity(BaseModel): + id: str = Field(description="The unique identifier for the image.") + session_id: str = Field(description="The session ID.") + node_id: str = Field(description="The node ID.") + metadata: CoreGenerationMetadata = Field( + description="The metadata for the image." + ) + + class UploadedImageEntity(BaseModel): + id: str = Field(description="The unique identifier for the image.") + metadata: UploadsMetadata = Field(description="The metadata for the image.") + + @abstractmethod + def get(self, id: str) -> Union[GeneratedImageEntity, UploadedImageEntity, None]: + """Gets an image from the `images` table.""" + pass + + @abstractmethod + def get_many( + self, image_kind: ImageKind, page: int = 0, per_page: int = 10 + ) -> PaginatedResults[Union[GeneratedImageEntity, UploadedImageEntity]]: + """Gets a page of images from the `images` table.""" + pass + + @abstractmethod + def delete(self, id: str) -> None: + """Deletes an image from the `images` table.""" + pass + + @abstractmethod + def set( + self, + id: str, + image_kind: ImageKind, + session_id: Optional[str], + node_id: Optional[str], + metadata: CoreGenerationMetadata | UploadsMetadata, + ) -> None: + """Sets an image in the `images` table.""" + pass + + +""" +New Tensor Database Service Base + +This is a new service that will be responsible for the new `tensor` table: + - Storing tensor in the table + - Retrieving individual tensor and pages of tensor + - Deleting individual tensor + +Operations will always use joins with the `tensor_metadata` table. +""" + + +class TensorDbServiceBase(ABC): + """Responsible for interfacing with `tensor` table.""" + + class GeneratedTensorEntity(BaseModel): + id: str = Field(description="The unique identifier for the tensor.") + session_id: str = Field(description="The session ID.") + node_id: str = Field(description="The node ID.") + metadata: CoreGenerationMetadata = Field( + description="The metadata for the tensor." + ) + + class UploadedTensorEntity(BaseModel): + id: str = Field(description="The unique identifier for the tensor.") + metadata: UploadsMetadata = Field(description="The metadata for the tensor.") + + @abstractmethod + def get(self, id: str) -> Union[GeneratedTensorEntity, UploadedTensorEntity, None]: + """Gets a tensor from the `tensor` table.""" + pass + + @abstractmethod + def get_many( + self, tensor_kind: TensorKind, page: int = 0, per_page: int = 10 + ) -> PaginatedResults[Union[GeneratedTensorEntity, UploadedTensorEntity]]: + """Gets a page of tensor from the `tensor` table.""" + pass + + @abstractmethod + def delete(self, id: str) -> None: + """Deletes a tensor from the `tensor` table.""" + pass + + @abstractmethod + def set( + self, + id: str, + tensor_kind: TensorKind, + session_id: Optional[str], + node_id: Optional[str], + metadata: CoreGenerationMetadata | UploadsMetadata, + ) -> None: + """Sets a tensor in the `tensor` table.""" + pass + + +""" +Database Changes + +The existing tables will remain as-is, new tables will be added. + +Tensor now also have the same types as images - `results`, `intermediates`, `uploads`. Storage, retrieval, and operations may diverge from images in the future, so they are managed separately. + +A few `images` tables are created to store all images: + - `results` and `intermediates` images have additional data: `session_id` and `node_id`, and may be further differentiated in the future. For this reason, they each get their own table. + - `uploads` do not get their own table, as they are never going to have more than an `id`, `image_kind` and `timestamp`. + - `images_metadata` holds the same image metadata that is written to the image. This table, along with the URL service, allow us to more efficiently serve images without having to read the image from storage. + +The same tables are made for `tensor` and for the moment, implementation is expected to be identical. + +Schemas for each table below. + +Insertions and updates of ancillary tables (e.g. `results_images`, `images_metadata`, etc) will need to be done manually in the services, but should be straightforward. Deletion via cascading will be handled by the database. +""" + + +def create_sql_values_string_from_string_enum(enum: Type[Enum]): + """ + Creates a string of the form "('value1'), ('value2'), ..., ('valueN')" from a StrEnum. + """ + + delimiter = ", " + values = [f"('{e.value}')" for e in enum] + return delimiter.join(values) + + +def create_sql_table_from_enum( + enum: Type[Enum], + table_name: str, + primary_key_name: str, + cursor: sqlite3.Cursor, + lock: threading.Lock, +): + """ + Creates and populates a table to be used as a functional enum. + """ + + try: + lock.acquire() + + values_string = create_sql_values_string_from_string_enum(enum) + + cursor.execute( + f"""--sql + CREATE TABLE IF NOT EXISTS {table_name} ( + {primary_key_name} TEXT PRIMARY KEY + ); + """ + ) + cursor.execute( + f"""--sql + INSERT OR IGNORE INTO {table_name} ({primary_key_name}) VALUES {values_string}; + """ + ) + finally: + lock.release() + + +""" +`resource_origins` functions as an enum for the ResourceOrigin model. +""" + + +def create_resource_origins_table(cursor: sqlite3.Cursor, lock: threading.Lock): + create_sql_table_from_enum( + enum=ResourceOrigin, + table_name="resource_origins", + primary_key_name="origin_name", + cursor=cursor, + lock=lock, + ) + + +""" +`image_kinds` functions as an enum for the ImageType model. +""" + + +def create_image_kinds_table(cursor: sqlite3.Cursor, lock: threading.Lock): + create_sql_table_from_enum( + enum=ImageKind, + table_name="image_kinds", + primary_key_name="kind_name", + cursor=cursor, + lock=lock, + ) + + +""" +`tensor_kinds` functions as an enum for the TensorType model. +""" + + +def create_tensor_kinds_table(cursor: sqlite3.Cursor, lock: threading.Lock): + create_sql_table_from_enum( + enum=TensorKind, + table_name="tensor_kinds", + primary_key_name="kind_name", + cursor=cursor, + lock=lock, + ) + + +""" +`images` stores all images, regardless of type +""" + + +def create_images_table(cursor: sqlite3.Cursor, lock: threading.Lock): + try: + lock.acquire() + + cursor.execute( + """--sql + CREATE TABLE IF NOT EXISTS images ( + id TEXT PRIMARY KEY, + origin TEXT, + image_kind TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(origin) REFERENCES resource_origins(origin_name), + FOREIGN KEY(image_kind) REFERENCES image_kinds(kind_name) + ); + """ + ) + cursor.execute( + """--sql + CREATE UNIQUE INDEX IF NOT EXISTS idx_images_id ON images(id); + """ + ) + cursor.execute( + """--sql + CREATE INDEX IF NOT EXISTS idx_images_origin ON images(origin); + """ + ) + cursor.execute( + """--sql + CREATE INDEX IF NOT EXISTS idx_images_image_kind ON images(image_kind); + """ + ) + finally: + lock.release() + + +""" +`image_results` stores additional data specific to `results` images. +""" + + +def create_image_results_table(cursor: sqlite3.Cursor, lock: threading.Lock): + try: + lock.acquire() + + cursor.execute( + """--sql + CREATE TABLE IF NOT EXISTS image_results ( + images_id TEXT PRIMARY KEY, + session_id TEXT NOT NULL, + node_id TEXT NOT NULL, + FOREIGN KEY(images_id) REFERENCES images(id) ON DELETE CASCADE + ); + """ + ) + cursor.execute( + """--sql + CREATE UNIQUE INDEX IF NOT EXISTS idx_image_results_images_id ON image_results(id); + """ + ) + finally: + lock.release() + + +""" +`image_intermediates` stores additional data specific to `intermediates` images +""" + + +def create_image_intermediates_table(cursor: sqlite3.Cursor, lock: threading.Lock): + try: + lock.acquire() + + cursor.execute( + """--sql + CREATE TABLE IF NOT EXISTS image_intermediates ( + images_id TEXT PRIMARY KEY, + session_id TEXT NOT NULL, + node_id TEXT NOT NULL, + FOREIGN KEY(images_id) REFERENCES images(id) ON DELETE CASCADE + ); + """ + ) + cursor.execute( + """--sql + CREATE UNIQUE INDEX IF NOT EXISTS idx_image_intermediates_images_id ON image_intermediates(id); + """ + ) + finally: + lock.release() + + +""" +`images_metadata` stores basic metadata for any image type +""" + + +def create_images_metadata_table(cursor: sqlite3.Cursor, lock: threading.Lock): + try: + lock.acquire() + + cursor.execute( + """--sql + CREATE TABLE IF NOT EXISTS images_metadata ( + images_id TEXT PRIMARY KEY, + metadata TEXT, + FOREIGN KEY(images_id) REFERENCES images(id) ON DELETE CASCADE + ); + """ + ) + cursor.execute( + """--sql + CREATE UNIQUE INDEX IF NOT EXISTS idx_images_metadata_images_id ON images_metadata(images_id); + """ + ) + finally: + lock.release() + + +# `tensor` table: stores references to tensor + + +def create_tensors_table(cursor: sqlite3.Cursor, lock: threading.Lock): + try: + lock.acquire() + + cursor.execute( + """--sql + CREATE TABLE IF NOT EXISTS tensors ( + id TEXT PRIMARY KEY, + origin TEXT, + tensor_kind TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(origin) REFERENCES resource_origins(origin_name), + FOREIGN KEY(tensor_kind) REFERENCES tensor_kinds(kind_name), + ); + """ + ) + cursor.execute( + """--sql + CREATE UNIQUE INDEX IF NOT EXISTS idx_tensors_id ON tensors(id); + """ + ) + cursor.execute( + """--sql + CREATE INDEX IF NOT EXISTS idx_tensors_origin ON tensors(origin); + """ + ) + cursor.execute( + """--sql + CREATE INDEX IF NOT EXISTS idx_tensors_tensor_kind ON tensors(tensor_kind); + """ + ) + finally: + lock.release() + + +# `results_tensor` stores additional data specific to `result` tensor + + +def create_tensor_results_table(cursor: sqlite3.Cursor, lock: threading.Lock): + try: + lock.acquire() + + cursor.execute( + """--sql + CREATE TABLE IF NOT EXISTS tensor_results ( + tensor_id TEXT PRIMARY KEY, + session_id TEXT NOT NULL, + node_id TEXT NOT NULL, + FOREIGN KEY(tensor_id) REFERENCES tensors(id) ON DELETE CASCADE + ); + """ + ) + cursor.execute( + """--sql + CREATE UNIQUE INDEX IF NOT EXISTS idx_tensor_results_tensor_id ON tensor_results(tensor_id); + """ + ) + finally: + lock.release() + + +# `tensor_intermediates` stores additional data specific to `intermediate` tensor + + +def create_tensor_intermediates_table(cursor: sqlite3.Cursor, lock: threading.Lock): + try: + lock.acquire() + + cursor.execute( + """--sql + CREATE TABLE IF NOT EXISTS tensor_intermediates ( + tensor_id TEXT PRIMARY KEY, + session_id TEXT NOT NULL, + node_id TEXT NOT NULL, + FOREIGN KEY(tensor_id) REFERENCES tensors(id) ON DELETE CASCADE + ); + """ + ) + cursor.execute( + """--sql + CREATE UNIQUE INDEX IF NOT EXISTS idx_tensor_intermediates_tensor_id ON tensor_intermediates(tensor_id); + """ + ) + finally: + lock.release() + + +# `tensors_metadata` table: stores generated/transformed metadata for tensor + + +def create_tensors_metadata_table(cursor: sqlite3.Cursor, lock: threading.Lock): + try: + lock.acquire() + + cursor.execute( + """--sql + CREATE TABLE IF NOT EXISTS tensors_metadata ( + tensor_id TEXT PRIMARY KEY, + metadata TEXT, + FOREIGN KEY(tensor_id) REFERENCES tensors(id) ON DELETE CASCADE + ); + """ + ) + cursor.execute( + """--sql + CREATE UNIQUE INDEX IF NOT EXISTS idx_tensors_metadata_tensor_id ON tensors_metadata(tensor_id); + """ + ) + finally: + lock.release() diff --git a/invokeai/app/services/results.py b/invokeai/app/services/results.py new file mode 100644 index 0000000000..df7bf7bc6b --- /dev/null +++ b/invokeai/app/services/results.py @@ -0,0 +1,466 @@ +from enum import Enum + +from abc import ABC, abstractmethod +import json +import sqlite3 +from threading import Lock +from typing import Any, Union + +import networkx as nx + +from pydantic import BaseModel, Field, parse_obj_as, parse_raw_as +from invokeai.app.invocations.image import ImageOutput +from invokeai.app.services.graph import Edge, GraphExecutionState +from invokeai.app.invocations.latent import LatentsOutput +from invokeai.app.services.item_storage import PaginatedResults +from invokeai.app.util.misc import get_timestamp + + +class ResultType(str, Enum): + image_output = "image_output" + latents_output = "latents_output" + + +class Result(BaseModel): + """A session result""" + + id: str = Field(description="Result ID") + session_id: str = Field(description="Session ID") + node_id: str = Field(description="Node ID") + data: Union[LatentsOutput, ImageOutput] = Field(description="The result data") + + +class ResultWithSession(BaseModel): + """A result with its session""" + + result: Result = Field(description="The result") + session: GraphExecutionState = Field(description="The session") + + +# Create a directed graph +from typing import Any, TypedDict, Union +from networkx import DiGraph +import networkx as nx +import json + + +# We need to use a loose class for nodes to allow for graceful parsing - we cannot use the stricter +# model used by the system, because we may be a graph in an old format. We can, however, use the +# Edge model, because the edge format does not change. +class LooseGraph(BaseModel): + id: str + nodes: dict[str, dict[str, Any]] + edges: list[Edge] + + +# An intermediate type used during parsing +class NearestAncestor(TypedDict): + node_id: str + metadata: dict[str, Any] + + +# The ancestor types that contain the core metadata +ANCESTOR_TYPES = ['t2l', 'l2l'] + +# The core metadata parameters in the ancestor types +ANCESTOR_PARAMS = ['steps', 'model', 'cfg_scale', 'scheduler', 'strength'] + +# The core metadata parameters in the noise node +NOISE_FIELDS = ['seed', 'width', 'height'] + +# Find nearest t2l or l2l ancestor from a given l2i node +def find_nearest_ancestor(G: DiGraph, node_id: str) -> Union[NearestAncestor, None]: + """Returns metadata for the nearest ancestor of a given node. + + Parameters: + G (DiGraph): A directed graph. + node_id (str): The ID of the starting node. + + Returns: + NearestAncestor | None: An object with the ID and metadata of the nearest ancestor. + """ + + # Retrieve the node from the graph + node = G.nodes[node_id] + + # If the node type is one of the core metadata node types, gather necessary metadata and return + if node.get('type') in ANCESTOR_TYPES: + parsed_metadata = {param: val for param, val in node.items() if param in ANCESTOR_PARAMS} + return NearestAncestor(node_id=node_id, metadata=parsed_metadata) + + + # Else, look for the ancestor in the predecessor nodes + for predecessor in G.predecessors(node_id): + result = find_nearest_ancestor(G, predecessor) + if result: + return result + + # If there are no valid ancestors, return None + return None + + +def get_additional_metadata(graph: LooseGraph, node_id: str) -> Union[dict[str, Any], None]: + """Collects additional metadata from nodes connected to a given node. + + Parameters: + graph (LooseGraph): The graph. + node_id (str): The ID of the node. + + Returns: + dict | None: A dictionary containing additional metadata. + """ + + metadata = {} + + # Iterate over all edges in the graph + for edge in graph.edges: + dest_node_id = edge.destination.node_id + dest_field = edge.destination.field + source_node = graph.nodes[edge.source.node_id] + + # If the destination node ID matches the given node ID, gather necessary metadata + if dest_node_id == node_id: + # If the destination field is 'positive_conditioning', add the 'prompt' from the source node + if dest_field == 'positive_conditioning': + metadata['positive_conditioning'] = source_node.get('prompt') + # If the destination field is 'negative_conditioning', add the 'prompt' from the source node + if dest_field == 'negative_conditioning': + metadata['negative_conditioning'] = source_node.get('prompt') + # If the destination field is 'noise', add the core noise fields from the source node + if dest_field == 'noise': + for field in NOISE_FIELDS: + metadata[field] = source_node.get(field) + return metadata + +def build_core_metadata(graph_raw: str, node_id: str) -> Union[dict, None]: + """Builds the core metadata for a given node. + + Parameters: + graph_raw (str): The graph structure as a raw string. + node_id (str): The ID of the node. + + Returns: + dict | None: A dictionary containing core metadata. + """ + + # Create a directed graph to facilitate traversal + G = nx.DiGraph() + + # Convert the raw graph string into a JSON object + graph = parse_obj_as(LooseGraph, graph_raw) + + # Add nodes and edges to the graph + for node_id, node_data in graph.nodes.items(): + G.add_node(node_id, **node_data) + for edge in graph.edges: + G.add_edge(edge.source.node_id, edge.destination.node_id) + + # Find the nearest ancestor of the given node + ancestor = find_nearest_ancestor(G, node_id) + + # If no ancestor was found, return None + if ancestor is None: + return None + + metadata = ancestor['metadata'] + ancestor_id = ancestor['node_id'] + + # Get additional metadata related to the ancestor + addl_metadata = get_additional_metadata(graph, ancestor_id) + + # If additional metadata was found, add it to the main metadata + if addl_metadata is not None: + metadata.update(addl_metadata) + + return metadata + + + +class ResultsServiceABC(ABC): + """The Results service is responsible for retrieving results.""" + + @abstractmethod + def get( + self, result_id: str, result_type: ResultType + ) -> Union[ResultWithSession, None]: + pass + + @abstractmethod + def get_many( + self, result_type: ResultType, page: int = 0, per_page: int = 10 + ) -> PaginatedResults[ResultWithSession]: + pass + + @abstractmethod + def search( + self, query: str, page: int = 0, per_page: int = 10 + ) -> PaginatedResults[ResultWithSession]: + pass + + @abstractmethod + def handle_graph_execution_state_change(self, session: GraphExecutionState) -> None: + pass + + +class SqliteResultsService(ResultsServiceABC): + """SQLite implementation of the Results service.""" + + _filename: str + _conn: sqlite3.Connection + _cursor: sqlite3.Cursor + _lock: Lock + + def __init__(self, filename: str): + super().__init__() + + self._filename = filename + self._lock = Lock() + + self._conn = sqlite3.connect( + self._filename, check_same_thread=False + ) # TODO: figure out a better threading solution + self._cursor = self._conn.cursor() + + self._create_table() + + def _create_table(self): + try: + self._lock.acquire() + self._cursor.execute( + """--sql + CREATE TABLE IF NOT EXISTS results ( + id TEXT PRIMARY KEY, -- the result's name + result_type TEXT, -- `image_output` | `latents_output` + node_id TEXT, -- the node that produced this result + session_id TEXT, -- the session that produced this result + created_at INTEGER, -- the time at which this result was created + data TEXT -- the result itself + ); + """ + ) + self._cursor.execute( + """--sql + CREATE UNIQUE INDEX IF NOT EXISTS idx_result_id ON results(id); + """ + ) + finally: + self._lock.release() + + def _parse_joined_result(self, result_row: Any, column_names: list[str]): + result_raw = {} + session_raw = {} + + for idx, name in enumerate(column_names): + if name == "session": + session_raw = json.loads(result_row[idx]) + elif name == "data": + result_raw[name] = json.loads(result_row[idx]) + else: + result_raw[name] = result_row[idx] + + graph_raw = session_raw['execution_graph'] + + result = parse_obj_as(Result, result_raw) + session = parse_obj_as(GraphExecutionState, session_raw) + + m = build_core_metadata(graph_raw, result.node_id) + print(m) + + # g = session.execution_graph.nx_graph() + # ancestors = nx.dag.ancestors(g, result.node_id) + + # nodes = [session.execution_graph.get_node(result.node_id)] + # for ancestor in ancestors: + # nodes.append(session.execution_graph.get_node(ancestor)) + + # filtered_nodes = filter(lambda n: n.type in NODE_TYPE_ALLOWLIST, nodes) + # print(list(map(lambda n: n.dict(), filtered_nodes))) + # metadata = {} + # for node in nodes: + # if (node.type in ['txt2img', 'img2img',]) + # for field, value in node.dict().items(): + # if field not in ['type', 'id']: + # if field not in metadata: + # metadata[field] = value + + # print(ancestors) + # print(nodes) + # print(metadata) + + # for node in nodes: + # print(node.dict()) + + # print(nodes) + + return ResultWithSession( + result=result, + session=session, + ) + + def get( + self, result_id: str, result_type: ResultType + ) -> Union[ResultWithSession, None]: + """Retrieves a result by ID and type.""" + try: + self._lock.acquire() + self._cursor.execute( + """--sql + SELECT + results.id AS id, + results.result_type AS result_type, + results.node_id AS node_id, + results.session_id AS session_id, + results.data AS data, + graph_executions.item AS session + FROM results + JOIN graph_executions ON results.session_id = graph_executions.id + WHERE results.id = ? AND results.result_type = ? + """, + (result_id, result_type), + ) + + result_row = self._cursor.fetchone() + + if result_row is None: + return None + + column_names = list(map(lambda x: x[0], self._cursor.description)) + result_parsed = self._parse_joined_result(result_row, column_names) + finally: + self._lock.release() + + if not result_parsed: + return None + + return result_parsed + + def get_many( + self, + result_type: ResultType, + page: int = 0, + per_page: int = 10, + ) -> PaginatedResults[ResultWithSession]: + """Lists results of a given type.""" + try: + self._lock.acquire() + + self._cursor.execute( + f"""--sql + SELECT + results.id AS id, + results.result_type AS result_type, + results.node_id AS node_id, + results.session_id AS session_id, + results.data AS data, + graph_executions.item AS session + FROM results + JOIN graph_executions ON results.session_id = graph_executions.id + WHERE results.result_type = ? + LIMIT ? OFFSET ?; + """, + (result_type.value, per_page, page * per_page), + ) + + result_rows = self._cursor.fetchall() + column_names = list(map(lambda c: c[0], self._cursor.description)) + + result_parsed = [] + + for result_row in result_rows: + result_parsed.append( + self._parse_joined_result(result_row, column_names) + ) + + self._cursor.execute("""SELECT count(*) FROM results;""") + count = self._cursor.fetchone()[0] + finally: + self._lock.release() + + pageCount = int(count / per_page) + 1 + + return PaginatedResults[ResultWithSession]( + items=result_parsed, + page=page, + pages=pageCount, + per_page=per_page, + total=count, + ) + + def search( + self, + query: str, + page: int = 0, + per_page: int = 10, + ) -> PaginatedResults[ResultWithSession]: + """Finds results by query.""" + try: + self._lock.acquire() + self._cursor.execute( + """--sql + SELECT results.data, graph_executions.item + FROM results + JOIN graph_executions ON results.session_id = graph_executions.id + WHERE item LIKE ? + LIMIT ? OFFSET ?; + """, + (f"%{query}%", per_page, page * per_page), + ) + + result_rows = self._cursor.fetchall() + + items = list( + map( + lambda r: ResultWithSession( + result=parse_raw_as(Result, r[0]), + session=parse_raw_as(GraphExecutionState, r[1]), + ), + result_rows, + ) + ) + self._cursor.execute( + """--sql + SELECT count(*) FROM results WHERE item LIKE ?; + """, + (f"%{query}%",), + ) + count = self._cursor.fetchone()[0] + finally: + self._lock.release() + + pageCount = int(count / per_page) + 1 + + return PaginatedResults[ResultWithSession]( + items=items, page=page, pages=pageCount, per_page=per_page, total=count + ) + + def handle_graph_execution_state_change(self, session: GraphExecutionState) -> None: + """Updates the results table with the results from the session.""" + with self._conn as conn: + for node_id, result in session.results.items(): + # We'll only process 'image_output' or 'latents_output' + if result.type not in ["image_output", "latents_output"]: + continue + + # The id depends on the result type + if result.type == "image_output": + id = result.image.image_name + result_type = "image_output" + else: + id = result.latents.latents_name + result_type = "latents_output" + + # Insert the result into the results table, ignoring if it already exists + conn.execute( + """--sql + INSERT OR IGNORE INTO results (id, result_type, node_id, session_id, created_at, data) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + id, + result_type, + node_id, + session.id, + get_timestamp(), + result.json(), + ), + ) diff --git a/invokeai/app/services/urls.py b/invokeai/app/services/urls.py new file mode 100644 index 0000000000..16f8fc7494 --- /dev/null +++ b/invokeai/app/services/urls.py @@ -0,0 +1,32 @@ +import os +from abc import ABC, abstractmethod + +from invokeai.app.models.image import ImageType +from invokeai.app.util.thumbnails import get_thumbnail_name + + +class UrlServiceBase(ABC): + """Responsible for building URLs for resources (eg images or tensors)""" + + @abstractmethod + def get_image_url(self, image_type: ImageType, image_name: str) -> str: + """Gets the URL for an image""" + pass + + @abstractmethod + def get_thumbnail_url(self, image_type: ImageType, image_name: str) -> str: + """Gets the URL for an image's thumbnail""" + pass + + +class LocalUrlService(UrlServiceBase): + def __init__(self, base_url: str = "api/v1"): + self._base_url = base_url + + def get_image_url(self, image_type: ImageType, image_name: str) -> str: + image_basename = os.path.basename(image_name) + return f"{self._base_url}/images/{image_type.value}/{image_basename}" + + def get_thumbnail_url(self, image_type: ImageType, image_name: str) -> str: + thumbnail_basename = get_thumbnail_name(os.path.basename(image_name)) + return f"{self._base_url}/images/{image_type.value}/thumbnails/{thumbnail_basename}" diff --git a/invokeai/app/services/util/create_enum_table.py b/invokeai/app/services/util/create_enum_table.py new file mode 100644 index 0000000000..03cbfd6e90 --- /dev/null +++ b/invokeai/app/services/util/create_enum_table.py @@ -0,0 +1,39 @@ +from enum import Enum +import sqlite3 +from typing import Type + + +def create_sql_values_string_from_string_enum(enum: Type[Enum]): + """ + Creates a string of the form "('value1'), ('value2'), ..., ('valueN')" from a StrEnum. + """ + + delimiter = ", " + values = [f"('{e.value}')" for e in enum] + return delimiter.join(values) + + +def create_enum_table( + enum: Type[Enum], + table_name: str, + primary_key_name: str, + cursor: sqlite3.Cursor, +): + """ + Creates and populates a table to be used as a functional enum. + """ + + values_string = create_sql_values_string_from_string_enum(enum) + + cursor.execute( + f"""--sql + CREATE TABLE IF NOT EXISTS {table_name} ( + {primary_key_name} TEXT PRIMARY KEY + ); + """ + ) + cursor.execute( + f"""--sql + INSERT OR IGNORE INTO {table_name} ({primary_key_name}) VALUES {values_string}; + """ + ) diff --git a/invokeai/app/services/util/deserialize_image_record.py b/invokeai/app/services/util/deserialize_image_record.py new file mode 100644 index 0000000000..52014b78c5 --- /dev/null +++ b/invokeai/app/services/util/deserialize_image_record.py @@ -0,0 +1,33 @@ +from invokeai.app.models.metadata import ( + GeneratedImageOrLatentsMetadata, + UploadedImageOrLatentsMetadata, +) +from invokeai.app.models.image import ImageCategory, ImageType +from invokeai.app.services.models.image_record import ImageRecord +from invokeai.app.util.misc import get_iso_timestamp + + +def deserialize_image_record(image: dict) -> ImageRecord: + """Deserializes an image record.""" + + # All values *should* be present, except `session_id` and `node_id`, but provide some defaults just in case + + image_type = ImageType(image.get("image_type", ImageType.RESULT.value)) + raw_metadata = image.get("metadata", {}) + + if image_type == ImageType.UPLOAD: + metadata = UploadedImageOrLatentsMetadata.parse_obj(raw_metadata) + else: + metadata = GeneratedImageOrLatentsMetadata.parse_obj(raw_metadata) + + return ImageRecord( + image_name=image.get("id", "unknown"), + session_id=image.get("session_id", None), + node_id=image.get("node_id", None), + metadata=metadata, + image_type=image_type, + image_category=ImageCategory( + image.get("image_category", ImageCategory.IMAGE.value) + ), + created_at=image.get("created_at", get_iso_timestamp()), + ) diff --git a/invokeai/app/util/enum.py b/invokeai/app/util/enum.py new file mode 100644 index 0000000000..5bba5712c5 --- /dev/null +++ b/invokeai/app/util/enum.py @@ -0,0 +1,12 @@ +from enum import EnumMeta + + +class MetaEnum(EnumMeta): + """Metaclass to support `in` syntax value checking in String Enums""" + + def __contains__(cls, item): + try: + cls(item) + except ValueError: + return False + return True diff --git a/invokeai/app/util/misc.py b/invokeai/app/util/misc.py index c3d091b653..7c674674e2 100644 --- a/invokeai/app/util/misc.py +++ b/invokeai/app/util/misc.py @@ -6,6 +6,14 @@ def get_timestamp(): return int(datetime.datetime.now(datetime.timezone.utc).timestamp()) +def get_iso_timestamp() -> str: + return datetime.datetime.utcnow().isoformat() + + +def get_datetime_from_iso_timestamp(iso_timestamp: str) -> datetime.datetime: + return datetime.datetime.fromisoformat(iso_timestamp) + + SEED_MAX = np.iinfo(np.int32).max diff --git a/invokeai/frontend/web/src/services/api/models/ImageOutput.ts b/invokeai/frontend/web/src/services/api/models/ImageOutput.ts index 09b842de26..d7db0c11de 100644 --- a/invokeai/frontend/web/src/services/api/models/ImageOutput.ts +++ b/invokeai/frontend/web/src/services/api/models/ImageOutput.ts @@ -8,7 +8,7 @@ import type { ImageField } from './ImageField'; * Base class for invocations that output an image */ export type ImageOutput = { - type: 'image'; + type: 'image_output'; /** * The output image */ diff --git a/invokeai/frontend/web/src/services/api/models/RandomIntInvocation.ts b/invokeai/frontend/web/src/services/api/models/RandomIntInvocation.ts index af7cf85666..0a5220c31d 100644 --- a/invokeai/frontend/web/src/services/api/models/RandomIntInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/RandomIntInvocation.ts @@ -11,5 +11,13 @@ export type RandomIntInvocation = { */ id: string; type?: 'rand_int'; + /** + * The inclusive low value + */ + low?: number; + /** + * The exclusive high value + */ + high?: number; }; diff --git a/invokeai/frontend/web/src/services/api/schemas/$RandomIntInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$RandomIntInvocation.ts index e5b0387d5a..e70192fae5 100644 --- a/invokeai/frontend/web/src/services/api/schemas/$RandomIntInvocation.ts +++ b/invokeai/frontend/web/src/services/api/schemas/$RandomIntInvocation.ts @@ -12,5 +12,13 @@ export const $RandomIntInvocation = { type: { type: 'Enum', }, + low: { + type: 'number', + description: `The inclusive low value`, + }, + high: { + type: 'number', + description: `The exclusive high value`, + }, }, } as const; diff --git a/invokeai/frontend/web/src/services/types/guards.ts b/invokeai/frontend/web/src/services/types/guards.ts index 72cf1108fb..5065290220 100644 --- a/invokeai/frontend/web/src/services/types/guards.ts +++ b/invokeai/frontend/web/src/services/types/guards.ts @@ -10,11 +10,16 @@ import { CollectInvocationOutput, ImageType, ImageField, + LatentsOutput, } from 'services/api'; export const isImageOutput = ( output: GraphExecutionState['results'][string] -): output is ImageOutput => output.type === 'image'; +): output is ImageOutput => output.type === 'image_output'; + +export const isLatentsOutput = ( + output: GraphExecutionState['results'][string] +): output is LatentsOutput => output.type === 'latents_output'; export const isMaskOutput = ( output: GraphExecutionState['results'][string] From 33d199c00751288b361325fc444d6a295a9b98e6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 21 May 2023 15:47:29 +1000 Subject: [PATCH 07/72] feat(nodes): image records router --- .../{image_resources.py => image_records.py} | 4 ++-- invokeai/app/api/routers/images.py | 11 ++++++----- invokeai/app/api_app.py | 4 ++-- invokeai/app/services/image_storage.py | 14 ++++++++++++++ 4 files changed, 24 insertions(+), 9 deletions(-) rename invokeai/app/api/routers/{image_resources.py => image_records.py} (96%) diff --git a/invokeai/app/api/routers/image_resources.py b/invokeai/app/api/routers/image_records.py similarity index 96% rename from invokeai/app/api/routers/image_resources.py rename to invokeai/app/api/routers/image_records.py index 56fcdcb2d1..e860bee8c7 100644 --- a/invokeai/app/api/routers/image_resources.py +++ b/invokeai/app/api/routers/image_records.py @@ -11,7 +11,7 @@ from invokeai.app.services.item_storage import PaginatedResults from ..dependencies import ApiDependencies -image_records_router = APIRouter(prefix="/v1/records/images", tags=["records"]) +image_records_router = APIRouter(prefix="/v1/images", tags=["images", "records"]) @image_records_router.get("/{image_type}/{image_name}", operation_id="get_image_record") @@ -57,7 +57,7 @@ async def list_image_records( @image_records_router.delete("/{image_type}/{image_name}", operation_id="delete_image") async def delete_image_record( - image_type: ImageType = Query(description="The type of image records to get"), + image_type: ImageType = Query(description="The type of image to delete"), image_name: str = Path(description="The name of the image to delete"), ) -> None: """Deletes an image record""" diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 41ba00ef7a..a42b2a1e63 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -19,7 +19,7 @@ from invokeai.app.services.item_storage import PaginatedResults from ..dependencies import ApiDependencies -images_router = APIRouter(prefix="/v1/images", tags=["images"]) +images_router = APIRouter(prefix="/v1/files/images", tags=["images", "files"]) # @images_router.get("/{image_type}/{image_name}", operation_id="get_image") @@ -38,15 +38,16 @@ images_router = APIRouter(prefix="/v1/images", tags=["images"]) # else: # raise HTTPException(status_code=404) -@images_router.get("/{image_type}/{image_id}", operation_id="get_image") + +@images_router.get("/{image_type}/{image_name}", operation_id="get_image") async def get_image( image_type: ImageType = Path(description="The type of the image to get"), - image_id: str = Path(description="The id of the image to get"), + image_name: str = Path(description="The id of the image to get"), ) -> FileResponse: """Gets an image""" path = ApiDependencies.invoker.services.images.get_path( - image_type=image_type, image_id=image_id + image_type=image_type, image_name=image_name ) if ApiDependencies.invoker.services.images.validate_path(path): @@ -77,7 +78,7 @@ async def get_thumbnail( """Gets a thumbnail""" path = ApiDependencies.invoker.services.images.get_path( - image_type=image_type, image_id=thumbnail_id, is_thumbnail=True + image_type=image_type, image_name=thumbnail_id, is_thumbnail=True ) if ApiDependencies.invoker.services.images.validate_path(path): diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index a67f36edd3..a3b5fbc63d 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -15,7 +15,7 @@ from fastapi_events.middleware import EventHandlerASGIMiddleware from pydantic.schema import schema from .api.dependencies import ApiDependencies -from .api.routers import image_resources, images, sessions, models +from .api.routers import image_records, images, sessions, models from .api.sockets import SocketIO from .invocations.baseinvocation import BaseInvocation from .services.config import InvokeAIAppConfig @@ -75,7 +75,7 @@ app.include_router(images.images_router, prefix="/api") app.include_router(models.models_router, prefix="/api") -app.include_router(image_resources.image_resources_router, prefix="/api") +app.include_router(image_records.image_records_router, prefix="/api") # Build a custom OpenAPI to include all outputs diff --git a/invokeai/app/services/image_storage.py b/invokeai/app/services/image_storage.py index 7610ac62bf..1fda2c61db 100644 --- a/invokeai/app/services/image_storage.py +++ b/invokeai/app/services/image_storage.py @@ -74,6 +74,20 @@ class ImageStorageBase(ABC): ) -> str: """Gets the external URI to an image or its thumbnail.""" pass + + @abstractmethod + def get_image_location( + self, image_type: ImageType, image_name: str + ) -> str: + """Gets the location of an image.""" + pass + + @abstractmethod + def get_thumbnail_location( + self, image_type: ImageType, image_name: str + ) -> str: + """Gets the location of an image's thumbnail.""" + pass # TODO: make this a bit more flexible for e.g. cloud storage @abstractmethod From d4aa79acd738afcc29e918884d120454f1447f3b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 21 May 2023 17:26:46 +1000 Subject: [PATCH 08/72] fix(nodes): use `save` instead of `set` `set` is a python builtin --- invokeai/app/invocations/compel.py | 2 +- invokeai/app/invocations/latent.py | 14 +++++++------- invokeai/app/services/latent_storage.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/invokeai/app/invocations/compel.py b/invokeai/app/invocations/compel.py index f0db3e6d9e..076ce81021 100644 --- a/invokeai/app/invocations/compel.py +++ b/invokeai/app/invocations/compel.py @@ -118,7 +118,7 @@ class CompelInvocation(BaseInvocation): conditioning_name = f"{context.graph_execution_state_id}_{self.id}_conditioning" # TODO: hacky but works ;D maybe rename latents somehow? - context.services.latents.set(conditioning_name, (c, ec)) + context.services.latents.save(conditioning_name, (c, ec)) return CompelOutput( conditioning=ConditioningField( diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index ac7139d031..64993e011a 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -20,7 +20,7 @@ from ...backend.stable_diffusion.diffusers_pipeline import ConditioningData, Sta from ...backend.stable_diffusion.schedulers import SCHEDULER_MAP from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationContext, InvocationConfig import numpy as np -from ..services.image_storage import ImageType +from ..services.image_file_storage import ImageType from .baseinvocation import BaseInvocation, InvocationContext from .image import ImageField, ImageOutput, build_image_output from .compel import ConditioningField @@ -144,7 +144,7 @@ class NoiseInvocation(BaseInvocation): noise = get_noise(self.width, self.height, device, self.seed) name = f'{context.graph_execution_state_id}__{self.id}' - context.services.latents.set(name, noise) + context.services.latents.save(name, noise) return build_noise_output(latents_name=name, latents=noise) @@ -260,7 +260,7 @@ class TextToLatentsInvocation(BaseInvocation): torch.cuda.empty_cache() name = f'{context.graph_execution_state_id}__{self.id}' - context.services.latents.set(name, result_latents) + context.services.latents.save(name, result_latents) return build_latents_output(latents_name=name, latents=result_latents) @@ -319,7 +319,7 @@ class LatentsToLatentsInvocation(TextToLatentsInvocation): torch.cuda.empty_cache() name = f'{context.graph_execution_state_id}__{self.id}' - context.services.latents.set(name, result_latents) + context.services.latents.save(name, result_latents) return build_latents_output(latents_name=name, latents=result_latents) @@ -404,7 +404,7 @@ class ResizeLatentsInvocation(BaseInvocation): torch.cuda.empty_cache() name = f"{context.graph_execution_state_id}__{self.id}" - context.services.latents.set(name, resized_latents) + context.services.latents.save(name, resized_latents) return build_latents_output(latents_name=name, latents=resized_latents) @@ -434,7 +434,7 @@ class ScaleLatentsInvocation(BaseInvocation): torch.cuda.empty_cache() name = f"{context.graph_execution_state_id}__{self.id}" - context.services.latents.set(name, resized_latents) + context.services.latents.save(name, resized_latents) return build_latents_output(latents_name=name, latents=resized_latents) @@ -478,5 +478,5 @@ class ImageToLatentsInvocation(BaseInvocation): ) name = f"{context.graph_execution_state_id}__{self.id}" - context.services.latents.set(name, latents) + context.services.latents.save(name, latents) return build_latents_output(latents_name=name, latents=latents) diff --git a/invokeai/app/services/latent_storage.py b/invokeai/app/services/latent_storage.py index 0184692e05..271bd17c1b 100644 --- a/invokeai/app/services/latent_storage.py +++ b/invokeai/app/services/latent_storage.py @@ -16,7 +16,7 @@ class LatentsStorageBase(ABC): pass @abstractmethod - def set(self, name: str, data: torch.Tensor) -> None: + def save(self, name: str, data: torch.Tensor) -> None: pass @abstractmethod @@ -47,7 +47,7 @@ class ForwardCacheLatentsStorage(LatentsStorageBase): self.__set_cache(name, latent) return latent - def set(self, name: str, data: torch.Tensor) -> None: + def save(self, name: str, data: torch.Tensor) -> None: self.__underlying_storage.set(name, data) self.__set_cache(name, data) @@ -80,7 +80,7 @@ class DiskLatentsStorage(LatentsStorageBase): latent_path = self.get_path(name) return torch.load(latent_path) - def set(self, name: str, data: torch.Tensor) -> None: + def save(self, name: str, data: torch.Tensor) -> None: latent_path = self.get_path(name) torch.save(data, latent_path) From 1b75d899ae8858981c505458a56a5caf7edefc8d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 21 May 2023 20:05:33 +1000 Subject: [PATCH 09/72] feat(nodes): wip image storage implementation --- invokeai/app/api/dependencies.py | 22 ++- invokeai/app/api/routers/image_files.py | 165 +++++++++++++++++ invokeai/app/api/routers/image_records.py | 27 ++- invokeai/app/api/routers/images.py | 174 +++++------------- invokeai/app/api_app.py | 4 +- invokeai/app/cli_app.py | 4 +- ...image_storage.py => image_file_storage.py} | 36 ++-- .../{image_db.py => image_record_storage.py} | 8 +- invokeai/app/services/images.py | 159 +++++++++------- invokeai/app/services/invocation_services.py | 13 +- invokeai/app/services/latent_storage.py | 2 +- invokeai/app/services/metadata.py | 8 +- invokeai/app/services/models/image_record.py | 29 ++- 13 files changed, 383 insertions(+), 268 deletions(-) create mode 100644 invokeai/app/api/routers/image_files.py rename invokeai/app/services/{image_storage.py => image_file_storage.py} (93%) rename invokeai/app/services/{image_db.py => image_record_storage.py} (97%) diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index 7494d24324..e0fe960ea0 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -2,9 +2,8 @@ import os from types import ModuleType -from invokeai.app.services.database.images.sqlite_images_db_service import ( - SqliteImageDb, -) +from invokeai.app.services.image_record_storage import SqliteImageRecordStorage +from invokeai.app.services.images import ImageService from invokeai.app.services.urls import LocalUrlService import invokeai.backend.util.logging as logger @@ -14,7 +13,7 @@ from ..services.latent_storage import DiskLatentsStorage, ForwardCacheLatentsSto from ..services.model_manager_initializer import get_model_manager from ..services.restoration_services import RestorationServices from ..services.graph import GraphExecutionState, LibraryGraph -from ..services.image_storage import DiskImageStorage +from ..services.image_file_storage import DiskImageFileStorage from ..services.invocation_queue import MemoryInvocationQueue from ..services.invocation_services import InvocationServices from ..services.invoker import Invoker @@ -63,7 +62,9 @@ class ApiDependencies: urls = LocalUrlService() - images = DiskImageStorage(f"{output_folder}/images", metadata_service=metadata) + image_file_storage = DiskImageFileStorage( + f"{output_folder}/images", metadata_service=metadata + ) # TODO: build a file/path manager? db_location = os.path.join(output_folder, "invokeai.db") @@ -72,7 +73,14 @@ class ApiDependencies: filename=db_location, table_name="graph_executions" ) - images_db = SqliteImageDb(filename=db_location) + image_record_storage = SqliteImageRecordStorage(db_location) + + images_new = ImageService( + image_record_storage=image_record_storage, + image_file_storage=image_file_storage, + metadata=metadata, + url=urls, + ) # register event handler to update the `results` table when a graph execution state is inserted or updated # graph_execution_manager.on_changed(results.handle_graph_execution_state_change) @@ -82,8 +90,8 @@ class ApiDependencies: events=events, latents=latents, images=images, + images_new=images_new, metadata=metadata, - images_db=images_db, urls=urls, queue=MemoryInvocationQueue(), graph_library=SqliteItemStorage[LibraryGraph]( diff --git a/invokeai/app/api/routers/image_files.py b/invokeai/app/api/routers/image_files.py new file mode 100644 index 0000000000..a42b2a1e63 --- /dev/null +++ b/invokeai/app/api/routers/image_files.py @@ -0,0 +1,165 @@ +# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) +import io +from datetime import datetime, timezone +import json +import os +from typing import Any +import uuid + +from fastapi import Body, HTTPException, Path, Query, Request, UploadFile +from fastapi.responses import FileResponse, Response +from fastapi.routing import APIRouter +from PIL import Image +from invokeai.app.api.models.images import ( + ImageResponse, + ImageResponseMetadata, +) +from invokeai.app.models.image import ImageType +from invokeai.app.services.item_storage import PaginatedResults + +from ..dependencies import ApiDependencies + +images_router = APIRouter(prefix="/v1/files/images", tags=["images", "files"]) + + +# @images_router.get("/{image_type}/{image_name}", operation_id="get_image") +# async def get_image( +# image_type: ImageType = Path(description="The type of image to get"), +# image_name: str = Path(description="The name of the image to get"), +# ) -> FileResponse: +# """Gets an image""" + +# path = ApiDependencies.invoker.services.images.get_path( +# image_type=image_type, image_name=image_name +# ) + +# if ApiDependencies.invoker.services.images.validate_path(path): +# return FileResponse(path) +# else: +# raise HTTPException(status_code=404) + + +@images_router.get("/{image_type}/{image_name}", operation_id="get_image") +async def get_image( + image_type: ImageType = Path(description="The type of the image to get"), + image_name: str = Path(description="The id of the image to get"), +) -> FileResponse: + """Gets an image""" + + path = ApiDependencies.invoker.services.images.get_path( + image_type=image_type, image_name=image_name + ) + + if ApiDependencies.invoker.services.images.validate_path(path): + return FileResponse(path) + else: + raise HTTPException(status_code=404) + + +@images_router.delete("/{image_type}/{image_name}", operation_id="delete_image") +async def delete_image( + image_type: ImageType = Path(description="The type of the image to delete"), + image_name: str = Path(description="The name of the image to delete"), +) -> None: + """Deletes an image and its thumbnail""" + + ApiDependencies.invoker.services.images.delete( + image_type=image_type, image_name=image_name + ) + + +@images_router.get( + "/{image_type}/thumbnails/{thumbnail_id}", operation_id="get_thumbnail" +) +async def get_thumbnail( + image_type: ImageType = Path(description="The type of the thumbnail to get"), + thumbnail_id: str = Path(description="The id of the thumbnail to get"), +) -> FileResponse | Response: + """Gets a thumbnail""" + + path = ApiDependencies.invoker.services.images.get_path( + image_type=image_type, image_name=thumbnail_id, is_thumbnail=True + ) + + if ApiDependencies.invoker.services.images.validate_path(path): + return FileResponse(path) + else: + raise HTTPException(status_code=404) + + +@images_router.post( + "/uploads/", + operation_id="upload_image", + responses={ + 201: { + "description": "The image was uploaded successfully", + "model": ImageResponse, + }, + 415: {"description": "Image upload failed"}, + }, + status_code=201, +) +async def upload_image( + file: UploadFile, image_type: ImageType, request: Request, response: Response +) -> ImageResponse: + if not file.content_type.startswith("image"): + raise HTTPException(status_code=415, detail="Not an image") + + contents = await file.read() + + try: + img = Image.open(io.BytesIO(contents)) + except: + # Error opening the image + raise HTTPException(status_code=415, detail="Failed to read image") + + filename = f"{uuid.uuid4()}_{str(int(datetime.now(timezone.utc).timestamp()))}.png" + + saved_image = ApiDependencies.invoker.services.images.save( + image_type, filename, img + ) + + invokeai_metadata = ApiDependencies.invoker.services.metadata.get_metadata(img) + + image_url = ApiDependencies.invoker.services.images.get_uri( + image_type, saved_image.image_name + ) + + thumbnail_url = ApiDependencies.invoker.services.images.get_uri( + image_type, saved_image.image_name, True + ) + + res = ImageResponse( + image_type=image_type, + image_name=saved_image.image_name, + image_url=image_url, + thumbnail_url=thumbnail_url, + metadata=ImageResponseMetadata( + created=saved_image.created, + width=img.width, + height=img.height, + invokeai=invokeai_metadata, + ), + ) + + response.status_code = 201 + response.headers["Location"] = image_url + + return res + + +@images_router.get( + "/", + operation_id="list_images", + responses={200: {"model": PaginatedResults[ImageResponse]}}, +) +async def list_images( + image_type: ImageType = Query( + default=ImageType.RESULT, description="The type of images to get" + ), + page: int = Query(default=0, description="The page of images to get"), + per_page: int = Query(default=10, description="The number of images per page"), +) -> PaginatedResults[ImageResponse]: + """Gets a list of images""" + result = ApiDependencies.invoker.services.images.list(image_type, page, per_page) + return result diff --git a/invokeai/app/api/routers/image_records.py b/invokeai/app/api/routers/image_records.py index e860bee8c7..5dccccae41 100644 --- a/invokeai/app/api/routers/image_records.py +++ b/invokeai/app/api/routers/image_records.py @@ -4,28 +4,28 @@ from invokeai.app.models.image import ( ImageCategory, ImageType, ) -from invokeai.app.services.image_db import ImageRecordServiceBase -from invokeai.app.services.image_storage import ImageStorageBase -from invokeai.app.services.models.image_record import ImageRecord from invokeai.app.services.item_storage import PaginatedResults +from invokeai.app.services.models.image_record import ImageDTO from ..dependencies import ApiDependencies -image_records_router = APIRouter(prefix="/v1/images", tags=["images", "records"]) +image_records_router = APIRouter( + prefix="/v1/images/records", tags=["images", "records"] +) @image_records_router.get("/{image_type}/{image_name}", operation_id="get_image_record") async def get_image_record( image_type: ImageType = Path(description="The type of the image record to get"), image_name: str = Path(description="The id of the image record to get"), -) -> ImageRecord: +) -> ImageDTO: """Gets an image record by id""" try: - return ApiDependencies.invoker.services.images_new.get_record( + return ApiDependencies.invoker.services.images_new.get_dto( image_type=image_type, image_name=image_name ) - except ImageRecordServiceBase.ImageRecordNotFoundException: + except Exception as e: raise HTTPException(status_code=404) @@ -42,17 +42,17 @@ async def list_image_records( per_page: int = Query( default=10, description="The number of image records per page" ), -) -> PaginatedResults[ImageRecord]: +) -> PaginatedResults[ImageDTO]: """Gets a list of image records by type and category""" - images = ApiDependencies.invoker.services.images_new.get_many( + image_dtos = ApiDependencies.invoker.services.images_new.get_many( image_type=image_type, image_category=image_category, page=page, per_page=per_page, ) - return images + return image_dtos @image_records_router.delete("/{image_type}/{image_name}", operation_id="delete_image") @@ -66,9 +66,6 @@ async def delete_image_record( ApiDependencies.invoker.services.images_new.delete( image_type=image_type, image_name=image_name ) - except ImageStorageBase.ImageFileDeleteException: - # TODO: log this - pass - except ImageRecordServiceBase.ImageRecordDeleteException: - # TODO: log this + except Exception as e: + # TODO: Does this need any exception handling at all? pass diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index a42b2a1e63..c38d99c74f 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -1,107 +1,39 @@ -# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) import io -from datetime import datetime, timezone -import json -import os -from typing import Any import uuid - -from fastapi import Body, HTTPException, Path, Query, Request, UploadFile -from fastapi.responses import FileResponse, Response +from fastapi import HTTPException, Path, Query, Request, Response, UploadFile from fastapi.routing import APIRouter from PIL import Image -from invokeai.app.api.models.images import ( - ImageResponse, - ImageResponseMetadata, +from invokeai.app.models.image import ( + ImageCategory, + ImageType, ) -from invokeai.app.models.image import ImageType +from invokeai.app.services.image_record_storage import ImageRecordStorageBase +from invokeai.app.services.image_file_storage import ImageFileStorageBase +from invokeai.app.services.models.image_record import ImageRecord from invokeai.app.services.item_storage import PaginatedResults from ..dependencies import ApiDependencies -images_router = APIRouter(prefix="/v1/files/images", tags=["images", "files"]) - - -# @images_router.get("/{image_type}/{image_name}", operation_id="get_image") -# async def get_image( -# image_type: ImageType = Path(description="The type of image to get"), -# image_name: str = Path(description="The name of the image to get"), -# ) -> FileResponse: -# """Gets an image""" - -# path = ApiDependencies.invoker.services.images.get_path( -# image_type=image_type, image_name=image_name -# ) - -# if ApiDependencies.invoker.services.images.validate_path(path): -# return FileResponse(path) -# else: -# raise HTTPException(status_code=404) - - -@images_router.get("/{image_type}/{image_name}", operation_id="get_image") -async def get_image( - image_type: ImageType = Path(description="The type of the image to get"), - image_name: str = Path(description="The id of the image to get"), -) -> FileResponse: - """Gets an image""" - - path = ApiDependencies.invoker.services.images.get_path( - image_type=image_type, image_name=image_name - ) - - if ApiDependencies.invoker.services.images.validate_path(path): - return FileResponse(path) - else: - raise HTTPException(status_code=404) - - -@images_router.delete("/{image_type}/{image_name}", operation_id="delete_image") -async def delete_image( - image_type: ImageType = Path(description="The type of the image to delete"), - image_name: str = Path(description="The name of the image to delete"), -) -> None: - """Deletes an image and its thumbnail""" - - ApiDependencies.invoker.services.images.delete( - image_type=image_type, image_name=image_name - ) - - -@images_router.get( - "/{image_type}/thumbnails/{thumbnail_id}", operation_id="get_thumbnail" -) -async def get_thumbnail( - image_type: ImageType = Path(description="The type of the thumbnail to get"), - thumbnail_id: str = Path(description="The id of the thumbnail to get"), -) -> FileResponse | Response: - """Gets a thumbnail""" - - path = ApiDependencies.invoker.services.images.get_path( - image_type=image_type, image_name=thumbnail_id, is_thumbnail=True - ) - - if ApiDependencies.invoker.services.images.validate_path(path): - return FileResponse(path) - else: - raise HTTPException(status_code=404) +images_router = APIRouter(prefix="/v1/images", tags=["images"]) @images_router.post( - "/uploads/", + "/", operation_id="upload_image", responses={ - 201: { - "description": "The image was uploaded successfully", - "model": ImageResponse, - }, + 201: {"description": "The image was uploaded successfully"}, 415: {"description": "Image upload failed"}, }, status_code=201, ) async def upload_image( - file: UploadFile, image_type: ImageType, request: Request, response: Response -) -> ImageResponse: + file: UploadFile, + image_type: ImageType, + request: Request, + response: Response, + image_category: ImageCategory = ImageCategory.IMAGE, +) -> ImageRecord: + """Uploads an image""" if not file.content_type.startswith("image"): raise HTTPException(status_code=415, detail="Not an image") @@ -113,53 +45,33 @@ async def upload_image( # Error opening the image raise HTTPException(status_code=415, detail="Failed to read image") - filename = f"{uuid.uuid4()}_{str(int(datetime.now(timezone.utc).timestamp()))}.png" + try: + image_record = ApiDependencies.invoker.services.images_new.create( + image=img, + image_type=image_type, + image_category=image_category, + ) - saved_image = ApiDependencies.invoker.services.images.save( - image_type, filename, img - ) + response.status_code = 201 + response.headers["Location"] = image_record.image_url - invokeai_metadata = ApiDependencies.invoker.services.metadata.get_metadata(img) - - image_url = ApiDependencies.invoker.services.images.get_uri( - image_type, saved_image.image_name - ) - - thumbnail_url = ApiDependencies.invoker.services.images.get_uri( - image_type, saved_image.image_name, True - ) - - res = ImageResponse( - image_type=image_type, - image_name=saved_image.image_name, - image_url=image_url, - thumbnail_url=thumbnail_url, - metadata=ImageResponseMetadata( - created=saved_image.created, - width=img.width, - height=img.height, - invokeai=invokeai_metadata, - ), - ) - - response.status_code = 201 - response.headers["Location"] = image_url - - return res + return image_record + except Exception as e: + raise HTTPException(status_code=500) -@images_router.get( - "/", - operation_id="list_images", - responses={200: {"model": PaginatedResults[ImageResponse]}}, -) -async def list_images( - image_type: ImageType = Query( - default=ImageType.RESULT, description="The type of images to get" - ), - page: int = Query(default=0, description="The page of images to get"), - per_page: int = Query(default=10, description="The number of images per page"), -) -> PaginatedResults[ImageResponse]: - """Gets a list of images""" - result = ApiDependencies.invoker.services.images.list(image_type, page, per_page) - return result + +@images_router.delete("/{image_type}/{image_name}", operation_id="delete_image") +async def delete_image_record( + image_type: ImageType = Query(description="The type of image to delete"), + image_name: str = Path(description="The name of the image to delete"), +) -> None: + """Deletes an image record""" + + try: + ApiDependencies.invoker.services.images_new.delete( + image_type=image_type, image_name=image_name + ) + except Exception as e: + # TODO: Does this need any exception handling at all? + pass diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index a3b5fbc63d..9720474109 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -15,7 +15,7 @@ from fastapi_events.middleware import EventHandlerASGIMiddleware from pydantic.schema import schema from .api.dependencies import ApiDependencies -from .api.routers import image_records, images, sessions, models +from .api.routers import image_files, image_records, sessions, models from .api.sockets import SocketIO from .invocations.baseinvocation import BaseInvocation from .services.config import InvokeAIAppConfig @@ -71,7 +71,7 @@ async def shutdown_event(): app.include_router(sessions.session_router, prefix="/api") -app.include_router(images.images_router, prefix="/api") +app.include_router(image_files.images_router, prefix="/api") app.include_router(models.models_router, prefix="/api") diff --git a/invokeai/app/cli_app.py b/invokeai/app/cli_app.py index 9f2705d800..073a8f569b 100644 --- a/invokeai/app/cli_app.py +++ b/invokeai/app/cli_app.py @@ -28,7 +28,7 @@ from .services.model_manager_initializer import get_model_manager from .services.restoration_services import RestorationServices from .services.graph import Edge, EdgeConnection, GraphExecutionState, GraphInvocation, LibraryGraph, are_connection_types_compatible from .services.default_graphs import default_text_to_image_graph_id -from .services.image_storage import DiskImageStorage +from .services.image_file_storage import DiskImageFileStorage from .services.invocation_queue import MemoryInvocationQueue from .services.invocation_services import InvocationServices from .services.invoker import Invoker @@ -215,7 +215,7 @@ def invoke_cli(): model_manager=model_manager, events=events, latents = ForwardCacheLatentsStorage(DiskLatentsStorage(f'{output_folder}/latents')), - images=DiskImageStorage(f'{output_folder}/images', metadata_service=metadata), + images=DiskImageFileStorage(f'{output_folder}/images', metadata_service=metadata), metadata=metadata, queue=MemoryInvocationQueue(), graph_library=SqliteItemStorage[LibraryGraph]( diff --git a/invokeai/app/services/image_storage.py b/invokeai/app/services/image_file_storage.py similarity index 93% rename from invokeai/app/services/image_storage.py rename to invokeai/app/services/image_file_storage.py index 1fda2c61db..ff3640011a 100644 --- a/invokeai/app/services/image_storage.py +++ b/invokeai/app/services/image_file_storage.py @@ -26,8 +26,8 @@ from invokeai.app.util.misc import get_timestamp from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail -class ImageStorageBase(ABC): - """Low-level service responsible for storing and retrieving images.""" +class ImageFileStorageBase(ABC): + """Low-level service responsible for storing and retrieving image files.""" class ImageFileNotFoundException(Exception): """Raised when an image file is not found in storage.""" @@ -75,19 +75,19 @@ class ImageStorageBase(ABC): """Gets the external URI to an image or its thumbnail.""" pass - @abstractmethod - def get_image_location( - self, image_type: ImageType, image_name: str - ) -> str: - """Gets the location of an image.""" - pass + # @abstractmethod + # def get_image_location( + # self, image_type: ImageType, image_name: str + # ) -> str: + # """Gets the location of an image.""" + # pass - @abstractmethod - def get_thumbnail_location( - self, image_type: ImageType, image_name: str - ) -> str: - """Gets the location of an image's thumbnail.""" - pass + # @abstractmethod + # def get_thumbnail_location( + # self, image_type: ImageType, image_name: str + # ) -> str: + # """Gets the location of an image's thumbnail.""" + # pass # TODO: make this a bit more flexible for e.g. cloud storage @abstractmethod @@ -116,7 +116,7 @@ class ImageStorageBase(ABC): return f"{context_id}_{node_id}_{str(get_timestamp())}.png" -class DiskImageStorage(ImageStorageBase): +class DiskImageFileStorage(ImageFileStorageBase): """Stores images on disk""" __output_folder: str @@ -206,7 +206,7 @@ class DiskImageStorage(ImageStorageBase): self.__set_cache(image_path, image) return image except Exception as e: - raise ImageStorageBase.ImageFileNotFoundException from e + raise ImageFileStorageBase.ImageFileNotFoundException from e # TODO: make this a bit more flexible for e.g. cloud storage def get_path( @@ -282,7 +282,7 @@ class DiskImageStorage(ImageStorageBase): created=int(os.path.getctime(image_path)), ) except Exception as e: - raise ImageStorageBase.ImageFileSaveException from e + raise ImageFileStorageBase.ImageFileSaveException from e def delete(self, image_type: ImageType, image_name: str) -> None: try: @@ -302,7 +302,7 @@ class DiskImageStorage(ImageStorageBase): if thumbnail_path in self.__cache: del self.__cache[thumbnail_path] except Exception as e: - raise ImageStorageBase.ImageFileDeleteException from e + raise ImageFileStorageBase.ImageFileDeleteException from e def __get_cache(self, image_name: str) -> Image | None: return None if image_name not in self.__cache else self.__cache[image_name] diff --git a/invokeai/app/services/image_db.py b/invokeai/app/services/image_record_storage.py similarity index 97% rename from invokeai/app/services/image_db.py rename to invokeai/app/services/image_record_storage.py index 73984c6685..6d2d9dab68 100644 --- a/invokeai/app/services/image_db.py +++ b/invokeai/app/services/image_record_storage.py @@ -26,7 +26,7 @@ from invokeai.app.services.util.deserialize_image_record import ( from invokeai.app.services.item_storage import PaginatedResults -class ImageRecordServiceBase(ABC): +class ImageRecordStorageBase(ABC): """Low-level service responsible for interfacing with the image record store.""" class ImageRecordNotFoundException(Exception): @@ -85,7 +85,7 @@ class ImageRecordServiceBase(ABC): pass -class SqliteImageRecordService(ImageRecordServiceBase): +class SqliteImageRecordStorage(ImageRecordStorageBase): _filename: str _conn: sqlite3.Connection _cursor: sqlite3.Cursor @@ -277,7 +277,7 @@ class SqliteImageRecordService(ImageRecordServiceBase): self._conn.commit() except sqlite3.Error as e: self._conn.rollback() - raise ImageRecordServiceBase.ImageRecordDeleteException from e + raise ImageRecordStorageBase.ImageRecordDeleteException from e finally: self._lock.release() @@ -324,6 +324,6 @@ class SqliteImageRecordService(ImageRecordServiceBase): self._conn.commit() except sqlite3.Error as e: self._conn.rollback() - raise ImageRecordServiceBase.ImageRecordNotFoundException from e + raise ImageRecordStorageBase.ImageRecordNotFoundException from e finally: self._lock.release() diff --git a/invokeai/app/services/images.py b/invokeai/app/services/images.py index 190ddaa8d6..d2ce269715 100644 --- a/invokeai/app/services/images.py +++ b/invokeai/app/services/images.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Optional, Union import uuid from PIL.Image import Image as PILImageType from invokeai.app.models.image import ImageCategory, ImageType @@ -6,11 +6,15 @@ from invokeai.app.models.metadata import ( GeneratedImageOrLatentsMetadata, UploadedImageOrLatentsMetadata, ) -from invokeai.app.services.image_db import ( - ImageRecordServiceBase, +from invokeai.app.services.image_record_storage import ( + ImageRecordStorageBase, ) -from invokeai.app.services.models.image_record import ImageRecord -from invokeai.app.services.image_storage import ImageStorageBase +from invokeai.app.services.models.image_record import ( + ImageRecord, + ImageDTO, + image_record_to_dto, +) +from invokeai.app.services.image_file_storage import ImageFileStorageBase from invokeai.app.services.item_storage import PaginatedResults from invokeai.app.services.metadata import MetadataServiceBase from invokeai.app.services.urls import UrlServiceBase @@ -20,22 +24,22 @@ from invokeai.app.util.misc import get_iso_timestamp class ImageServiceDependencies: """Service dependencies for the ImageManagementService.""" - db: ImageRecordServiceBase - storage: ImageStorageBase + records: ImageRecordStorageBase + files: ImageFileStorageBase metadata: MetadataServiceBase urls: UrlServiceBase def __init__( self, - image_db_service: ImageRecordServiceBase, - image_storage_service: ImageStorageBase, - image_metadata_service: MetadataServiceBase, - url_service: UrlServiceBase, + image_record_storage: ImageRecordStorageBase, + image_file_storage: ImageFileStorageBase, + metadata: MetadataServiceBase, + url: UrlServiceBase, ): - self.db = image_db_service - self.storage = image_storage_service - self.metadata = image_metadata_service - self.url = url_service + self.records = image_record_storage + self.files = image_file_storage + self.metadata = metadata + self.urls = url class ImageService: @@ -45,24 +49,24 @@ class ImageService: def __init__( self, - image_db_service: ImageRecordServiceBase, - image_storage_service: ImageStorageBase, - image_metadata_service: MetadataServiceBase, - url_service: UrlServiceBase, + image_record_storage: ImageRecordStorageBase, + image_file_storage: ImageFileStorageBase, + metadata: MetadataServiceBase, + url: UrlServiceBase, ): self._services = ImageServiceDependencies( - image_db_service=image_db_service, - image_storage_service=image_storage_service, - image_metadata_service=image_metadata_service, - url_service=url_service, + image_record_storage=image_record_storage, + image_file_storage=image_file_storage, + metadata=metadata, + url=url, ) def _create_image_name( self, image_type: ImageType, image_category: ImageCategory, - node_id: Union[str, None], - session_id: Union[str, None], + node_id: Optional[str] = None, + session_id: Optional[str] = None, ) -> str: """Creates an image name.""" uuid_str = str(uuid.uuid4()) @@ -77,12 +81,12 @@ class ImageService: image: PILImageType, image_type: ImageType, image_category: ImageCategory, - node_id: Union[str, None], - session_id: Union[str, None], - metadata: Union[ - GeneratedImageOrLatentsMetadata, UploadedImageOrLatentsMetadata, None - ], - ) -> ImageRecord: + node_id: Optional[str] = None, + session_id: Optional[str] = None, + metadata: Optional[ + Union[GeneratedImageOrLatentsMetadata, UploadedImageOrLatentsMetadata] + ] = None, + ) -> ImageDTO: """Creates an image, storing the file and its metadata.""" image_name = self._create_image_name( image_type=image_type, @@ -95,14 +99,14 @@ class ImageService: try: # TODO: Consider using a transaction here to ensure consistency between storage and database - self._services.storage.save( + self._services.files.save( image_type=image_type, image_name=image_name, image=image, metadata=metadata, ) - self._services.db.save( + self._services.records.save( image_name=image_name, image_type=image_type, image_category=image_category, @@ -112,15 +116,10 @@ class ImageService: created_at=timestamp, ) - image_url = self._services.url.get_image_url( - image_type=image_type, image_name=image_name - ) + image_url = self._services.urls.get_image_url(image_type, image_name) + thumbnail_url = self._services.urls.get_thumbnail_url(image_type, image_name) - thumbnail_url = self._services.url.get_thumbnail_url( - image_type=image_type, image_name=image_name - ) - - return ImageRecord( + return ImageDTO( image_name=image_name, image_type=image_type, image_category=image_category, @@ -131,32 +130,42 @@ class ImageService: image_url=image_url, thumbnail_url=thumbnail_url, ) - except ImageRecordServiceBase.ImageRecordSaveException: + except ImageRecordStorageBase.ImageRecordSaveException: # TODO: log this raise - except ImageStorageBase.ImageFileSaveException: + except ImageFileStorageBase.ImageFileSaveException: # TODO: log this raise def get_pil_image(self, image_type: ImageType, image_name: str) -> PILImageType: """Gets an image as a PIL image.""" try: - pil_image = self._services.storage.get( - image_type=image_type, image_name=image_name - ) - return pil_image - except ImageStorageBase.ImageFileNotFoundException: + return self._services.files.get(image_type, image_name) + except ImageFileStorageBase.ImageFileNotFoundException: # TODO: log this raise def get_record(self, image_type: ImageType, image_name: str) -> ImageRecord: """Gets an image record.""" try: - image_record = self._services.db.get( - image_type=image_type, image_name=image_name + return self._services.records.get(image_type, image_name) + except ImageRecordStorageBase.ImageRecordNotFoundException: + # TODO: log this + raise + + def get_dto(self, image_type: ImageType, image_name: str) -> ImageDTO: + """Gets an image DTO.""" + try: + image_record = self._services.records.get(image_type, image_name) + + image_dto = image_record_to_dto( + image_record, + self._services.urls.get_image_url(image_type, image_name), + self._services.urls.get_thumbnail_url(image_type, image_name), ) - return image_record - except ImageRecordServiceBase.ImageRecordNotFoundException: + + return image_dto + except ImageRecordStorageBase.ImageRecordNotFoundException: # TODO: log this raise @@ -164,12 +173,12 @@ class ImageService: """Deletes an image.""" # TODO: Consider using a transaction here to ensure consistency between storage and database try: - self._services.storage.delete(image_type=image_type, image_name=image_name) - self._services.db.delete(image_type=image_type, image_name=image_name) - except ImageRecordServiceBase.ImageRecordDeleteException: + self._services.files.delete(image_type, image_name) + self._services.records.delete(image_type, image_name) + except ImageRecordStorageBase.ImageRecordDeleteException: # TODO: log this raise - except ImageStorageBase.ImageFileDeleteException: + except ImageFileStorageBase.ImageFileDeleteException: # TODO: log this raise @@ -179,26 +188,34 @@ class ImageService: image_category: ImageCategory, page: int = 0, per_page: int = 10, - ) -> PaginatedResults[ImageRecord]: - """Gets a paginated list of image records.""" + ) -> PaginatedResults[ImageDTO]: + """Gets a paginated list of image DTOs.""" try: - results = self._services.db.get_many( - image_type=image_type, - image_category=image_category, - page=page, - per_page=per_page, + results = self._services.records.get_many( + image_type, + image_category, + page, + per_page, ) - for r in results.items: - r.image_url = self._services.url.get_image_url( - image_type=image_type, image_name=r.image_name + image_dtos = list( + map( + lambda r: image_record_to_dto( + r, + self._services.urls.get_image_url(image_type, r.image_name), + self._services.urls.get_thumbnail_url(image_type, r.image_name), + ), + results.items, ) + ) - r.thumbnail_url = self._services.url.get_thumbnail_url( - image_type=image_type, image_name=r.image_name - ) - - return results + return PaginatedResults[ImageDTO]( + items=image_dtos, + page=results.page, + pages=results.pages, + per_page=results.per_page, + total=results.total, + ) except Exception as e: raise e diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index 74fb7accff..37a6a02e73 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -1,8 +1,8 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI Team from types import ModuleType -from invokeai.app.services.image_db import ( - ImageRecordServiceBase, +from invokeai.app.services.image_record_storage import ( + ImageRecordStorageBase, ) from invokeai.app.services.images import ImageService from invokeai.app.services.metadata import MetadataServiceBase @@ -11,7 +11,7 @@ from invokeai.backend import ModelManager from .events import EventServiceBase from .latent_storage import LatentsStorageBase -from .image_storage import ImageStorageBase +from .image_file_storage import ImageFileStorageBase from .restoration_services import RestorationServices from .invocation_queue import InvocationQueueABC from .item_storage import ItemStorageABC @@ -23,13 +23,12 @@ class InvocationServices: events: EventServiceBase latents: LatentsStorageBase - images: ImageStorageBase + images: ImageFileStorageBase metadata: MetadataServiceBase queue: InvocationQueueABC model_manager: ModelManager restoration: RestorationServices configuration: InvokeAISettings - images_db: ImageRecordServiceBase urls: UrlServiceBase images_new: ImageService @@ -44,10 +43,9 @@ class InvocationServices: events: EventServiceBase, logger: ModuleType, latents: LatentsStorageBase, - images: ImageStorageBase, + images: ImageFileStorageBase, metadata: MetadataServiceBase, queue: InvocationQueueABC, - images_db: ImageRecordServiceBase, images_new: ImageService, urls: UrlServiceBase, graph_library: ItemStorageABC["LibraryGraph"], @@ -63,7 +61,6 @@ class InvocationServices: self.images = images self.metadata = metadata self.queue = queue - self.images_db = images_db self.images_new = images_new self.urls = urls self.graph_library = graph_library diff --git a/invokeai/app/services/latent_storage.py b/invokeai/app/services/latent_storage.py index 271bd17c1b..519c254087 100644 --- a/invokeai/app/services/latent_storage.py +++ b/invokeai/app/services/latent_storage.py @@ -48,7 +48,7 @@ class ForwardCacheLatentsStorage(LatentsStorageBase): return latent def save(self, name: str, data: torch.Tensor) -> None: - self.__underlying_storage.set(name, data) + self.__underlying_storage.save(name, data) self.__set_cache(name, data) def delete(self, name: str) -> None: diff --git a/invokeai/app/services/metadata.py b/invokeai/app/services/metadata.py index bc1cfdb063..910b291593 100644 --- a/invokeai/app/services/metadata.py +++ b/invokeai/app/services/metadata.py @@ -75,10 +75,10 @@ class MetadataServiceBase(ABC): """Builds an InvokeAIMetadata object""" pass - @abstractmethod - def create_metadata(self, session_id: str, node_id: str) -> dict: - """Creates metadata for a result""" - pass + # @abstractmethod + # def create_metadata(self, session_id: str, node_id: str) -> dict: + # """Creates metadata for a result""" + # pass class PngMetadataService(MetadataServiceBase): diff --git a/invokeai/app/services/models/image_record.py b/invokeai/app/services/models/image_record.py index 600508e57f..731e42e132 100644 --- a/invokeai/app/services/models/image_record.py +++ b/invokeai/app/services/models/image_record.py @@ -1,12 +1,11 @@ import datetime -from typing import Literal, Optional, Union +from typing import Optional, Union from pydantic import BaseModel, Field from invokeai.app.models.metadata import ( GeneratedImageOrLatentsMetadata, UploadedImageOrLatentsMetadata, ) from invokeai.app.models.image import ImageCategory, ImageType -from invokeai.app.models.resources import ResourceType class ImageRecord(BaseModel): @@ -23,7 +22,27 @@ class ImageRecord(BaseModel): metadata: Optional[ Union[GeneratedImageOrLatentsMetadata, UploadedImageOrLatentsMetadata] ] = Field(default=None, description="The image's metadata.") - image_url: Optional[str] = Field(default=None, description="The URL of the image.") - thumbnail_url: Optional[str] = Field( - default=None, description="The thumbnail URL of the image." + + +class ImageDTO(ImageRecord): + """Deserialized image record with URLs.""" + + image_url: str = Field(description="The URL of the image.") + thumbnail_url: str = Field(description="The thumbnail URL of the image.") + + +def image_record_to_dto( + image_record: ImageRecord, image_url: str, thumbnail_url: str +) -> ImageDTO: + """Converts an image record to an image DTO.""" + return ImageDTO( + image_name=image_record.image_name, + image_type=image_record.image_type, + image_category=image_record.image_category, + created_at=image_record.created_at, + session_id=image_record.session_id, + node_id=image_record.node_id, + metadata=image_record.metadata, + image_url=image_url, + thumbnail_url=thumbnail_url, ) From d14b02e93fd5d86e8516986750cc54c5550e45f6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 21 May 2023 20:24:37 +1000 Subject: [PATCH 10/72] feat(logger): fix logger type issues --- invokeai/app/api/dependencies.py | 7 +++++-- invokeai/app/services/invocation_services.py | 7 ++----- invokeai/backend/util/logging.py | 20 ++++++++++---------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index e0fe960ea0..9a8816a5f2 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -1,12 +1,12 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) +from logging import Logger import os from types import ModuleType from invokeai.app.services.image_record_storage import SqliteImageRecordStorage from invokeai.app.services.images import ImageService from invokeai.app.services.urls import LocalUrlService - -import invokeai.backend.util.logging as logger +from invokeai.backend.util.logging import InvokeAILogger from ..services.default_graphs import create_system_graphs from ..services.latent_storage import DiskLatentsStorage, ForwardCacheLatentsStorage @@ -40,6 +40,9 @@ def check_internet() -> bool: return False +logger = InvokeAILogger.getLogger() + + class ApiDependencies: """Contains and initializes all dependencies for the API""" diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index 37a6a02e73..ab6069b7c0 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -1,9 +1,6 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI Team -from types import ModuleType -from invokeai.app.services.image_record_storage import ( - ImageRecordStorageBase, -) +from logging import Logger from invokeai.app.services.images import ImageService from invokeai.app.services.metadata import MetadataServiceBase from invokeai.app.services.urls import UrlServiceBase @@ -41,7 +38,7 @@ class InvocationServices: self, model_manager: ModelManager, events: EventServiceBase, - logger: ModuleType, + logger: Logger, latents: LatentsStorageBase, images: ImageFileStorageBase, metadata: MetadataServiceBase, diff --git a/invokeai/backend/util/logging.py b/invokeai/backend/util/logging.py index 3822ccafbe..9d1262d5c6 100644 --- a/invokeai/backend/util/logging.py +++ b/invokeai/backend/util/logging.py @@ -76,16 +76,16 @@ class InvokeAILogFormatter(logging.Formatter): reset = "\x1b[0m" # Log Format - format = "[%(asctime)s]::[%(name)s]::%(levelname)s --> %(message)s" + log_format = "[%(asctime)s]::[%(name)s]::%(levelname)s --> %(message)s" ## More Formatting Options: %(pathname)s, %(filename)s, %(module)s, %(lineno)d # Format Map FORMATS = { - logging.DEBUG: cyan + format + reset, - logging.INFO: grey + format + reset, - logging.WARNING: yellow + format + reset, - logging.ERROR: red + format + reset, - logging.CRITICAL: bold_red + format + reset + logging.DEBUG: cyan + log_format + reset, + logging.INFO: grey + log_format + reset, + logging.WARNING: yellow + log_format + reset, + logging.ERROR: red + log_format + reset, + logging.CRITICAL: bold_red + log_format + reset } def format(self, record): @@ -98,13 +98,13 @@ class InvokeAILogger(object): loggers = dict() @classmethod - def getLogger(self, name: str = 'InvokeAI') -> logging.Logger: - if name not in self.loggers: + def getLogger(cls, name: str = 'InvokeAI') -> logging.Logger: + if name not in cls.loggers: logger = logging.getLogger(name) logger.setLevel(logging.DEBUG) ch = logging.StreamHandler() fmt = InvokeAILogFormatter() ch.setFormatter(fmt) logger.addHandler(ch) - self.loggers[name] = logger - return self.loggers[name] + cls.loggers[name] = logger + return cls.loggers[name] From f7804f61268fd0e24df167a8bcb6b40b4cc7e376 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 21 May 2023 20:24:59 +1000 Subject: [PATCH 11/72] feat(nodes): add logger to images service --- invokeai/app/api/dependencies.py | 10 ++------ invokeai/app/services/images.py | 25 +++++++++++++------- invokeai/app/services/invocation_services.py | 8 ------- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index 9a8816a5f2..09be2daecc 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -2,7 +2,6 @@ from logging import Logger import os -from types import ModuleType from invokeai.app.services.image_record_storage import SqliteImageRecordStorage from invokeai.app.services.images import ImageService from invokeai.app.services.urls import LocalUrlService @@ -20,7 +19,6 @@ from ..services.invoker import Invoker from ..services.processor import DefaultInvocationProcessor from ..services.sqlite import SqliteItemStorage from ..services.metadata import PngMetadataService -from ..services.results import SqliteResultsService from .events import FastAPIEventService @@ -83,19 +81,15 @@ class ApiDependencies: image_file_storage=image_file_storage, metadata=metadata, url=urls, + logger=logger, ) - # register event handler to update the `results` table when a graph execution state is inserted or updated - # graph_execution_manager.on_changed(results.handle_graph_execution_state_change) - services = InvocationServices( model_manager=get_model_manager(config, logger), events=events, latents=latents, - images=images, + images=image_file_storage, images_new=images_new, - metadata=metadata, - urls=urls, queue=MemoryInvocationQueue(), graph_library=SqliteItemStorage[LibraryGraph]( filename=db_location, table_name="graphs" diff --git a/invokeai/app/services/images.py b/invokeai/app/services/images.py index d2ce269715..1559e518b4 100644 --- a/invokeai/app/services/images.py +++ b/invokeai/app/services/images.py @@ -1,3 +1,4 @@ +from logging import Logger from typing import Optional, Union import uuid from PIL.Image import Image as PILImageType @@ -28,6 +29,7 @@ class ImageServiceDependencies: files: ImageFileStorageBase metadata: MetadataServiceBase urls: UrlServiceBase + logger: Logger def __init__( self, @@ -35,11 +37,13 @@ class ImageServiceDependencies: image_file_storage: ImageFileStorageBase, metadata: MetadataServiceBase, url: UrlServiceBase, + logger: Logger, ): self.records = image_record_storage self.files = image_file_storage self.metadata = metadata self.urls = url + self.logger = logger class ImageService: @@ -53,12 +57,14 @@ class ImageService: image_file_storage: ImageFileStorageBase, metadata: MetadataServiceBase, url: UrlServiceBase, + logger: Logger, ): self._services = ImageServiceDependencies( image_record_storage=image_record_storage, image_file_storage=image_file_storage, metadata=metadata, url=url, + logger=logger, ) def _create_image_name( @@ -117,7 +123,9 @@ class ImageService: ) image_url = self._services.urls.get_image_url(image_type, image_name) - thumbnail_url = self._services.urls.get_thumbnail_url(image_type, image_name) + thumbnail_url = self._services.urls.get_thumbnail_url( + image_type, image_name + ) return ImageDTO( image_name=image_name, @@ -131,10 +139,10 @@ class ImageService: thumbnail_url=thumbnail_url, ) except ImageRecordStorageBase.ImageRecordSaveException: - # TODO: log this + self._services.logger.error("Failed to save image record") raise except ImageFileStorageBase.ImageFileSaveException: - # TODO: log this + self._services.logger.error("Failed to save image file") raise def get_pil_image(self, image_type: ImageType, image_name: str) -> PILImageType: @@ -142,7 +150,7 @@ class ImageService: try: return self._services.files.get(image_type, image_name) except ImageFileStorageBase.ImageFileNotFoundException: - # TODO: log this + self._services.logger.error("Failed to get image file") raise def get_record(self, image_type: ImageType, image_name: str) -> ImageRecord: @@ -150,7 +158,7 @@ class ImageService: try: return self._services.records.get(image_type, image_name) except ImageRecordStorageBase.ImageRecordNotFoundException: - # TODO: log this + self._services.logger.error("Failed to get image record") raise def get_dto(self, image_type: ImageType, image_name: str) -> ImageDTO: @@ -166,7 +174,7 @@ class ImageService: return image_dto except ImageRecordStorageBase.ImageRecordNotFoundException: - # TODO: log this + self._services.logger.error("Failed to get image DTO") raise def delete(self, image_type: ImageType, image_name: str): @@ -176,10 +184,10 @@ class ImageService: self._services.files.delete(image_type, image_name) self._services.records.delete(image_type, image_name) except ImageRecordStorageBase.ImageRecordDeleteException: - # TODO: log this + self._services.logger.error(f"Failed to delete image record") raise except ImageFileStorageBase.ImageFileDeleteException: - # TODO: log this + self._services.logger.error(f"Failed to delete image file") raise def get_many( @@ -217,6 +225,7 @@ class ImageService: total=results.total, ) except Exception as e: + self._services.logger.error("Failed to get paginated image DTOs") raise e def add_tag(self, image_type: ImageType, image_id: str, tag: str) -> None: diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index ab6069b7c0..41654168d6 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -2,8 +2,6 @@ from logging import Logger from invokeai.app.services.images import ImageService -from invokeai.app.services.metadata import MetadataServiceBase -from invokeai.app.services.urls import UrlServiceBase from invokeai.backend import ModelManager from .events import EventServiceBase @@ -21,12 +19,10 @@ class InvocationServices: events: EventServiceBase latents: LatentsStorageBase images: ImageFileStorageBase - metadata: MetadataServiceBase queue: InvocationQueueABC model_manager: ModelManager restoration: RestorationServices configuration: InvokeAISettings - urls: UrlServiceBase images_new: ImageService # NOTE: we must forward-declare any types that include invocations, since invocations can use services @@ -41,10 +37,8 @@ class InvocationServices: logger: Logger, latents: LatentsStorageBase, images: ImageFileStorageBase, - metadata: MetadataServiceBase, queue: InvocationQueueABC, images_new: ImageService, - urls: UrlServiceBase, graph_library: ItemStorageABC["LibraryGraph"], graph_execution_manager: ItemStorageABC["GraphExecutionState"], processor: "InvocationProcessorABC", @@ -56,10 +50,8 @@ class InvocationServices: self.logger = logger self.latents = latents self.images = images - self.metadata = metadata self.queue = queue self.images_new = images_new - self.urls = urls self.graph_library = graph_library self.graph_execution_manager = graph_execution_manager self.processor = processor From 22c34c343a86bea7117a1dbc4cee440403b38025 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 21 May 2023 20:27:34 +1000 Subject: [PATCH 12/72] feat(nodes): fix types for InvocationServices --- invokeai/app/services/invocation_services.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index 41654168d6..a85089554c 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -1,9 +1,9 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI Team - +from typing import TYPE_CHECKING from logging import Logger + from invokeai.app.services.images import ImageService from invokeai.backend import ModelManager - from .events import EventServiceBase from .latent_storage import LatentsStorageBase from .image_file_storage import ImageFileStorageBase @@ -13,6 +13,11 @@ from .item_storage import ItemStorageABC from .config import InvokeAISettings +if TYPE_CHECKING: + from invokeai.app.services.graph import GraphExecutionState, LibraryGraph + from invokeai.app.services.invoker import InvocationProcessorABC + + class InvocationServices: """Services that can be used by invocations""" From 5bf9891553eb423c0d39539bc66c2ad551f37323 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 21 May 2023 22:15:44 +1000 Subject: [PATCH 13/72] feat(nodes): it works --- invokeai/app/api/dependencies.py | 4 +- invokeai/app/api/routers/image_files.py | 164 ++---------- invokeai/app/api_app.py | 2 +- invokeai/app/invocations/generate.py | 50 ++-- invokeai/app/models/metadata.py | 21 +- invokeai/app/services/image_file_storage.py | 233 ++++-------------- invokeai/app/services/image_record_storage.py | 25 +- invokeai/app/services/images.py | 208 ++++++++++++---- invokeai/app/services/models/image_record.py | 37 ++- invokeai/app/services/urls.py | 6 +- .../services/util/deserialize_image_record.py | 33 --- 11 files changed, 302 insertions(+), 481 deletions(-) delete mode 100644 invokeai/app/services/util/deserialize_image_record.py diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index 09be2daecc..1ad53f31ca 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -63,9 +63,7 @@ class ApiDependencies: urls = LocalUrlService() - image_file_storage = DiskImageFileStorage( - f"{output_folder}/images", metadata_service=metadata - ) + image_file_storage = DiskImageFileStorage(f"{output_folder}/images") # TODO: build a file/path manager? db_location = os.path.join(output_folder, "invokeai.db") diff --git a/invokeai/app/api/routers/image_files.py b/invokeai/app/api/routers/image_files.py index a42b2a1e63..2694df5b19 100644 --- a/invokeai/app/api/routers/image_files.py +++ b/invokeai/app/api/routers/image_files.py @@ -1,165 +1,47 @@ -# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) -import io -from datetime import datetime, timezone -import json -import os -from typing import Any -import uuid - -from fastapi import Body, HTTPException, Path, Query, Request, UploadFile -from fastapi.responses import FileResponse, Response +# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI Team +from fastapi import HTTPException, Path +from fastapi.responses import FileResponse from fastapi.routing import APIRouter -from PIL import Image -from invokeai.app.api.models.images import ( - ImageResponse, - ImageResponseMetadata, -) from invokeai.app.models.image import ImageType -from invokeai.app.services.item_storage import PaginatedResults from ..dependencies import ApiDependencies -images_router = APIRouter(prefix="/v1/files/images", tags=["images", "files"]) +image_files_router = APIRouter(prefix="/v1/files/images", tags=["images", "files"]) -# @images_router.get("/{image_type}/{image_name}", operation_id="get_image") -# async def get_image( -# image_type: ImageType = Path(description="The type of image to get"), -# image_name: str = Path(description="The name of the image to get"), -# ) -> FileResponse: -# """Gets an image""" - -# path = ApiDependencies.invoker.services.images.get_path( -# image_type=image_type, image_name=image_name -# ) - -# if ApiDependencies.invoker.services.images.validate_path(path): -# return FileResponse(path) -# else: -# raise HTTPException(status_code=404) - - -@images_router.get("/{image_type}/{image_name}", operation_id="get_image") +@image_files_router.get("/{image_type}/{image_name}", operation_id="get_image") async def get_image( image_type: ImageType = Path(description="The type of the image to get"), image_name: str = Path(description="The id of the image to get"), ) -> FileResponse: """Gets an image""" - path = ApiDependencies.invoker.services.images.get_path( - image_type=image_type, image_name=image_name - ) + try: + path = ApiDependencies.invoker.services.images_new.get_path( + image_type=image_type, image_name=image_name + ) - if ApiDependencies.invoker.services.images.validate_path(path): return FileResponse(path) - else: + except Exception as e: raise HTTPException(status_code=404) -@images_router.delete("/{image_type}/{image_name}", operation_id="delete_image") -async def delete_image( - image_type: ImageType = Path(description="The type of the image to delete"), - image_name: str = Path(description="The name of the image to delete"), -) -> None: - """Deletes an image and its thumbnail""" - - ApiDependencies.invoker.services.images.delete( - image_type=image_type, image_name=image_name - ) - - -@images_router.get( - "/{image_type}/thumbnails/{thumbnail_id}", operation_id="get_thumbnail" +@image_files_router.get( + "/{image_type}/{image_name}/thumbnail", operation_id="get_thumbnail" ) async def get_thumbnail( - image_type: ImageType = Path(description="The type of the thumbnail to get"), - thumbnail_id: str = Path(description="The id of the thumbnail to get"), -) -> FileResponse | Response: + image_type: ImageType = Path( + description="The type of the image whose thumbnail to get" + ), + image_name: str = Path(description="The id of the image whose thumbnail to get"), +) -> FileResponse: """Gets a thumbnail""" - path = ApiDependencies.invoker.services.images.get_path( - image_type=image_type, image_name=thumbnail_id, is_thumbnail=True - ) - - if ApiDependencies.invoker.services.images.validate_path(path): - return FileResponse(path) - else: - raise HTTPException(status_code=404) - - -@images_router.post( - "/uploads/", - operation_id="upload_image", - responses={ - 201: { - "description": "The image was uploaded successfully", - "model": ImageResponse, - }, - 415: {"description": "Image upload failed"}, - }, - status_code=201, -) -async def upload_image( - file: UploadFile, image_type: ImageType, request: Request, response: Response -) -> ImageResponse: - if not file.content_type.startswith("image"): - raise HTTPException(status_code=415, detail="Not an image") - - contents = await file.read() - try: - img = Image.open(io.BytesIO(contents)) - except: - # Error opening the image - raise HTTPException(status_code=415, detail="Failed to read image") + path = ApiDependencies.invoker.services.images_new.get_path( + image_type=image_type, image_name=image_name, thumbnail=True + ) - filename = f"{uuid.uuid4()}_{str(int(datetime.now(timezone.utc).timestamp()))}.png" - - saved_image = ApiDependencies.invoker.services.images.save( - image_type, filename, img - ) - - invokeai_metadata = ApiDependencies.invoker.services.metadata.get_metadata(img) - - image_url = ApiDependencies.invoker.services.images.get_uri( - image_type, saved_image.image_name - ) - - thumbnail_url = ApiDependencies.invoker.services.images.get_uri( - image_type, saved_image.image_name, True - ) - - res = ImageResponse( - image_type=image_type, - image_name=saved_image.image_name, - image_url=image_url, - thumbnail_url=thumbnail_url, - metadata=ImageResponseMetadata( - created=saved_image.created, - width=img.width, - height=img.height, - invokeai=invokeai_metadata, - ), - ) - - response.status_code = 201 - response.headers["Location"] = image_url - - return res - - -@images_router.get( - "/", - operation_id="list_images", - responses={200: {"model": PaginatedResults[ImageResponse]}}, -) -async def list_images( - image_type: ImageType = Query( - default=ImageType.RESULT, description="The type of images to get" - ), - page: int = Query(default=0, description="The page of images to get"), - per_page: int = Query(default=10, description="The number of images per page"), -) -> PaginatedResults[ImageResponse]: - """Gets a list of images""" - result = ApiDependencies.invoker.services.images.list(image_type, page, per_page) - return result + return FileResponse(path) + except Exception as e: + raise HTTPException(status_code=404) diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index 9720474109..dffb2ec139 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -71,7 +71,7 @@ async def shutdown_event(): app.include_router(sessions.session_router, prefix="/api") -app.include_router(image_files.images_router, prefix="/api") +app.include_router(image_files.image_files_router, prefix="/api") app.include_router(models.models_router, prefix="/api") diff --git a/invokeai/app/invocations/generate.py b/invokeai/app/invocations/generate.py index 525be128e4..a27027dfe4 100644 --- a/invokeai/app/invocations/generate.py +++ b/invokeai/app/invocations/generate.py @@ -93,34 +93,42 @@ class TextToImageInvocation(BaseInvocation, SDImageInvocation): # each time it is called. We only need the first one. generate_output = next(outputs) - # Results are image and seed, unwrap for now and ignore the seed - # TODO: pre-seed? - # TODO: can this return multiple results? Should it? - image_type = ImageType.RESULT - image_name = context.services.images.create_name( - context.graph_execution_state_id, self.id - ) - - metadata = context.services.metadata.build_metadata( - session_id=context.graph_execution_state_id, node=self - ) - - context.services.images.save( - image_type, image_name, generate_output.image, metadata - ) - - context.services.images_db.set( - id=image_name, + image_dto = context.services.images_new.create( + image=generate_output.image, image_type=ImageType.RESULT, image_category=ImageCategory.IMAGE, session_id=context.graph_execution_state_id, node_id=self.id, - metadata=GeneratedImageOrLatentsMetadata(), ) + # Results are image and seed, unwrap for now and ignore the seed + # TODO: pre-seed? + # TODO: can this return multiple results? Should it? + # image_type = ImageType.RESULT + # image_name = context.services.images.create_name( + # context.graph_execution_state_id, self.id + # ) + + # metadata = context.services.metadata.build_metadata( + # session_id=context.graph_execution_state_id, node=self + # ) + + # context.services.images.save( + # image_type, image_name, generate_output.image, metadata + # ) + + # context.services.images_db.set( + # id=image_name, + # image_type=ImageType.RESULT, + # image_category=ImageCategory.IMAGE, + # session_id=context.graph_execution_state_id, + # node_id=self.id, + # metadata=GeneratedImageOrLatentsMetadata(), + # ) + return build_image_output( - image_type=image_type, - image_name=image_name, + image_type=image_dto.image_type, + image_name=image_dto.image_name, image=generate_output.image, ) diff --git a/invokeai/app/models/metadata.py b/invokeai/app/models/metadata.py index aae3337266..35998fa27e 100644 --- a/invokeai/app/models/metadata.py +++ b/invokeai/app/models/metadata.py @@ -2,8 +2,11 @@ from typing import Optional from pydantic import BaseModel, Field, StrictFloat, StrictInt, StrictStr -class GeneratedImageOrLatentsMetadata(BaseModel): - """Core generation metadata for an image/tensor generated in InvokeAI. +class ImageMetadata(BaseModel): + """ + Core generation metadata for an image/tensor generated in InvokeAI. + + Also includes any metadata from the image's PNG tEXt chunks. Generated by traversing the execution graph, collecting the parameters of the nearest ancestors of a given node. @@ -51,20 +54,6 @@ class GeneratedImageOrLatentsMetadata(BaseModel): # vae: Optional[str] = Field(default=None,description="The VAE used for decoding.") # unet: Optional[str] = Field(default=None,description="The UNet used dor inference.") # clip: Optional[str] = Field(default=None,description="The CLIP Encoder used for conditioning.") - - -class UploadedImageOrLatentsMetadata(BaseModel): - """Limited metadata for an uploaded image/tensor.""" - - width: Optional[StrictInt] = Field( - default=None, description="Width of the image/tensor in pixels." - ) - height: Optional[StrictInt] = Field( - default=None, description="Height of the image/tensor in pixels." - ) - # The extra field will be the contents of the PNG file's tEXt chunk. It may have come - # from another SD application or InvokeAI, so it needs to be flexible. - # If the upload is a not an image or `image_latents` tensor, this will be omitted. extra: Optional[StrictStr] = Field( default=None, description="Extra metadata, extracted from the PNG tEXt chunk." ) diff --git a/invokeai/app/services/image_file_storage.py b/invokeai/app/services/image_file_storage.py index ff3640011a..3a99940068 100644 --- a/invokeai/app/services/image_file_storage.py +++ b/invokeai/app/services/image_file_storage.py @@ -1,28 +1,16 @@ -# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) - +# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI Team import os -from glob import glob from abc import ABC, abstractmethod from pathlib import Path from queue import Queue -from typing import Dict, List +from typing import Dict, Optional -from PIL.Image import Image -import PIL.Image as PILImage +from PIL.Image import Image as PILImageType +from PIL import Image +from PIL.PngImagePlugin import PngInfo from send2trash import send2trash -from invokeai.app.api.models.images import ( - ImageResponse, - ImageResponseMetadata, - SavedImage, -) + from invokeai.app.models.image import ImageType -from invokeai.app.services.metadata import ( - InvokeAIMetadata, - MetadataServiceBase, - build_invokeai_metadata_pnginfo, -) -from invokeai.app.services.item_storage import PaginatedResults -from invokeai.app.util.misc import get_timestamp from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail @@ -48,61 +36,27 @@ class ImageFileStorageBase(ABC): super().__init__(message) @abstractmethod - def get(self, image_type: ImageType, image_name: str) -> Image: + def get(self, image_type: ImageType, image_name: str) -> PILImageType: """Retrieves an image as PIL Image.""" pass - @abstractmethod - def list( - self, image_type: ImageType, page: int = 0, per_page: int = 10 - ) -> PaginatedResults[ImageResponse]: - """Gets a paginated list of images.""" - pass - - # TODO: make this a bit more flexible for e.g. cloud storage + # # TODO: make this a bit more flexible for e.g. cloud storage @abstractmethod def get_path( - self, image_type: ImageType, image_name: str, is_thumbnail: bool = False + self, image_type: ImageType, image_name: str, thumbnail: bool = False ) -> str: - """Gets the internal path to an image or its thumbnail.""" - pass - - # TODO: make this a bit more flexible for e.g. cloud storage - @abstractmethod - def get_uri( - self, image_type: ImageType, image_name: str, is_thumbnail: bool = False - ) -> str: - """Gets the external URI to an image or its thumbnail.""" - pass - - # @abstractmethod - # def get_image_location( - # self, image_type: ImageType, image_name: str - # ) -> str: - # """Gets the location of an image.""" - # pass - - # @abstractmethod - # def get_thumbnail_location( - # self, image_type: ImageType, image_name: str - # ) -> str: - # """Gets the location of an image's thumbnail.""" - # pass - - # TODO: make this a bit more flexible for e.g. cloud storage - @abstractmethod - def validate_path(self, path: str) -> bool: - """Validates an image path.""" + """Gets the internal path to an image or thumbnail.""" pass @abstractmethod def save( self, + image: PILImageType, image_type: ImageType, image_name: str, - image: Image, - metadata: InvokeAIMetadata | None = None, - ) -> SavedImage: + pnginfo: Optional[PngInfo] = 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.""" pass @@ -111,26 +65,20 @@ class ImageFileStorageBase(ABC): """Deletes an image and its thumbnail (if one exists).""" pass - def create_name(self, context_id: str, node_id: str) -> str: - """Creates a unique contextual image filename.""" - return f"{context_id}_{node_id}_{str(get_timestamp())}.png" - class DiskImageFileStorage(ImageFileStorageBase): """Stores images on disk""" __output_folder: str __cache_ids: Queue # TODO: this is an incredibly naive cache - __cache: Dict[str, Image] + __cache: Dict[str, PILImageType] __max_cache_size: int - __metadata_service: MetadataServiceBase - def __init__(self, output_folder: str, metadata_service: MetadataServiceBase): + def __init__(self, output_folder: str): self.__output_folder = output_folder self.__cache = dict() self.__cache_ids = Queue() self.__max_cache_size = 10 # TODO: get this from config - self.__metadata_service = metadata_service Path(output_folder).mkdir(parents=True, exist_ok=True) @@ -143,144 +91,38 @@ class DiskImageFileStorage(ImageFileStorageBase): parents=True, exist_ok=True ) - def list( - self, image_type: ImageType, page: int = 0, per_page: int = 10 - ) -> PaginatedResults[ImageResponse]: - dir_path = os.path.join(self.__output_folder, image_type) - image_paths = glob(f"{dir_path}/*.png") - count = len(image_paths) - - sorted_image_paths = sorted( - glob(f"{dir_path}/*.png"), key=os.path.getctime, reverse=True - ) - - page_of_image_paths = sorted_image_paths[ - page * per_page : (page + 1) * per_page - ] - - page_of_images: List[ImageResponse] = [] - - for path in page_of_image_paths: - filename = os.path.basename(path) - img = PILImage.open(path) - - invokeai_metadata = self.__metadata_service.get_metadata(img) - - page_of_images.append( - ImageResponse( - image_type=image_type, - image_name=filename, - # TODO: DiskImageStorage should not be building URLs...? - image_url=self.get_uri(image_type, filename), - thumbnail_url=self.get_uri(image_type, filename, True), - # TODO: Creation of this object should happen elsewhere (?), just making it fit here so it works - metadata=ImageResponseMetadata( - created=int(os.path.getctime(path)), - width=img.width, - height=img.height, - invokeai=invokeai_metadata, - ), - ) - ) - - page_count_trunc = int(count / per_page) - page_count_mod = count % per_page - page_count = page_count_trunc if page_count_mod == 0 else page_count_trunc + 1 - - return PaginatedResults[ImageResponse]( - items=page_of_images, - page=page, - pages=page_count, - per_page=per_page, - total=count, - ) - - def get(self, image_type: ImageType, image_name: str) -> Image: + def get(self, image_type: ImageType, image_name: str) -> PILImageType: try: image_path = self.get_path(image_type, image_name) cache_item = self.__get_cache(image_path) if cache_item: return cache_item - image = PILImage.open(image_path) + image = Image.open(image_path) self.__set_cache(image_path, image) return image - except Exception as e: + except FileNotFoundError as e: raise ImageFileStorageBase.ImageFileNotFoundException from e - # TODO: make this a bit more flexible for e.g. cloud storage - def get_path( - self, image_type: ImageType, image_name: str, is_thumbnail: bool = False - ) -> str: - # strip out any relative path shenanigans - basename = os.path.basename(image_name) - - if is_thumbnail: - path = os.path.join( - self.__output_folder, image_type, "thumbnails", basename - ) - else: - path = os.path.join(self.__output_folder, image_type, basename) - - abspath = os.path.abspath(path) - - return abspath - - def get_uri( - self, image_type: ImageType, image_name: str, is_thumbnail: bool = False - ) -> str: - # strip out any relative path shenanigans - basename = os.path.basename(image_name) - - if is_thumbnail: - thumbnail_basename = get_thumbnail_name(basename) - uri = f"api/v1/images/{image_type.value}/thumbnails/{thumbnail_basename}" - else: - uri = f"api/v1/images/{image_type.value}/{basename}" - - return uri - - def validate_path(self, path: str) -> bool: - try: - os.stat(path) - return True - except FileNotFoundError: - return False - except Exception as e: - raise e - def save( self, + image: PILImageType, image_type: ImageType, image_name: str, - image: Image, - metadata: InvokeAIMetadata | None = None, - ) -> SavedImage: + pnginfo: Optional[PngInfo] = None, + thumbnail_size: int = 256, + ) -> None: try: image_path = self.get_path(image_type, image_name) - - # TODO: Reading the image and then saving it strips the metadata... - if metadata: - pnginfo = build_invokeai_metadata_pnginfo(metadata=metadata) - image.save(image_path, "PNG", pnginfo=pnginfo) - else: - image.save(image_path) # this saved image has an empty info + image.save(image_path, "PNG", pnginfo=pnginfo) thumbnail_name = get_thumbnail_name(image_name) - thumbnail_path = self.get_path( - image_type, thumbnail_name, is_thumbnail=True - ) - thumbnail_image = make_thumbnail(image) + thumbnail_path = self.get_path(image_type, thumbnail_name, thumbnail=True) + thumbnail_image = make_thumbnail(image, thumbnail_size) thumbnail_image.save(thumbnail_path) self.__set_cache(image_path, image) self.__set_cache(thumbnail_path, thumbnail_image) - - return SavedImage( - image_name=image_name, - thumbnail_name=thumbnail_name, - created=int(os.path.getctime(image_path)), - ) except Exception as e: raise ImageFileStorageBase.ImageFileSaveException from e @@ -304,10 +146,29 @@ class DiskImageFileStorage(ImageFileStorageBase): except Exception as e: raise ImageFileStorageBase.ImageFileDeleteException from e - def __get_cache(self, image_name: str) -> Image | None: + # TODO: make this a bit more flexible for e.g. cloud storage + def get_path( + self, image_type: ImageType, image_name: str, thumbnail: bool = False + ) -> str: + # strip out any relative path shenanigans + basename = os.path.basename(image_name) + + if thumbnail: + thumbnail_name = get_thumbnail_name(basename) + path = os.path.join( + self.__output_folder, image_type, "thumbnails", thumbnail_name + ) + else: + path = os.path.join(self.__output_folder, image_type, basename) + + abspath = os.path.abspath(path) + + return abspath + + def __get_cache(self, image_name: str) -> PILImageType | None: return None if image_name not in self.__cache else self.__cache[image_name] - def __set_cache(self, image_name: str, image: Image): + def __set_cache(self, image_name: str, image: PILImageType): if not image_name in self.__cache: self.__cache[image_name] = image self.__cache_ids.put( diff --git a/invokeai/app/services/image_record_storage.py b/invokeai/app/services/image_record_storage.py index 6d2d9dab68..7c79cf7a34 100644 --- a/invokeai/app/services/image_record_storage.py +++ b/invokeai/app/services/image_record_storage.py @@ -1,25 +1,18 @@ from abc import ABC, abstractmethod import datetime from typing import Optional -from invokeai.app.models.metadata import ( - GeneratedImageOrLatentsMetadata, - UploadedImageOrLatentsMetadata, -) - import sqlite3 import threading from typing import Optional, Union -from invokeai.app.models.metadata import ( - GeneratedImageOrLatentsMetadata, - UploadedImageOrLatentsMetadata, -) + +from invokeai.app.models.metadata import ImageMetadata from invokeai.app.models.image import ( ImageCategory, ImageType, ) from invokeai.app.services.util.create_enum_table import create_enum_table -from invokeai.app.services.models.image_record import ImageRecord -from invokeai.app.services.util.deserialize_image_record import ( +from invokeai.app.services.models.image_record import ( + ImageRecord, deserialize_image_record, ) @@ -76,9 +69,7 @@ class ImageRecordStorageBase(ABC): image_category: ImageCategory, session_id: Optional[str], node_id: Optional[str], - metadata: Optional[ - GeneratedImageOrLatentsMetadata | UploadedImageOrLatentsMetadata - ], + metadata: Optional[ImageMetadata], created_at: str = datetime.datetime.utcnow().isoformat(), ) -> None: """Saves an image record.""" @@ -288,9 +279,7 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): image_category: ImageCategory, session_id: Optional[str], node_id: Optional[str], - metadata: Union[ - GeneratedImageOrLatentsMetadata, UploadedImageOrLatentsMetadata, None - ], + metadata: Optional[ImageMetadata], created_at: str, ) -> None: try: @@ -306,7 +295,7 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): image_category, node_id, session_id, - metadata + metadata, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?); diff --git a/invokeai/app/services/images.py b/invokeai/app/services/images.py index 1559e518b4..53f6a756d6 100644 --- a/invokeai/app/services/images.py +++ b/invokeai/app/services/images.py @@ -1,12 +1,13 @@ +from abc import ABC, abstractmethod +import json from logging import Logger from typing import Optional, Union import uuid from PIL.Image import Image as PILImageType +from PIL import PngImagePlugin + from invokeai.app.models.image import ImageCategory, ImageType -from invokeai.app.models.metadata import ( - GeneratedImageOrLatentsMetadata, - UploadedImageOrLatentsMetadata, -) +from invokeai.app.models.metadata import ImageMetadata from invokeai.app.services.image_record_storage import ( ImageRecordStorageBase, ) @@ -22,8 +23,95 @@ from invokeai.app.services.urls import UrlServiceBase from invokeai.app.util.misc import get_iso_timestamp +class ImageServiceABC(ABC): + """ + High-level service for image management. + + Provides methods for creating, retrieving, and deleting images. + """ + + @abstractmethod + def create( + self, + image: PILImageType, + image_type: ImageType, + image_category: ImageCategory, + node_id: Optional[str] = None, + session_id: Optional[str] = None, + metadata: Optional[ImageMetadata] = None, + ) -> ImageDTO: + """Creates an image, storing the file and its metadata.""" + pass + + @abstractmethod + def get_pil_image(self, image_type: ImageType, image_name: str) -> PILImageType: + """Gets an image as a PIL image.""" + pass + + @abstractmethod + def get_record(self, image_type: ImageType, image_name: str) -> ImageRecord: + """Gets an image record.""" + pass + + @abstractmethod + def get_path(self, image_type: ImageType, image_name: str) -> str: + """Gets an image's path""" + pass + + @abstractmethod + def get_image_url(self, image_type: ImageType, image_name: str) -> str: + """Gets an image's URL""" + pass + + @abstractmethod + def get_thumbnail_url(self, image_type: ImageType, image_name: str) -> str: + """Gets an image's URL""" + pass + + @abstractmethod + def get_dto(self, image_type: ImageType, image_name: str) -> ImageDTO: + """Gets an image DTO.""" + pass + + @abstractmethod + def get_many( + self, + image_type: ImageType, + image_category: ImageCategory, + page: int = 0, + per_page: int = 10, + ) -> PaginatedResults[ImageDTO]: + """Gets a paginated list of image DTOs.""" + pass + + @abstractmethod + def delete(self, image_type: ImageType, image_name: str): + """Deletes an image.""" + pass + + @abstractmethod + def add_tag(self, image_type: ImageType, image_id: str, tag: str) -> None: + """Adds a tag to an image.""" + pass + + @abstractmethod + def remove_tag(self, image_type: ImageType, image_id: str, tag: str) -> None: + """Removes a tag from an image.""" + pass + + @abstractmethod + def favorite(self, image_type: ImageType, image_id: str) -> None: + """Favorites an image.""" + pass + + @abstractmethod + def unfavorite(self, image_type: ImageType, image_id: str) -> None: + """Unfavorites an image.""" + pass + + class ImageServiceDependencies: - """Service dependencies for the ImageManagementService.""" + """Service dependencies for the ImageService.""" records: ImageRecordStorageBase files: ImageFileStorageBase @@ -46,9 +134,7 @@ class ImageServiceDependencies: self.logger = logger -class ImageService: - """High-level service for image management.""" - +class ImageService(ImageServiceABC): _services: ImageServiceDependencies def __init__( @@ -67,21 +153,6 @@ class ImageService: logger=logger, ) - def _create_image_name( - self, - image_type: ImageType, - image_category: ImageCategory, - node_id: Optional[str] = None, - session_id: Optional[str] = None, - ) -> str: - """Creates an image name.""" - uuid_str = str(uuid.uuid4()) - - if node_id is not None and session_id is not None: - return f"{image_type.value}_{image_category.value}_{session_id}_{node_id}_{uuid_str}.png" - - return f"{image_type.value}_{image_category.value}_{uuid_str}.png" - def create( self, image: PILImageType, @@ -89,11 +160,8 @@ class ImageService: image_category: ImageCategory, node_id: Optional[str] = None, session_id: Optional[str] = None, - metadata: Optional[ - Union[GeneratedImageOrLatentsMetadata, UploadedImageOrLatentsMetadata] - ] = None, + metadata: Optional[ImageMetadata] = None, ) -> ImageDTO: - """Creates an image, storing the file and its metadata.""" image_name = self._create_image_name( image_type=image_type, image_category=image_category, @@ -103,13 +171,19 @@ class ImageService: timestamp = get_iso_timestamp() + if metadata is not None: + pnginfo = PngImagePlugin.PngInfo() + pnginfo.add_text("invokeai", json.dumps(metadata)) + else: + pnginfo = None + try: # TODO: Consider using a transaction here to ensure consistency between storage and database self._services.files.save( image_type=image_type, image_name=image_name, image=image, - metadata=metadata, + pnginfo=pnginfo, ) self._services.records.save( @@ -144,25 +218,40 @@ class ImageService: except ImageFileStorageBase.ImageFileSaveException: self._services.logger.error("Failed to save image file") raise + except Exception as e: + self._services.logger.error("Problem saving image record and file") + raise e def get_pil_image(self, image_type: ImageType, image_name: str) -> PILImageType: - """Gets an image as a PIL image.""" try: return self._services.files.get(image_type, image_name) except ImageFileStorageBase.ImageFileNotFoundException: self._services.logger.error("Failed to get image file") raise + except Exception as e: + self._services.logger.error("Problem getting image file") + raise e def get_record(self, image_type: ImageType, image_name: str) -> ImageRecord: - """Gets an image record.""" try: return self._services.records.get(image_type, image_name) except ImageRecordStorageBase.ImageRecordNotFoundException: - self._services.logger.error("Failed to get image record") + self._services.logger.error("Image record not found") raise + except Exception as e: + self._services.logger.error("Problem getting image record") + raise e + + def get_path( + self, image_type: ImageType, image_name: str, thumbnail: bool = False + ) -> str: + try: + return self._services.files.get_path(image_type, image_name, thumbnail) + except Exception as e: + self._services.logger.error("Problem getting image path") + raise e def get_dto(self, image_type: ImageType, image_name: str) -> ImageDTO: - """Gets an image DTO.""" try: image_record = self._services.records.get(image_type, image_name) @@ -174,21 +263,11 @@ class ImageService: return image_dto except ImageRecordStorageBase.ImageRecordNotFoundException: - self._services.logger.error("Failed to get image DTO") - raise - - def delete(self, image_type: ImageType, image_name: str): - """Deletes an image.""" - # TODO: Consider using a transaction here to ensure consistency between storage and database - try: - self._services.files.delete(image_type, image_name) - self._services.records.delete(image_type, image_name) - except ImageRecordStorageBase.ImageRecordDeleteException: - self._services.logger.error(f"Failed to delete image record") - raise - except ImageFileStorageBase.ImageFileDeleteException: - self._services.logger.error(f"Failed to delete image file") + self._services.logger.error("Image record not found") raise + except Exception as e: + self._services.logger.error("Problem getting image DTO") + raise e def get_many( self, @@ -197,7 +276,6 @@ class ImageService: page: int = 0, per_page: int = 10, ) -> PaginatedResults[ImageDTO]: - """Gets a paginated list of image DTOs.""" try: results = self._services.records.get_many( image_type, @@ -225,21 +303,47 @@ class ImageService: total=results.total, ) except Exception as e: - self._services.logger.error("Failed to get paginated image DTOs") + self._services.logger.error("Problem getting paginated image DTOs") + raise e + + def delete(self, image_type: ImageType, image_name: str): + # TODO: Consider using a transaction here to ensure consistency between storage and database + try: + self._services.files.delete(image_type, image_name) + self._services.records.delete(image_type, image_name) + except ImageRecordStorageBase.ImageRecordDeleteException: + self._services.logger.error(f"Failed to delete image record") + raise + except ImageFileStorageBase.ImageFileDeleteException: + self._services.logger.error(f"Failed to delete image file") + raise + except Exception as e: + self._services.logger.error("Problem deleting image record and file") raise e def add_tag(self, image_type: ImageType, image_id: str, tag: str) -> None: - """Adds a tag to an image.""" raise NotImplementedError("The 'add_tag' method is not implemented yet.") def remove_tag(self, image_type: ImageType, image_id: str, tag: str) -> None: - """Removes a tag from an image.""" raise NotImplementedError("The 'remove_tag' method is not implemented yet.") def favorite(self, image_type: ImageType, image_id: str) -> None: - """Favorites an image.""" raise NotImplementedError("The 'favorite' method is not implemented yet.") def unfavorite(self, image_type: ImageType, image_id: str) -> None: - """Unfavorites an image.""" raise NotImplementedError("The 'unfavorite' method is not implemented yet.") + + def _create_image_name( + self, + image_type: ImageType, + image_category: ImageCategory, + node_id: Optional[str] = None, + session_id: Optional[str] = None, + ) -> str: + """Create a unique image name.""" + uuid_str = str(uuid.uuid4()) + + if node_id is not None and session_id is not None: + return f"{image_type.value}_{image_category.value}_{session_id}_{node_id}_{uuid_str}.png" + + return f"{image_type.value}_{image_category.value}_{uuid_str}.png" diff --git a/invokeai/app/services/models/image_record.py b/invokeai/app/services/models/image_record.py index 731e42e132..cd2f3aacbc 100644 --- a/invokeai/app/services/models/image_record.py +++ b/invokeai/app/services/models/image_record.py @@ -1,11 +1,10 @@ import datetime +import sqlite3 from typing import Optional, Union from pydantic import BaseModel, Field -from invokeai.app.models.metadata import ( - GeneratedImageOrLatentsMetadata, - UploadedImageOrLatentsMetadata, -) from invokeai.app.models.image import ImageCategory, ImageType +from invokeai.app.models.metadata import ImageMetadata +from invokeai.app.util.misc import get_iso_timestamp class ImageRecord(BaseModel): @@ -19,9 +18,9 @@ class ImageRecord(BaseModel): ) session_id: Optional[str] = Field(default=None, description="The session ID.") node_id: Optional[str] = Field(default=None, description="The node ID.") - metadata: Optional[ - Union[GeneratedImageOrLatentsMetadata, UploadedImageOrLatentsMetadata] - ] = Field(default=None, description="The image's metadata.") + metadata: Optional[ImageMetadata] = Field( + default=None, description="The image's metadata." + ) class ImageDTO(ImageRecord): @@ -46,3 +45,27 @@ def image_record_to_dto( image_url=image_url, thumbnail_url=thumbnail_url, ) + + +def deserialize_image_record(image_row: sqlite3.Row) -> ImageRecord: + """Deserializes an image record.""" + + image_dict = dict(image_row) + + image_type = ImageType(image_dict.get("image_type", ImageType.RESULT.value)) + + raw_metadata = image_dict.get("metadata", "{}") + + metadata = ImageMetadata.parse_raw(raw_metadata) + + return ImageRecord( + image_name=image_dict.get("id", "unknown"), + session_id=image_dict.get("session_id", None), + node_id=image_dict.get("node_id", None), + metadata=metadata, + image_type=image_type, + image_category=ImageCategory( + image_dict.get("image_category", ImageCategory.IMAGE.value) + ), + created_at=image_dict.get("created_at", get_iso_timestamp()), + ) diff --git a/invokeai/app/services/urls.py b/invokeai/app/services/urls.py index 16f8fc7494..8b2d53d2af 100644 --- a/invokeai/app/services/urls.py +++ b/invokeai/app/services/urls.py @@ -25,8 +25,8 @@ class LocalUrlService(UrlServiceBase): def get_image_url(self, image_type: ImageType, image_name: str) -> str: image_basename = os.path.basename(image_name) - return f"{self._base_url}/images/{image_type.value}/{image_basename}" + return f"{self._base_url}/files/images/{image_type.value}/{image_basename}" def get_thumbnail_url(self, image_type: ImageType, image_name: str) -> str: - thumbnail_basename = get_thumbnail_name(os.path.basename(image_name)) - return f"{self._base_url}/images/{image_type.value}/thumbnails/{thumbnail_basename}" + image_basename = os.path.basename(image_name) + return f"{self._base_url}/files/images/{image_type.value}/{image_basename}/thumbnail" diff --git a/invokeai/app/services/util/deserialize_image_record.py b/invokeai/app/services/util/deserialize_image_record.py deleted file mode 100644 index 52014b78c5..0000000000 --- a/invokeai/app/services/util/deserialize_image_record.py +++ /dev/null @@ -1,33 +0,0 @@ -from invokeai.app.models.metadata import ( - GeneratedImageOrLatentsMetadata, - UploadedImageOrLatentsMetadata, -) -from invokeai.app.models.image import ImageCategory, ImageType -from invokeai.app.services.models.image_record import ImageRecord -from invokeai.app.util.misc import get_iso_timestamp - - -def deserialize_image_record(image: dict) -> ImageRecord: - """Deserializes an image record.""" - - # All values *should* be present, except `session_id` and `node_id`, but provide some defaults just in case - - image_type = ImageType(image.get("image_type", ImageType.RESULT.value)) - raw_metadata = image.get("metadata", {}) - - if image_type == ImageType.UPLOAD: - metadata = UploadedImageOrLatentsMetadata.parse_obj(raw_metadata) - else: - metadata = GeneratedImageOrLatentsMetadata.parse_obj(raw_metadata) - - return ImageRecord( - image_name=image.get("id", "unknown"), - session_id=image.get("session_id", None), - node_id=image.get("node_id", None), - metadata=metadata, - image_type=image_type, - image_category=ImageCategory( - image.get("image_category", ImageCategory.IMAGE.value) - ), - created_at=image.get("created_at", get_iso_timestamp()), - ) From adde8450bcc6e4064111af1bd409dbb25cce56a3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 21 May 2023 22:43:04 +1000 Subject: [PATCH 14/72] fix(nodes): remove bad import --- invokeai/app/invocations/generate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/invokeai/app/invocations/generate.py b/invokeai/app/invocations/generate.py index a27027dfe4..64c5662137 100644 --- a/invokeai/app/invocations/generate.py +++ b/invokeai/app/invocations/generate.py @@ -10,7 +10,6 @@ from pydantic import BaseModel, Field from invokeai.app.models.image import ColorField, ImageField, ImageType from invokeai.app.invocations.util.choose_model import choose_model -from invokeai.app.models.metadata import GeneratedImageOrLatentsMetadata from invokeai.app.models.image import ImageCategory, ImageType from invokeai.app.util.misc import SEED_MAX, get_random_seed from invokeai.backend.generator.inpaint import infill_methods From cc1160a43a17157a81a457e4ab2b233eeac442b3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 21 May 2023 22:44:16 +1000 Subject: [PATCH 15/72] feat(nodes): streamline urlservice --- invokeai/app/services/images.py | 28 +++++++++++++++++----------- invokeai/app/services/urls.py | 24 +++++++++++------------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/invokeai/app/services/images.py b/invokeai/app/services/images.py index 53f6a756d6..9b46ebcc09 100644 --- a/invokeai/app/services/images.py +++ b/invokeai/app/services/images.py @@ -59,13 +59,8 @@ class ImageServiceABC(ABC): pass @abstractmethod - def get_image_url(self, image_type: ImageType, image_name: str) -> str: - """Gets an image's URL""" - pass - - @abstractmethod - def get_thumbnail_url(self, image_type: ImageType, image_name: str) -> str: - """Gets an image's URL""" + def get_url(self, image_type: ImageType, image_name: str, thumbnail: bool = False) -> str: + """Gets an image's or thumbnail's URL""" pass @abstractmethod @@ -197,8 +192,8 @@ class ImageService(ImageServiceABC): ) image_url = self._services.urls.get_image_url(image_type, image_name) - thumbnail_url = self._services.urls.get_thumbnail_url( - image_type, image_name + thumbnail_url = self._services.urls.get_image_url( + image_type, image_name, True ) return ImageDTO( @@ -251,6 +246,15 @@ class ImageService(ImageServiceABC): self._services.logger.error("Problem getting image path") raise e + def get_url( + self, image_type: ImageType, image_name: str, thumbnail: bool = False + ) -> str: + try: + return self._services.urls.get_image_url(image_type, image_name, thumbnail) + except Exception as e: + self._services.logger.error("Problem getting image path") + raise e + def get_dto(self, image_type: ImageType, image_name: str) -> ImageDTO: try: image_record = self._services.records.get(image_type, image_name) @@ -258,7 +262,7 @@ class ImageService(ImageServiceABC): image_dto = image_record_to_dto( image_record, self._services.urls.get_image_url(image_type, image_name), - self._services.urls.get_thumbnail_url(image_type, image_name), + self._services.urls.get_image_url(image_type, image_name, True), ) return image_dto @@ -289,7 +293,9 @@ class ImageService(ImageServiceABC): lambda r: image_record_to_dto( r, self._services.urls.get_image_url(image_type, r.image_name), - self._services.urls.get_thumbnail_url(image_type, r.image_name), + self._services.urls.get_image_url( + image_type, r.image_name, True + ), ), results.items, ) diff --git a/invokeai/app/services/urls.py b/invokeai/app/services/urls.py index 8b2d53d2af..989f6853c2 100644 --- a/invokeai/app/services/urls.py +++ b/invokeai/app/services/urls.py @@ -6,16 +6,13 @@ from invokeai.app.util.thumbnails import get_thumbnail_name class UrlServiceBase(ABC): - """Responsible for building URLs for resources (eg images or tensors)""" + """Responsible for building URLs for resources.""" @abstractmethod - def get_image_url(self, image_type: ImageType, image_name: str) -> str: - """Gets the URL for an image""" - pass - - @abstractmethod - def get_thumbnail_url(self, image_type: ImageType, image_name: str) -> str: - """Gets the URL for an image's thumbnail""" + def get_image_url( + self, image_type: ImageType, image_name: str, thumbnail: bool = False + ) -> str: + """Gets the URL for an image or thumbnail.""" pass @@ -23,10 +20,11 @@ class LocalUrlService(UrlServiceBase): def __init__(self, base_url: str = "api/v1"): self._base_url = base_url - def get_image_url(self, image_type: ImageType, image_name: str) -> str: + def get_image_url( + self, image_type: ImageType, image_name: str, thumbnail: bool = False + ) -> str: image_basename = os.path.basename(image_name) - return f"{self._base_url}/files/images/{image_type.value}/{image_basename}" + if thumbnail: + return f"{self._base_url}/files/images/{image_type.value}/{image_basename}/thumbnail" - def get_thumbnail_url(self, image_type: ImageType, image_name: str) -> str: - image_basename = os.path.basename(image_name) - return f"{self._base_url}/files/images/{image_type.value}/{image_basename}/thumbnail" + return f"{self._base_url}/files/images/{image_type.value}/{image_basename}" From c0f132e41a416d7c4a65d17a95a2200926eb1b3c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 21 May 2023 22:46:56 +1000 Subject: [PATCH 16/72] hack(nodes): hack to get image urls in the invocation complete event --- invokeai/app/services/events.py | 6 +++++- invokeai/app/services/processor.py | 23 ++++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/invokeai/app/services/events.py b/invokeai/app/services/events.py index 5f26c42c17..dd44bcf73a 100644 --- a/invokeai/app/services/events.py +++ b/invokeai/app/services/events.py @@ -1,6 +1,6 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) -from typing import Any +from typing import Any, Optional from invokeai.app.api.models.images import ProgressImage from invokeai.app.util.misc import get_timestamp @@ -50,6 +50,8 @@ class EventServiceBase: result: dict, node: dict, source_node_id: str, + image_url: Optional[str] = None, + thumbnail_url: Optional[str] = None, ) -> None: """Emitted when an invocation has completed""" self.__emit_session_event( @@ -59,6 +61,8 @@ class EventServiceBase: node=node, source_node_id=source_node_id, result=result, + image_url=image_url, + thumbnail_url=thumbnail_url ), ) diff --git a/invokeai/app/services/processor.py b/invokeai/app/services/processor.py index 9e3b5a0a30..250d007d06 100644 --- a/invokeai/app/services/processor.py +++ b/invokeai/app/services/processor.py @@ -1,7 +1,10 @@ import time import traceback from threading import Event, Thread, BoundedSemaphore +from typing import Any, TypeGuard +from invokeai.app.invocations.image import ImageOutput +from invokeai.app.models.image import ImageType from ..invocations.baseinvocation import InvocationContext from .invocation_queue import InvocationQueueItem from .invoker import InvocationProcessorABC, Invoker @@ -88,12 +91,30 @@ class DefaultInvocationProcessor(InvocationProcessorABC): graph_execution_state ) + def is_image_output(obj: Any) -> TypeGuard[ImageOutput]: + return obj.__class__ == ImageOutput + + outputs_dict = outputs.dict() + + if is_image_output(outputs): + image_url = self.__invoker.services.images_new.get_url( + ImageType.RESULT, outputs.image.image_name + ) + thumbnail_url = self.__invoker.services.images_new.get_url( + ImageType.RESULT, outputs.image.image_name, True + ) + else: + image_url = None + thumbnail_url = None + # Send complete event self.__invoker.services.events.emit_invocation_complete( graph_execution_state_id=graph_execution_state.id, node=invocation.dict(), source_node_id=source_node_id, - result=outputs.dict(), + result=outputs_dict, + image_url=image_url, + thumbnail_url=thumbnail_url, ) except KeyboardInterrupt: From 52c9e6ec91c334b027762ee499ad3212b9b73bbb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 21 May 2023 22:53:41 +1000 Subject: [PATCH 17/72] feat(nodes): organise/tidy --- invokeai/app/api/routers/results.py | 42 -- invokeai/app/services/image_record_storage.py | 41 +- invokeai/app/services/proposeddesign.py | 657 ------------------ invokeai/app/services/results.py | 466 ------------- .../app/services/util/create_enum_table.py | 39 -- 5 files changed, 38 insertions(+), 1207 deletions(-) delete mode 100644 invokeai/app/api/routers/results.py delete mode 100644 invokeai/app/services/proposeddesign.py delete mode 100644 invokeai/app/services/results.py delete mode 100644 invokeai/app/services/util/create_enum_table.py diff --git a/invokeai/app/api/routers/results.py b/invokeai/app/api/routers/results.py deleted file mode 100644 index 4190e5bd27..0000000000 --- a/invokeai/app/api/routers/results.py +++ /dev/null @@ -1,42 +0,0 @@ -from fastapi import HTTPException, Path, Query -from fastapi.routing import APIRouter -from invokeai.app.services.results import ResultType, ResultWithSession -from invokeai.app.services.item_storage import PaginatedResults - -from ..dependencies import ApiDependencies - -results_router = APIRouter(prefix="/v1/results", tags=["results"]) - - -@results_router.get("/{result_type}/{result_name}", operation_id="get_result") -async def get_result( - result_type: ResultType = Path(description="The type of result to get"), - result_name: str = Path(description="The name of the result to get"), -) -> ResultWithSession: - """Gets a result""" - - result = ApiDependencies.invoker.services.results.get( - result_id=result_name, result_type=result_type - ) - - if result is not None: - return result - else: - raise HTTPException(status_code=404) - - -@results_router.get( - "/", - operation_id="list_results", - responses={200: {"model": PaginatedResults[ResultWithSession]}}, -) -async def list_results( - result_type: ResultType = Query(description="The type of results to get"), - page: int = Query(default=0, description="The page of results to get"), - per_page: int = Query(default=10, description="The number of results per page"), -) -> PaginatedResults[ResultWithSession]: - """Gets a list of results""" - results = ApiDependencies.invoker.services.results.get_many( - result_type=result_type, page=page, per_page=per_page - ) - return results diff --git a/invokeai/app/services/image_record_storage.py b/invokeai/app/services/image_record_storage.py index 7c79cf7a34..d6b421a094 100644 --- a/invokeai/app/services/image_record_storage.py +++ b/invokeai/app/services/image_record_storage.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod import datetime -from typing import Optional +from enum import Enum +from typing import Optional, Type import sqlite3 import threading from typing import Optional, Union @@ -10,15 +11,49 @@ from invokeai.app.models.image import ( ImageCategory, ImageType, ) -from invokeai.app.services.util.create_enum_table import create_enum_table from invokeai.app.services.models.image_record import ( ImageRecord, deserialize_image_record, ) - from invokeai.app.services.item_storage import PaginatedResults +def create_sql_values_string_from_string_enum(enum: Type[Enum]): + """ + Creates a string of the form "('value1'), ('value2'), ..., ('valueN')" from a StrEnum. + """ + + delimiter = ", " + values = [f"('{e.value}')" for e in enum] + return delimiter.join(values) + + +def create_enum_table( + enum: Type[Enum], + table_name: str, + primary_key_name: str, + cursor: sqlite3.Cursor, +): + """ + Creates and populates a table to be used as a functional enum. + """ + + values_string = create_sql_values_string_from_string_enum(enum) + + cursor.execute( + f"""--sql + CREATE TABLE IF NOT EXISTS {table_name} ( + {primary_key_name} TEXT PRIMARY KEY + ); + """ + ) + cursor.execute( + f"""--sql + INSERT OR IGNORE INTO {table_name} ({primary_key_name}) VALUES {values_string}; + """ + ) + + class ImageRecordStorageBase(ABC): """Low-level service responsible for interfacing with the image record store.""" diff --git a/invokeai/app/services/proposeddesign.py b/invokeai/app/services/proposeddesign.py deleted file mode 100644 index 712d7224e9..0000000000 --- a/invokeai/app/services/proposeddesign.py +++ /dev/null @@ -1,657 +0,0 @@ -from abc import ABC, abstractmethod -from enum import Enum -import enum -import sqlite3 -import threading -from typing import Optional, Type, TypeVar, Union -from PIL.Image import Image as PILImage -from pydantic import BaseModel, Field -from torch import Tensor - -from invokeai.app.services.item_storage import PaginatedResults - - -""" -Substantial proposed changes to the management of images and tensor. - -tl;dr: -With the upcoming move to latents-only nodes, we need to handle metadata differently. After struggling with this unsuccessfully - trying to smoosh it in to the existing setup - I believe we need to expand the scope of the refactor to include the management of images and latents - and make `latents` a special case of `tensor`. - -full story: -The consensus for tensor-only nodes' metadata was to traverse the execution graph and grab the core parameters to write to the image. This was straightforward, and I've written functions to find the nearest t2l/l2l, noise, and compel nodes and build the metadata from those. - -But struggling to integrate this and the associated edge cases this brought up a number of issues deeper in the system (some of which I had previously implemented). The ImageStorageService is doing way too much, and we have a need to be able to retrieve sessions the session given image/latents id, which is not currently feasible due to SQLite's JSON parsing performance. - -I made a new ResultsService and `results` table in the db to facilitate this. This first attempt failed because it doesn't handle uploads and leaves the codebase messy. - -So I've spent the day trying to figure out to handle this in a sane way and think I've got something decent. I've described some changes to service bases and the database below. - -The gist of it is to store the core parameters for an image in its metadata when the image is saved, but never to read from it. Instead, the same metadata is stored in the database, which will be set up for efficient access. So when a page of images is requested, the metadata comes from the db instead of a filesystem operation. - -The URL generation responsibilities have been split off the image storage service in to a URL service. New database services/tables for images and tensor are added. These services will provide paginated images/tensors for the API to serve. This also paves the way for handling tensors as first-class outputs. -""" - - -# TODO: Make a new model for this -class ResourceOrigin(str, Enum): - """The origin of a resource (eg image or tensor).""" - - RESULTS = "results" - UPLOADS = "uploads" - INTERMEDIATES = "intermediates" - - -class ImageKind(str, Enum): - """The kind of an image.""" - - IMAGE = "image" - CONTROL_IMAGE = "control_image" - - -class TensorKind(str, Enum): - """The kind of a tensor.""" - - IMAGE_TENSOR = "tensor" - CONDITIONING = "conditioning" - - -""" -Core Generation Metadata Pydantic Model - -I've already implemented the code to traverse a session to build this object. -""" - - -class CoreGenerationMetadata(BaseModel): - """Core generation metadata for an image/tensor generated in InvokeAI. - - Generated by traversing the execution graph, collecting the parameters of the nearest ancestors of a given node. - - Full metadata may be accessed by querying for the session in the `graph_executions` table. - """ - - positive_conditioning: Optional[str] = Field( - description="The positive conditioning." - ) - negative_conditioning: Optional[str] = Field( - description="The negative conditioning." - ) - width: Optional[int] = Field(description="Width of the image/tensor in pixels.") - height: Optional[int] = Field(description="Height of the image/tensor in pixels.") - seed: Optional[int] = Field(description="The seed used for noise generation.") - cfg_scale: Optional[float] = Field( - description="The classifier-free guidance scale." - ) - steps: Optional[int] = Field(description="The number of steps used for inference.") - scheduler: Optional[str] = Field(description="The scheduler used for inference.") - model: Optional[str] = Field(description="The model used for inference.") - strength: Optional[float] = Field( - description="The strength used for image-to-image/tensor-to-tensor." - ) - image: Optional[str] = Field(description="The ID of the initial image.") - tensor: Optional[str] = Field(description="The ID of the initial tensor.") - # Pending model refactor: - # vae: Optional[str] = Field(description="The VAE used for decoding.") - # unet: Optional[str] = Field(description="The UNet used dor inference.") - # clip: Optional[str] = Field(description="The CLIP Encoder used for conditioning.") - - -""" -Minimal Uploads Metadata Model -""" - - -class UploadsMetadata(BaseModel): - """Limited metadata for an uploaded image/tensor.""" - - width: Optional[int] = Field(description="Width of the image/tensor in pixels.") - height: Optional[int] = Field(description="Height of the image/tensor in pixels.") - # The extra field will be the contents of the PNG file's tEXt chunk. It may have come - # from another SD application or InvokeAI, so we need to make it very flexible. I think it's - # best to just store it as a string and let the frontend parse it. - # If the upload is a tensor type, this will be omitted. - extra: Optional[str] = Field( - description="Extra metadata, extracted from the PNG tEXt chunk." - ) - - -""" -Slimmed-down Image Storage Service Base - - No longer lists images or generates URLs - only stores and retrieves images. - - OSS implementation for disk storage -""" - - -class ImageStorageBase(ABC): - """Responsible for storing and retrieving images.""" - - @abstractmethod - def save( - self, - image: PILImage, - image_kind: ImageKind, - origin: ResourceOrigin, - context_id: str, - node_id: str, - metadata: CoreGenerationMetadata, - ) -> str: - """Saves an image and its thumbnail, returning its unique identifier.""" - pass - - @abstractmethod - def get(self, id: str, thumbnail: bool = False) -> Union[PILImage, None]: - """Retrieves an image as a PIL Image.""" - pass - - @abstractmethod - def delete(self, id: str) -> None: - """Deletes an image.""" - pass - - -class TensorStorageBase(ABC): - """Responsible for storing and retrieving tensors.""" - - @abstractmethod - def save( - self, - tensor: Tensor, - tensor_kind: TensorKind, - origin: ResourceOrigin, - context_id: str, - node_id: str, - metadata: CoreGenerationMetadata, - ) -> str: - """Saves a tensor, returning its unique identifier.""" - pass - - @abstractmethod - def get(self, id: str, thumbnail: bool = False) -> Union[Tensor, None]: - """Retrieves a tensor as a torch Tensor.""" - pass - - @abstractmethod - def delete(self, id: str) -> None: - """Deletes a tensor.""" - pass - - -""" -New Url Service Base - - Abstracts the logic for generating URLs out of the storage service - - OSS implementation for locally-hosted URLs - - Also provides a method to get the internal path to a resource (for OSS, the FS path) -""" - - -class ResourceLocationServiceBase(ABC): - """Responsible for locating resources (eg images or tensors).""" - - @abstractmethod - def get_url(self, id: str) -> str: - """Gets the URL for a resource.""" - pass - - @abstractmethod - def get_path(self, id: str) -> str: - """Gets the path for a resource.""" - pass - - -""" -New Images Database Service Base - -This is a new service that will be responsible for the new `images` table(s): - - Storing images in the table - - Retrieving individual images and pages of images - - Deleting individual images - -Operations will typically use joins with the various `images` tables. -""" - - -class ImagesDbServiceBase(ABC): - """Responsible for interfacing with `images` table.""" - - class GeneratedImageEntity(BaseModel): - id: str = Field(description="The unique identifier for the image.") - session_id: str = Field(description="The session ID.") - node_id: str = Field(description="The node ID.") - metadata: CoreGenerationMetadata = Field( - description="The metadata for the image." - ) - - class UploadedImageEntity(BaseModel): - id: str = Field(description="The unique identifier for the image.") - metadata: UploadsMetadata = Field(description="The metadata for the image.") - - @abstractmethod - def get(self, id: str) -> Union[GeneratedImageEntity, UploadedImageEntity, None]: - """Gets an image from the `images` table.""" - pass - - @abstractmethod - def get_many( - self, image_kind: ImageKind, page: int = 0, per_page: int = 10 - ) -> PaginatedResults[Union[GeneratedImageEntity, UploadedImageEntity]]: - """Gets a page of images from the `images` table.""" - pass - - @abstractmethod - def delete(self, id: str) -> None: - """Deletes an image from the `images` table.""" - pass - - @abstractmethod - def set( - self, - id: str, - image_kind: ImageKind, - session_id: Optional[str], - node_id: Optional[str], - metadata: CoreGenerationMetadata | UploadsMetadata, - ) -> None: - """Sets an image in the `images` table.""" - pass - - -""" -New Tensor Database Service Base - -This is a new service that will be responsible for the new `tensor` table: - - Storing tensor in the table - - Retrieving individual tensor and pages of tensor - - Deleting individual tensor - -Operations will always use joins with the `tensor_metadata` table. -""" - - -class TensorDbServiceBase(ABC): - """Responsible for interfacing with `tensor` table.""" - - class GeneratedTensorEntity(BaseModel): - id: str = Field(description="The unique identifier for the tensor.") - session_id: str = Field(description="The session ID.") - node_id: str = Field(description="The node ID.") - metadata: CoreGenerationMetadata = Field( - description="The metadata for the tensor." - ) - - class UploadedTensorEntity(BaseModel): - id: str = Field(description="The unique identifier for the tensor.") - metadata: UploadsMetadata = Field(description="The metadata for the tensor.") - - @abstractmethod - def get(self, id: str) -> Union[GeneratedTensorEntity, UploadedTensorEntity, None]: - """Gets a tensor from the `tensor` table.""" - pass - - @abstractmethod - def get_many( - self, tensor_kind: TensorKind, page: int = 0, per_page: int = 10 - ) -> PaginatedResults[Union[GeneratedTensorEntity, UploadedTensorEntity]]: - """Gets a page of tensor from the `tensor` table.""" - pass - - @abstractmethod - def delete(self, id: str) -> None: - """Deletes a tensor from the `tensor` table.""" - pass - - @abstractmethod - def set( - self, - id: str, - tensor_kind: TensorKind, - session_id: Optional[str], - node_id: Optional[str], - metadata: CoreGenerationMetadata | UploadsMetadata, - ) -> None: - """Sets a tensor in the `tensor` table.""" - pass - - -""" -Database Changes - -The existing tables will remain as-is, new tables will be added. - -Tensor now also have the same types as images - `results`, `intermediates`, `uploads`. Storage, retrieval, and operations may diverge from images in the future, so they are managed separately. - -A few `images` tables are created to store all images: - - `results` and `intermediates` images have additional data: `session_id` and `node_id`, and may be further differentiated in the future. For this reason, they each get their own table. - - `uploads` do not get their own table, as they are never going to have more than an `id`, `image_kind` and `timestamp`. - - `images_metadata` holds the same image metadata that is written to the image. This table, along with the URL service, allow us to more efficiently serve images without having to read the image from storage. - -The same tables are made for `tensor` and for the moment, implementation is expected to be identical. - -Schemas for each table below. - -Insertions and updates of ancillary tables (e.g. `results_images`, `images_metadata`, etc) will need to be done manually in the services, but should be straightforward. Deletion via cascading will be handled by the database. -""" - - -def create_sql_values_string_from_string_enum(enum: Type[Enum]): - """ - Creates a string of the form "('value1'), ('value2'), ..., ('valueN')" from a StrEnum. - """ - - delimiter = ", " - values = [f"('{e.value}')" for e in enum] - return delimiter.join(values) - - -def create_sql_table_from_enum( - enum: Type[Enum], - table_name: str, - primary_key_name: str, - cursor: sqlite3.Cursor, - lock: threading.Lock, -): - """ - Creates and populates a table to be used as a functional enum. - """ - - try: - lock.acquire() - - values_string = create_sql_values_string_from_string_enum(enum) - - cursor.execute( - f"""--sql - CREATE TABLE IF NOT EXISTS {table_name} ( - {primary_key_name} TEXT PRIMARY KEY - ); - """ - ) - cursor.execute( - f"""--sql - INSERT OR IGNORE INTO {table_name} ({primary_key_name}) VALUES {values_string}; - """ - ) - finally: - lock.release() - - -""" -`resource_origins` functions as an enum for the ResourceOrigin model. -""" - - -def create_resource_origins_table(cursor: sqlite3.Cursor, lock: threading.Lock): - create_sql_table_from_enum( - enum=ResourceOrigin, - table_name="resource_origins", - primary_key_name="origin_name", - cursor=cursor, - lock=lock, - ) - - -""" -`image_kinds` functions as an enum for the ImageType model. -""" - - -def create_image_kinds_table(cursor: sqlite3.Cursor, lock: threading.Lock): - create_sql_table_from_enum( - enum=ImageKind, - table_name="image_kinds", - primary_key_name="kind_name", - cursor=cursor, - lock=lock, - ) - - -""" -`tensor_kinds` functions as an enum for the TensorType model. -""" - - -def create_tensor_kinds_table(cursor: sqlite3.Cursor, lock: threading.Lock): - create_sql_table_from_enum( - enum=TensorKind, - table_name="tensor_kinds", - primary_key_name="kind_name", - cursor=cursor, - lock=lock, - ) - - -""" -`images` stores all images, regardless of type -""" - - -def create_images_table(cursor: sqlite3.Cursor, lock: threading.Lock): - try: - lock.acquire() - - cursor.execute( - """--sql - CREATE TABLE IF NOT EXISTS images ( - id TEXT PRIMARY KEY, - origin TEXT, - image_kind TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY(origin) REFERENCES resource_origins(origin_name), - FOREIGN KEY(image_kind) REFERENCES image_kinds(kind_name) - ); - """ - ) - cursor.execute( - """--sql - CREATE UNIQUE INDEX IF NOT EXISTS idx_images_id ON images(id); - """ - ) - cursor.execute( - """--sql - CREATE INDEX IF NOT EXISTS idx_images_origin ON images(origin); - """ - ) - cursor.execute( - """--sql - CREATE INDEX IF NOT EXISTS idx_images_image_kind ON images(image_kind); - """ - ) - finally: - lock.release() - - -""" -`image_results` stores additional data specific to `results` images. -""" - - -def create_image_results_table(cursor: sqlite3.Cursor, lock: threading.Lock): - try: - lock.acquire() - - cursor.execute( - """--sql - CREATE TABLE IF NOT EXISTS image_results ( - images_id TEXT PRIMARY KEY, - session_id TEXT NOT NULL, - node_id TEXT NOT NULL, - FOREIGN KEY(images_id) REFERENCES images(id) ON DELETE CASCADE - ); - """ - ) - cursor.execute( - """--sql - CREATE UNIQUE INDEX IF NOT EXISTS idx_image_results_images_id ON image_results(id); - """ - ) - finally: - lock.release() - - -""" -`image_intermediates` stores additional data specific to `intermediates` images -""" - - -def create_image_intermediates_table(cursor: sqlite3.Cursor, lock: threading.Lock): - try: - lock.acquire() - - cursor.execute( - """--sql - CREATE TABLE IF NOT EXISTS image_intermediates ( - images_id TEXT PRIMARY KEY, - session_id TEXT NOT NULL, - node_id TEXT NOT NULL, - FOREIGN KEY(images_id) REFERENCES images(id) ON DELETE CASCADE - ); - """ - ) - cursor.execute( - """--sql - CREATE UNIQUE INDEX IF NOT EXISTS idx_image_intermediates_images_id ON image_intermediates(id); - """ - ) - finally: - lock.release() - - -""" -`images_metadata` stores basic metadata for any image type -""" - - -def create_images_metadata_table(cursor: sqlite3.Cursor, lock: threading.Lock): - try: - lock.acquire() - - cursor.execute( - """--sql - CREATE TABLE IF NOT EXISTS images_metadata ( - images_id TEXT PRIMARY KEY, - metadata TEXT, - FOREIGN KEY(images_id) REFERENCES images(id) ON DELETE CASCADE - ); - """ - ) - cursor.execute( - """--sql - CREATE UNIQUE INDEX IF NOT EXISTS idx_images_metadata_images_id ON images_metadata(images_id); - """ - ) - finally: - lock.release() - - -# `tensor` table: stores references to tensor - - -def create_tensors_table(cursor: sqlite3.Cursor, lock: threading.Lock): - try: - lock.acquire() - - cursor.execute( - """--sql - CREATE TABLE IF NOT EXISTS tensors ( - id TEXT PRIMARY KEY, - origin TEXT, - tensor_kind TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY(origin) REFERENCES resource_origins(origin_name), - FOREIGN KEY(tensor_kind) REFERENCES tensor_kinds(kind_name), - ); - """ - ) - cursor.execute( - """--sql - CREATE UNIQUE INDEX IF NOT EXISTS idx_tensors_id ON tensors(id); - """ - ) - cursor.execute( - """--sql - CREATE INDEX IF NOT EXISTS idx_tensors_origin ON tensors(origin); - """ - ) - cursor.execute( - """--sql - CREATE INDEX IF NOT EXISTS idx_tensors_tensor_kind ON tensors(tensor_kind); - """ - ) - finally: - lock.release() - - -# `results_tensor` stores additional data specific to `result` tensor - - -def create_tensor_results_table(cursor: sqlite3.Cursor, lock: threading.Lock): - try: - lock.acquire() - - cursor.execute( - """--sql - CREATE TABLE IF NOT EXISTS tensor_results ( - tensor_id TEXT PRIMARY KEY, - session_id TEXT NOT NULL, - node_id TEXT NOT NULL, - FOREIGN KEY(tensor_id) REFERENCES tensors(id) ON DELETE CASCADE - ); - """ - ) - cursor.execute( - """--sql - CREATE UNIQUE INDEX IF NOT EXISTS idx_tensor_results_tensor_id ON tensor_results(tensor_id); - """ - ) - finally: - lock.release() - - -# `tensor_intermediates` stores additional data specific to `intermediate` tensor - - -def create_tensor_intermediates_table(cursor: sqlite3.Cursor, lock: threading.Lock): - try: - lock.acquire() - - cursor.execute( - """--sql - CREATE TABLE IF NOT EXISTS tensor_intermediates ( - tensor_id TEXT PRIMARY KEY, - session_id TEXT NOT NULL, - node_id TEXT NOT NULL, - FOREIGN KEY(tensor_id) REFERENCES tensors(id) ON DELETE CASCADE - ); - """ - ) - cursor.execute( - """--sql - CREATE UNIQUE INDEX IF NOT EXISTS idx_tensor_intermediates_tensor_id ON tensor_intermediates(tensor_id); - """ - ) - finally: - lock.release() - - -# `tensors_metadata` table: stores generated/transformed metadata for tensor - - -def create_tensors_metadata_table(cursor: sqlite3.Cursor, lock: threading.Lock): - try: - lock.acquire() - - cursor.execute( - """--sql - CREATE TABLE IF NOT EXISTS tensors_metadata ( - tensor_id TEXT PRIMARY KEY, - metadata TEXT, - FOREIGN KEY(tensor_id) REFERENCES tensors(id) ON DELETE CASCADE - ); - """ - ) - cursor.execute( - """--sql - CREATE UNIQUE INDEX IF NOT EXISTS idx_tensors_metadata_tensor_id ON tensors_metadata(tensor_id); - """ - ) - finally: - lock.release() diff --git a/invokeai/app/services/results.py b/invokeai/app/services/results.py deleted file mode 100644 index df7bf7bc6b..0000000000 --- a/invokeai/app/services/results.py +++ /dev/null @@ -1,466 +0,0 @@ -from enum import Enum - -from abc import ABC, abstractmethod -import json -import sqlite3 -from threading import Lock -from typing import Any, Union - -import networkx as nx - -from pydantic import BaseModel, Field, parse_obj_as, parse_raw_as -from invokeai.app.invocations.image import ImageOutput -from invokeai.app.services.graph import Edge, GraphExecutionState -from invokeai.app.invocations.latent import LatentsOutput -from invokeai.app.services.item_storage import PaginatedResults -from invokeai.app.util.misc import get_timestamp - - -class ResultType(str, Enum): - image_output = "image_output" - latents_output = "latents_output" - - -class Result(BaseModel): - """A session result""" - - id: str = Field(description="Result ID") - session_id: str = Field(description="Session ID") - node_id: str = Field(description="Node ID") - data: Union[LatentsOutput, ImageOutput] = Field(description="The result data") - - -class ResultWithSession(BaseModel): - """A result with its session""" - - result: Result = Field(description="The result") - session: GraphExecutionState = Field(description="The session") - - -# Create a directed graph -from typing import Any, TypedDict, Union -from networkx import DiGraph -import networkx as nx -import json - - -# We need to use a loose class for nodes to allow for graceful parsing - we cannot use the stricter -# model used by the system, because we may be a graph in an old format. We can, however, use the -# Edge model, because the edge format does not change. -class LooseGraph(BaseModel): - id: str - nodes: dict[str, dict[str, Any]] - edges: list[Edge] - - -# An intermediate type used during parsing -class NearestAncestor(TypedDict): - node_id: str - metadata: dict[str, Any] - - -# The ancestor types that contain the core metadata -ANCESTOR_TYPES = ['t2l', 'l2l'] - -# The core metadata parameters in the ancestor types -ANCESTOR_PARAMS = ['steps', 'model', 'cfg_scale', 'scheduler', 'strength'] - -# The core metadata parameters in the noise node -NOISE_FIELDS = ['seed', 'width', 'height'] - -# Find nearest t2l or l2l ancestor from a given l2i node -def find_nearest_ancestor(G: DiGraph, node_id: str) -> Union[NearestAncestor, None]: - """Returns metadata for the nearest ancestor of a given node. - - Parameters: - G (DiGraph): A directed graph. - node_id (str): The ID of the starting node. - - Returns: - NearestAncestor | None: An object with the ID and metadata of the nearest ancestor. - """ - - # Retrieve the node from the graph - node = G.nodes[node_id] - - # If the node type is one of the core metadata node types, gather necessary metadata and return - if node.get('type') in ANCESTOR_TYPES: - parsed_metadata = {param: val for param, val in node.items() if param in ANCESTOR_PARAMS} - return NearestAncestor(node_id=node_id, metadata=parsed_metadata) - - - # Else, look for the ancestor in the predecessor nodes - for predecessor in G.predecessors(node_id): - result = find_nearest_ancestor(G, predecessor) - if result: - return result - - # If there are no valid ancestors, return None - return None - - -def get_additional_metadata(graph: LooseGraph, node_id: str) -> Union[dict[str, Any], None]: - """Collects additional metadata from nodes connected to a given node. - - Parameters: - graph (LooseGraph): The graph. - node_id (str): The ID of the node. - - Returns: - dict | None: A dictionary containing additional metadata. - """ - - metadata = {} - - # Iterate over all edges in the graph - for edge in graph.edges: - dest_node_id = edge.destination.node_id - dest_field = edge.destination.field - source_node = graph.nodes[edge.source.node_id] - - # If the destination node ID matches the given node ID, gather necessary metadata - if dest_node_id == node_id: - # If the destination field is 'positive_conditioning', add the 'prompt' from the source node - if dest_field == 'positive_conditioning': - metadata['positive_conditioning'] = source_node.get('prompt') - # If the destination field is 'negative_conditioning', add the 'prompt' from the source node - if dest_field == 'negative_conditioning': - metadata['negative_conditioning'] = source_node.get('prompt') - # If the destination field is 'noise', add the core noise fields from the source node - if dest_field == 'noise': - for field in NOISE_FIELDS: - metadata[field] = source_node.get(field) - return metadata - -def build_core_metadata(graph_raw: str, node_id: str) -> Union[dict, None]: - """Builds the core metadata for a given node. - - Parameters: - graph_raw (str): The graph structure as a raw string. - node_id (str): The ID of the node. - - Returns: - dict | None: A dictionary containing core metadata. - """ - - # Create a directed graph to facilitate traversal - G = nx.DiGraph() - - # Convert the raw graph string into a JSON object - graph = parse_obj_as(LooseGraph, graph_raw) - - # Add nodes and edges to the graph - for node_id, node_data in graph.nodes.items(): - G.add_node(node_id, **node_data) - for edge in graph.edges: - G.add_edge(edge.source.node_id, edge.destination.node_id) - - # Find the nearest ancestor of the given node - ancestor = find_nearest_ancestor(G, node_id) - - # If no ancestor was found, return None - if ancestor is None: - return None - - metadata = ancestor['metadata'] - ancestor_id = ancestor['node_id'] - - # Get additional metadata related to the ancestor - addl_metadata = get_additional_metadata(graph, ancestor_id) - - # If additional metadata was found, add it to the main metadata - if addl_metadata is not None: - metadata.update(addl_metadata) - - return metadata - - - -class ResultsServiceABC(ABC): - """The Results service is responsible for retrieving results.""" - - @abstractmethod - def get( - self, result_id: str, result_type: ResultType - ) -> Union[ResultWithSession, None]: - pass - - @abstractmethod - def get_many( - self, result_type: ResultType, page: int = 0, per_page: int = 10 - ) -> PaginatedResults[ResultWithSession]: - pass - - @abstractmethod - def search( - self, query: str, page: int = 0, per_page: int = 10 - ) -> PaginatedResults[ResultWithSession]: - pass - - @abstractmethod - def handle_graph_execution_state_change(self, session: GraphExecutionState) -> None: - pass - - -class SqliteResultsService(ResultsServiceABC): - """SQLite implementation of the Results service.""" - - _filename: str - _conn: sqlite3.Connection - _cursor: sqlite3.Cursor - _lock: Lock - - def __init__(self, filename: str): - super().__init__() - - self._filename = filename - self._lock = Lock() - - self._conn = sqlite3.connect( - self._filename, check_same_thread=False - ) # TODO: figure out a better threading solution - self._cursor = self._conn.cursor() - - self._create_table() - - def _create_table(self): - try: - self._lock.acquire() - self._cursor.execute( - """--sql - CREATE TABLE IF NOT EXISTS results ( - id TEXT PRIMARY KEY, -- the result's name - result_type TEXT, -- `image_output` | `latents_output` - node_id TEXT, -- the node that produced this result - session_id TEXT, -- the session that produced this result - created_at INTEGER, -- the time at which this result was created - data TEXT -- the result itself - ); - """ - ) - self._cursor.execute( - """--sql - CREATE UNIQUE INDEX IF NOT EXISTS idx_result_id ON results(id); - """ - ) - finally: - self._lock.release() - - def _parse_joined_result(self, result_row: Any, column_names: list[str]): - result_raw = {} - session_raw = {} - - for idx, name in enumerate(column_names): - if name == "session": - session_raw = json.loads(result_row[idx]) - elif name == "data": - result_raw[name] = json.loads(result_row[idx]) - else: - result_raw[name] = result_row[idx] - - graph_raw = session_raw['execution_graph'] - - result = parse_obj_as(Result, result_raw) - session = parse_obj_as(GraphExecutionState, session_raw) - - m = build_core_metadata(graph_raw, result.node_id) - print(m) - - # g = session.execution_graph.nx_graph() - # ancestors = nx.dag.ancestors(g, result.node_id) - - # nodes = [session.execution_graph.get_node(result.node_id)] - # for ancestor in ancestors: - # nodes.append(session.execution_graph.get_node(ancestor)) - - # filtered_nodes = filter(lambda n: n.type in NODE_TYPE_ALLOWLIST, nodes) - # print(list(map(lambda n: n.dict(), filtered_nodes))) - # metadata = {} - # for node in nodes: - # if (node.type in ['txt2img', 'img2img',]) - # for field, value in node.dict().items(): - # if field not in ['type', 'id']: - # if field not in metadata: - # metadata[field] = value - - # print(ancestors) - # print(nodes) - # print(metadata) - - # for node in nodes: - # print(node.dict()) - - # print(nodes) - - return ResultWithSession( - result=result, - session=session, - ) - - def get( - self, result_id: str, result_type: ResultType - ) -> Union[ResultWithSession, None]: - """Retrieves a result by ID and type.""" - try: - self._lock.acquire() - self._cursor.execute( - """--sql - SELECT - results.id AS id, - results.result_type AS result_type, - results.node_id AS node_id, - results.session_id AS session_id, - results.data AS data, - graph_executions.item AS session - FROM results - JOIN graph_executions ON results.session_id = graph_executions.id - WHERE results.id = ? AND results.result_type = ? - """, - (result_id, result_type), - ) - - result_row = self._cursor.fetchone() - - if result_row is None: - return None - - column_names = list(map(lambda x: x[0], self._cursor.description)) - result_parsed = self._parse_joined_result(result_row, column_names) - finally: - self._lock.release() - - if not result_parsed: - return None - - return result_parsed - - def get_many( - self, - result_type: ResultType, - page: int = 0, - per_page: int = 10, - ) -> PaginatedResults[ResultWithSession]: - """Lists results of a given type.""" - try: - self._lock.acquire() - - self._cursor.execute( - f"""--sql - SELECT - results.id AS id, - results.result_type AS result_type, - results.node_id AS node_id, - results.session_id AS session_id, - results.data AS data, - graph_executions.item AS session - FROM results - JOIN graph_executions ON results.session_id = graph_executions.id - WHERE results.result_type = ? - LIMIT ? OFFSET ?; - """, - (result_type.value, per_page, page * per_page), - ) - - result_rows = self._cursor.fetchall() - column_names = list(map(lambda c: c[0], self._cursor.description)) - - result_parsed = [] - - for result_row in result_rows: - result_parsed.append( - self._parse_joined_result(result_row, column_names) - ) - - self._cursor.execute("""SELECT count(*) FROM results;""") - count = self._cursor.fetchone()[0] - finally: - self._lock.release() - - pageCount = int(count / per_page) + 1 - - return PaginatedResults[ResultWithSession]( - items=result_parsed, - page=page, - pages=pageCount, - per_page=per_page, - total=count, - ) - - def search( - self, - query: str, - page: int = 0, - per_page: int = 10, - ) -> PaginatedResults[ResultWithSession]: - """Finds results by query.""" - try: - self._lock.acquire() - self._cursor.execute( - """--sql - SELECT results.data, graph_executions.item - FROM results - JOIN graph_executions ON results.session_id = graph_executions.id - WHERE item LIKE ? - LIMIT ? OFFSET ?; - """, - (f"%{query}%", per_page, page * per_page), - ) - - result_rows = self._cursor.fetchall() - - items = list( - map( - lambda r: ResultWithSession( - result=parse_raw_as(Result, r[0]), - session=parse_raw_as(GraphExecutionState, r[1]), - ), - result_rows, - ) - ) - self._cursor.execute( - """--sql - SELECT count(*) FROM results WHERE item LIKE ?; - """, - (f"%{query}%",), - ) - count = self._cursor.fetchone()[0] - finally: - self._lock.release() - - pageCount = int(count / per_page) + 1 - - return PaginatedResults[ResultWithSession]( - items=items, page=page, pages=pageCount, per_page=per_page, total=count - ) - - def handle_graph_execution_state_change(self, session: GraphExecutionState) -> None: - """Updates the results table with the results from the session.""" - with self._conn as conn: - for node_id, result in session.results.items(): - # We'll only process 'image_output' or 'latents_output' - if result.type not in ["image_output", "latents_output"]: - continue - - # The id depends on the result type - if result.type == "image_output": - id = result.image.image_name - result_type = "image_output" - else: - id = result.latents.latents_name - result_type = "latents_output" - - # Insert the result into the results table, ignoring if it already exists - conn.execute( - """--sql - INSERT OR IGNORE INTO results (id, result_type, node_id, session_id, created_at, data) - VALUES (?, ?, ?, ?, ?, ?) - """, - ( - id, - result_type, - node_id, - session.id, - get_timestamp(), - result.json(), - ), - ) diff --git a/invokeai/app/services/util/create_enum_table.py b/invokeai/app/services/util/create_enum_table.py deleted file mode 100644 index 03cbfd6e90..0000000000 --- a/invokeai/app/services/util/create_enum_table.py +++ /dev/null @@ -1,39 +0,0 @@ -from enum import Enum -import sqlite3 -from typing import Type - - -def create_sql_values_string_from_string_enum(enum: Type[Enum]): - """ - Creates a string of the form "('value1'), ('value2'), ..., ('valueN')" from a StrEnum. - """ - - delimiter = ", " - values = [f"('{e.value}')" for e in enum] - return delimiter.join(values) - - -def create_enum_table( - enum: Type[Enum], - table_name: str, - primary_key_name: str, - cursor: sqlite3.Cursor, -): - """ - Creates and populates a table to be used as a functional enum. - """ - - values_string = create_sql_values_string_from_string_enum(enum) - - cursor.execute( - f"""--sql - CREATE TABLE IF NOT EXISTS {table_name} ( - {primary_key_name} TEXT PRIMARY KEY - ); - """ - ) - cursor.execute( - f"""--sql - INSERT OR IGNORE INTO {table_name} ({primary_key_name}) VALUES {values_string}; - """ - ) From 734b653a5f6e85966dc67f434e78bfabc10b37b2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 21 May 2023 23:03:52 +1000 Subject: [PATCH 18/72] fix(nodes): add base images router --- invokeai/app/api_app.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index dffb2ec139..aaef1d78be 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -15,7 +15,7 @@ from fastapi_events.middleware import EventHandlerASGIMiddleware from pydantic.schema import schema from .api.dependencies import ApiDependencies -from .api.routers import image_files, image_records, sessions, models +from .api.routers import image_files, image_records, sessions, models, images from .api.sockets import SocketIO from .invocations.baseinvocation import BaseInvocation from .services.config import InvokeAIAppConfig @@ -71,12 +71,13 @@ async def shutdown_event(): app.include_router(sessions.session_router, prefix="/api") -app.include_router(image_files.image_files_router, prefix="/api") - app.include_router(models.models_router, prefix="/api") +app.include_router(image_files.image_files_router, prefix="/api") + app.include_router(image_records.image_records_router, prefix="/api") +app.include_router(images.images_router, prefix="/api") # Build a custom OpenAPI to include all outputs # TODO: can outputs be included on metadata of invocation schemas somehow? From 60d25f105fd3a4e03a5986887a99d771183f9239 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 21 May 2023 23:28:18 +1000 Subject: [PATCH 19/72] fix(nodes): restore metadata traverser --- invokeai/app/services/metadata.py | 177 ++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) diff --git a/invokeai/app/services/metadata.py b/invokeai/app/services/metadata.py index 910b291593..40ec189cd0 100644 --- a/invokeai/app/services/metadata.py +++ b/invokeai/app/services/metadata.py @@ -116,3 +116,180 @@ class PngMetadataService(MetadataServiceBase): metadata = InvokeAIMetadata(session_id=session_id, node=node.dict()) return metadata + + +from enum import Enum + +from abc import ABC, abstractmethod +import json +import sqlite3 +from threading import Lock +from typing import Any, Union + +import networkx as nx + +from pydantic import BaseModel, Field, parse_obj_as, parse_raw_as +from invokeai.app.invocations.image import ImageOutput +from invokeai.app.services.graph import Edge, GraphExecutionState +from invokeai.app.invocations.latent import LatentsOutput +from invokeai.app.services.item_storage import PaginatedResults +from invokeai.app.util.misc import get_timestamp + + +class ResultType(str, Enum): + image_output = "image_output" + latents_output = "latents_output" + + +class Result(BaseModel): + """A session result""" + + id: str = Field(description="Result ID") + session_id: str = Field(description="Session ID") + node_id: str = Field(description="Node ID") + data: Union[LatentsOutput, ImageOutput] = Field(description="The result data") + + +class ResultWithSession(BaseModel): + """A result with its session""" + + result: Result = Field(description="The result") + session: GraphExecutionState = Field(description="The session") + + +# # Create a directed graph +# from typing import Any, TypedDict, Union +# from networkx import DiGraph +# import networkx as nx +# import json + + +# # We need to use a loose class for nodes to allow for graceful parsing - we cannot use the stricter +# # model used by the system, because we may be a graph in an old format. We can, however, use the +# # Edge model, because the edge format does not change. +# class LooseGraph(BaseModel): +# id: str +# nodes: dict[str, dict[str, Any]] +# edges: list[Edge] + + +# # An intermediate type used during parsing +# class NearestAncestor(TypedDict): +# node_id: str +# metadata: dict[str, Any] + + +# # The ancestor types that contain the core metadata +# ANCESTOR_TYPES = ['t2l', 'l2l'] + +# # The core metadata parameters in the ancestor types +# ANCESTOR_PARAMS = ['steps', 'model', 'cfg_scale', 'scheduler', 'strength'] + +# # The core metadata parameters in the noise node +# NOISE_FIELDS = ['seed', 'width', 'height'] + +# # Find nearest t2l or l2l ancestor from a given l2i node +# def find_nearest_ancestor(G: DiGraph, node_id: str) -> Union[NearestAncestor, None]: +# """Returns metadata for the nearest ancestor of a given node. + +# Parameters: +# G (DiGraph): A directed graph. +# node_id (str): The ID of the starting node. + +# Returns: +# NearestAncestor | None: An object with the ID and metadata of the nearest ancestor. +# """ + +# # Retrieve the node from the graph +# node = G.nodes[node_id] + +# # If the node type is one of the core metadata node types, gather necessary metadata and return +# if node.get('type') in ANCESTOR_TYPES: +# parsed_metadata = {param: val for param, val in node.items() if param in ANCESTOR_PARAMS} +# return NearestAncestor(node_id=node_id, metadata=parsed_metadata) + + +# # Else, look for the ancestor in the predecessor nodes +# for predecessor in G.predecessors(node_id): +# result = find_nearest_ancestor(G, predecessor) +# if result: +# return result + +# # If there are no valid ancestors, return None +# return None + + +# def get_additional_metadata(graph: LooseGraph, node_id: str) -> Union[dict[str, Any], None]: +# """Collects additional metadata from nodes connected to a given node. + +# Parameters: +# graph (LooseGraph): The graph. +# node_id (str): The ID of the node. + +# Returns: +# dict | None: A dictionary containing additional metadata. +# """ + +# metadata = {} + +# # Iterate over all edges in the graph +# for edge in graph.edges: +# dest_node_id = edge.destination.node_id +# dest_field = edge.destination.field +# source_node = graph.nodes[edge.source.node_id] + +# # If the destination node ID matches the given node ID, gather necessary metadata +# if dest_node_id == node_id: +# # If the destination field is 'positive_conditioning', add the 'prompt' from the source node +# if dest_field == 'positive_conditioning': +# metadata['positive_conditioning'] = source_node.get('prompt') +# # If the destination field is 'negative_conditioning', add the 'prompt' from the source node +# if dest_field == 'negative_conditioning': +# metadata['negative_conditioning'] = source_node.get('prompt') +# # If the destination field is 'noise', add the core noise fields from the source node +# if dest_field == 'noise': +# for field in NOISE_FIELDS: +# metadata[field] = source_node.get(field) +# return metadata + +# def build_core_metadata(graph_raw: str, node_id: str) -> Union[dict, None]: +# """Builds the core metadata for a given node. + +# Parameters: +# graph_raw (str): The graph structure as a raw string. +# node_id (str): The ID of the node. + +# Returns: +# dict | None: A dictionary containing core metadata. +# """ + +# # Create a directed graph to facilitate traversal +# G = nx.DiGraph() + +# # Convert the raw graph string into a JSON object +# graph = parse_obj_as(LooseGraph, graph_raw) + +# # Add nodes and edges to the graph +# for node_id, node_data in graph.nodes.items(): +# G.add_node(node_id, **node_data) +# for edge in graph.edges: +# G.add_edge(edge.source.node_id, edge.destination.node_id) + +# # Find the nearest ancestor of the given node +# ancestor = find_nearest_ancestor(G, node_id) + +# # If no ancestor was found, return None +# if ancestor is None: +# return None + +# metadata = ancestor['metadata'] +# ancestor_id = ancestor['node_id'] + +# # Get additional metadata related to the ancestor +# addl_metadata = get_additional_metadata(graph, ancestor_id) + +# # If additional metadata was found, add it to the main metadata +# if addl_metadata is not None: +# metadata.update(addl_metadata) + +# return metadata From 96653eebb68ab65f827805b74d66dee7439fa5b3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 22 May 2023 11:17:33 +1000 Subject: [PATCH 20/72] build(ui): do not export schemas on api client generation --- invokeai/frontend/web/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 317929c6a4..13b8d78bf7 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -23,8 +23,8 @@ "dev": "concurrently \"vite dev\" \"yarn run theme:watch\"", "dev:host": "concurrently \"vite dev --host\" \"yarn run theme:watch\"", "build": "yarn run lint && vite build", - "api:web": "openapi -i http://localhost:9090/openapi.json -o src/services/api --client axios --useOptions --useUnionTypes --exportSchemas true --indent 2 --request src/services/fixtures/request.ts", - "api:file": "openapi -i src/services/fixtures/openapi.json -o src/services/api --client axios --useOptions --useUnionTypes --exportSchemas true --indent 2 --request src/services/fixtures/request.ts", + "api:web": "openapi -i http://localhost:9090/openapi.json -o src/services/api --client axios --useOptions --useUnionTypes --indent 2 --request src/services/fixtures/request.ts", + "api:file": "openapi -i src/services/fixtures/openapi.json -o src/services/api --client axios --useOptions --useUnionTypes --indent 2 --request src/services/fixtures/request.ts", "preview": "vite preview", "lint:madge": "madge --circular src/main.tsx", "lint:eslint": "eslint --max-warnings=0 .", From b77ccfaf32c30eb6b14e5014f36fddf52e5564db Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 22 May 2023 11:18:10 +1000 Subject: [PATCH 21/72] chore(ui): regen api client --- .../frontend/web/src/services/api/index.ts | 83 +----- .../web/src/services/api/models/Graph.ts | 2 +- .../api/models/GraphExecutionState.ts | 2 +- .../src/services/api/models/ImageCategory.ts | 8 + .../src/services/api/models/ImageResponse.ts | 33 --- .../api/models/ImageResponseMetadata.ts | 28 -- .../web/src/services/api/models/ImageType.ts | 4 +- .../services/api/models/InvokeAIMetadata.ts | 13 - .../services/api/models/MetadataColorField.ts | 11 - .../services/api/models/MetadataImageField.ts | 11 - .../api/models/MetadataLatentsField.ts | 8 - .../models/PaginatedResults_ImageResponse_.ts | 32 --- .../services/api/schemas/$AddInvocation.ts | 24 -- .../services/api/schemas/$BlurInvocation.ts | 30 --- .../api/schemas/$Body_upload_image.ts | 12 - .../services/api/schemas/$CkptModelInfo.ts | 37 --- .../api/schemas/$CollectInvocation.ts | 28 -- .../api/schemas/$CollectInvocationOutput.ts | 20 -- .../src/services/api/schemas/$ColorField.ts | 31 --- .../services/api/schemas/$CompelInvocation.ts | 24 -- .../src/services/api/schemas/$CompelOutput.ts | 18 -- .../api/schemas/$ConditioningField.ts | 12 - .../api/schemas/$CreateModelRequest.ts | 22 -- .../api/schemas/$CropImageInvocation.ts | 39 --- .../api/schemas/$CvInpaintInvocation.ts | 30 --- .../api/schemas/$DiffusersModelInfo.ts | 29 -- .../services/api/schemas/$DivideInvocation.ts | 24 -- .../web/src/services/api/schemas/$Edge.ts | 23 -- .../services/api/schemas/$EdgeConnection.ts | 17 -- .../web/src/services/api/schemas/$Graph.ts | 96 ------- .../api/schemas/$GraphExecutionState.ts | 97 ------- .../services/api/schemas/$GraphInvocation.ts | 24 -- .../api/schemas/$GraphInvocationOutput.ts | 12 - .../api/schemas/$HTTPValidationError.ts | 13 - .../src/services/api/schemas/$ImageField.ts | 21 -- .../src/services/api/schemas/$ImageOutput.ts | 30 --- .../services/api/schemas/$ImageResponse.ts | 39 --- .../api/schemas/$ImageResponseMetadata.ts | 30 --- .../api/schemas/$ImageToImageInvocation.ts | 67 ----- .../api/schemas/$ImageToLatentsInvocation.ts | 27 -- .../src/services/api/schemas/$ImageType.ts | 6 - .../api/schemas/$InfillColorInvocation.ts | 30 --- .../schemas/$InfillPatchMatchInvocation.ts | 23 -- .../api/schemas/$InfillTileInvocation.ts | 33 --- .../api/schemas/$InpaintInvocation.ts | 123 --------- .../api/schemas/$IntCollectionOutput.ts | 17 -- .../src/services/api/schemas/$IntOutput.ts | 15 -- .../api/schemas/$InverseLerpInvocation.ts | 33 --- .../services/api/schemas/$InvokeAIMetadata.ts | 31 --- .../api/schemas/$IterateInvocation.ts | 28 -- .../api/schemas/$IterateInvocationOutput.ts | 18 -- .../src/services/api/schemas/$LatentsField.ts | 13 - .../services/api/schemas/$LatentsOutput.ts | 28 -- .../api/schemas/$LatentsToImageInvocation.ts | 27 -- .../schemas/$LatentsToLatentsInvocation.ts | 71 ----- .../services/api/schemas/$LerpInvocation.ts | 33 --- .../api/schemas/$LoadImageInvocation.ts | 29 -- .../api/schemas/$MaskFromAlphaInvocation.ts | 27 -- .../src/services/api/schemas/$MaskOutput.ts | 20 -- .../api/schemas/$MetadataColorField.ts | 23 -- .../api/schemas/$MetadataImageField.ts | 15 -- .../api/schemas/$MetadataLatentsField.ts | 11 - .../src/services/api/schemas/$ModelsList.ts | 19 -- .../api/schemas/$MultiplyInvocation.ts | 24 -- .../services/api/schemas/$NoiseInvocation.ts | 31 --- .../src/services/api/schemas/$NoiseOutput.ts | 28 -- .../$PaginatedResults_GraphExecutionState_.ts | 35 --- .../$PaginatedResults_ImageResponse_.ts | 35 --- .../api/schemas/$ParamIntInvocation.ts | 20 -- .../api/schemas/$PasteImageInvocation.ts | 45 ---- .../src/services/api/schemas/$PromptOutput.ts | 17 -- .../api/schemas/$RandomIntInvocation.ts | 24 -- .../api/schemas/$RandomRangeInvocation.ts | 33 --- .../services/api/schemas/$RangeInvocation.ts | 28 -- .../api/schemas/$ResizeLatentsInvocation.ts | 44 --- .../api/schemas/$RestoreFaceInvocation.ts | 28 -- .../api/schemas/$ScaleLatentsInvocation.ts | 35 --- .../api/schemas/$ShowImageInvocation.ts | 23 -- .../api/schemas/$SubtractInvocation.ts | 24 -- .../api/schemas/$TextToImageInvocation.ts | 51 ---- .../api/schemas/$TextToLatentsInvocation.ts | 60 ----- .../api/schemas/$UpscaleInvocation.ts | 31 --- .../web/src/services/api/schemas/$VaeRepo.ts | 20 -- .../services/api/schemas/$ValidationError.ts | 27 -- .../src/services/api/services/FilesService.ts | 76 ++++++ .../services/api/services/ImagesService.ts | 250 ++++++++++-------- .../services/api/services/RecordsService.ts | 89 +++++++ .../services/api/services/SessionsService.ts | 4 +- 88 files changed, 326 insertions(+), 2540 deletions(-) create mode 100644 invokeai/frontend/web/src/services/api/models/ImageCategory.ts delete mode 100644 invokeai/frontend/web/src/services/api/models/ImageResponse.ts delete mode 100644 invokeai/frontend/web/src/services/api/models/ImageResponseMetadata.ts delete mode 100644 invokeai/frontend/web/src/services/api/models/InvokeAIMetadata.ts delete mode 100644 invokeai/frontend/web/src/services/api/models/MetadataColorField.ts delete mode 100644 invokeai/frontend/web/src/services/api/models/MetadataImageField.ts delete mode 100644 invokeai/frontend/web/src/services/api/models/MetadataLatentsField.ts delete mode 100644 invokeai/frontend/web/src/services/api/models/PaginatedResults_ImageResponse_.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$AddInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$BlurInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$Body_upload_image.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$CkptModelInfo.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$CollectInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$CollectInvocationOutput.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$ColorField.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$CompelInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$CompelOutput.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$ConditioningField.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$CreateModelRequest.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$CropImageInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$CvInpaintInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$DiffusersModelInfo.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$DivideInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$Edge.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$EdgeConnection.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$Graph.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$GraphExecutionState.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$GraphInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$GraphInvocationOutput.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$HTTPValidationError.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$ImageField.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$ImageOutput.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$ImageResponse.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$ImageResponseMetadata.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$ImageToImageInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$ImageToLatentsInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$ImageType.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$InfillColorInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$InfillPatchMatchInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$InfillTileInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$InpaintInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$IntCollectionOutput.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$IntOutput.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$InverseLerpInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$InvokeAIMetadata.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$IterateInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$IterateInvocationOutput.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$LatentsField.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$LatentsOutput.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$LatentsToImageInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$LerpInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$LoadImageInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$MaskFromAlphaInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$MaskOutput.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$MetadataColorField.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$MetadataImageField.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$MetadataLatentsField.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$ModelsList.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$MultiplyInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$NoiseInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$NoiseOutput.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_GraphExecutionState_.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_ImageResponse_.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$ParamIntInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$PasteImageInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$PromptOutput.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$RandomIntInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$RandomRangeInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$RangeInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$ResizeLatentsInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$RestoreFaceInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$ScaleLatentsInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$ShowImageInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$SubtractInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$TextToImageInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$UpscaleInvocation.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$VaeRepo.ts delete mode 100644 invokeai/frontend/web/src/services/api/schemas/$ValidationError.ts create mode 100644 invokeai/frontend/web/src/services/api/services/FilesService.ts create mode 100644 invokeai/frontend/web/src/services/api/services/RecordsService.ts diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index 3b89d2a40c..ada9153de5 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -28,10 +28,9 @@ export type { GraphExecutionState } from './models/GraphExecutionState'; export type { GraphInvocation } from './models/GraphInvocation'; export type { GraphInvocationOutput } from './models/GraphInvocationOutput'; export type { HTTPValidationError } from './models/HTTPValidationError'; +export type { ImageCategory } from './models/ImageCategory'; export type { ImageField } from './models/ImageField'; export type { ImageOutput } from './models/ImageOutput'; -export type { ImageResponse } from './models/ImageResponse'; -export type { ImageResponseMetadata } from './models/ImageResponseMetadata'; export type { ImageToImageInvocation } from './models/ImageToImageInvocation'; export type { ImageToLatentsInvocation } from './models/ImageToLatentsInvocation'; export type { ImageType } from './models/ImageType'; @@ -42,7 +41,6 @@ export type { InpaintInvocation } from './models/InpaintInvocation'; export type { IntCollectionOutput } from './models/IntCollectionOutput'; export type { IntOutput } from './models/IntOutput'; export type { InverseLerpInvocation } from './models/InverseLerpInvocation'; -export type { InvokeAIMetadata } from './models/InvokeAIMetadata'; export type { IterateInvocation } from './models/IterateInvocation'; export type { IterateInvocationOutput } from './models/IterateInvocationOutput'; export type { LatentsField } from './models/LatentsField'; @@ -53,15 +51,11 @@ export type { LerpInvocation } from './models/LerpInvocation'; export type { LoadImageInvocation } from './models/LoadImageInvocation'; export type { MaskFromAlphaInvocation } from './models/MaskFromAlphaInvocation'; export type { MaskOutput } from './models/MaskOutput'; -export type { MetadataColorField } from './models/MetadataColorField'; -export type { MetadataImageField } from './models/MetadataImageField'; -export type { MetadataLatentsField } from './models/MetadataLatentsField'; export type { ModelsList } from './models/ModelsList'; export type { MultiplyInvocation } from './models/MultiplyInvocation'; export type { NoiseInvocation } from './models/NoiseInvocation'; export type { NoiseOutput } from './models/NoiseOutput'; export type { PaginatedResults_GraphExecutionState_ } from './models/PaginatedResults_GraphExecutionState_'; -export type { PaginatedResults_ImageResponse_ } from './models/PaginatedResults_ImageResponse_'; export type { ParamIntInvocation } from './models/ParamIntInvocation'; export type { PasteImageInvocation } from './models/PasteImageInvocation'; export type { PromptOutput } from './models/PromptOutput'; @@ -79,79 +73,8 @@ export type { UpscaleInvocation } from './models/UpscaleInvocation'; export type { VaeRepo } from './models/VaeRepo'; export type { ValidationError } from './models/ValidationError'; -export { $AddInvocation } from './schemas/$AddInvocation'; -export { $BlurInvocation } from './schemas/$BlurInvocation'; -export { $Body_upload_image } from './schemas/$Body_upload_image'; -export { $CkptModelInfo } from './schemas/$CkptModelInfo'; -export { $CollectInvocation } from './schemas/$CollectInvocation'; -export { $CollectInvocationOutput } from './schemas/$CollectInvocationOutput'; -export { $ColorField } from './schemas/$ColorField'; -export { $CompelInvocation } from './schemas/$CompelInvocation'; -export { $CompelOutput } from './schemas/$CompelOutput'; -export { $ConditioningField } from './schemas/$ConditioningField'; -export { $CreateModelRequest } from './schemas/$CreateModelRequest'; -export { $CropImageInvocation } from './schemas/$CropImageInvocation'; -export { $CvInpaintInvocation } from './schemas/$CvInpaintInvocation'; -export { $DiffusersModelInfo } from './schemas/$DiffusersModelInfo'; -export { $DivideInvocation } from './schemas/$DivideInvocation'; -export { $Edge } from './schemas/$Edge'; -export { $EdgeConnection } from './schemas/$EdgeConnection'; -export { $Graph } from './schemas/$Graph'; -export { $GraphExecutionState } from './schemas/$GraphExecutionState'; -export { $GraphInvocation } from './schemas/$GraphInvocation'; -export { $GraphInvocationOutput } from './schemas/$GraphInvocationOutput'; -export { $HTTPValidationError } from './schemas/$HTTPValidationError'; -export { $ImageField } from './schemas/$ImageField'; -export { $ImageOutput } from './schemas/$ImageOutput'; -export { $ImageResponse } from './schemas/$ImageResponse'; -export { $ImageResponseMetadata } from './schemas/$ImageResponseMetadata'; -export { $ImageToImageInvocation } from './schemas/$ImageToImageInvocation'; -export { $ImageToLatentsInvocation } from './schemas/$ImageToLatentsInvocation'; -export { $ImageType } from './schemas/$ImageType'; -export { $InfillColorInvocation } from './schemas/$InfillColorInvocation'; -export { $InfillPatchMatchInvocation } from './schemas/$InfillPatchMatchInvocation'; -export { $InfillTileInvocation } from './schemas/$InfillTileInvocation'; -export { $InpaintInvocation } from './schemas/$InpaintInvocation'; -export { $IntCollectionOutput } from './schemas/$IntCollectionOutput'; -export { $IntOutput } from './schemas/$IntOutput'; -export { $InverseLerpInvocation } from './schemas/$InverseLerpInvocation'; -export { $InvokeAIMetadata } from './schemas/$InvokeAIMetadata'; -export { $IterateInvocation } from './schemas/$IterateInvocation'; -export { $IterateInvocationOutput } from './schemas/$IterateInvocationOutput'; -export { $LatentsField } from './schemas/$LatentsField'; -export { $LatentsOutput } from './schemas/$LatentsOutput'; -export { $LatentsToImageInvocation } from './schemas/$LatentsToImageInvocation'; -export { $LatentsToLatentsInvocation } from './schemas/$LatentsToLatentsInvocation'; -export { $LerpInvocation } from './schemas/$LerpInvocation'; -export { $LoadImageInvocation } from './schemas/$LoadImageInvocation'; -export { $MaskFromAlphaInvocation } from './schemas/$MaskFromAlphaInvocation'; -export { $MaskOutput } from './schemas/$MaskOutput'; -export { $MetadataColorField } from './schemas/$MetadataColorField'; -export { $MetadataImageField } from './schemas/$MetadataImageField'; -export { $MetadataLatentsField } from './schemas/$MetadataLatentsField'; -export { $ModelsList } from './schemas/$ModelsList'; -export { $MultiplyInvocation } from './schemas/$MultiplyInvocation'; -export { $NoiseInvocation } from './schemas/$NoiseInvocation'; -export { $NoiseOutput } from './schemas/$NoiseOutput'; -export { $PaginatedResults_GraphExecutionState_ } from './schemas/$PaginatedResults_GraphExecutionState_'; -export { $PaginatedResults_ImageResponse_ } from './schemas/$PaginatedResults_ImageResponse_'; -export { $ParamIntInvocation } from './schemas/$ParamIntInvocation'; -export { $PasteImageInvocation } from './schemas/$PasteImageInvocation'; -export { $PromptOutput } from './schemas/$PromptOutput'; -export { $RandomIntInvocation } from './schemas/$RandomIntInvocation'; -export { $RandomRangeInvocation } from './schemas/$RandomRangeInvocation'; -export { $RangeInvocation } from './schemas/$RangeInvocation'; -export { $ResizeLatentsInvocation } from './schemas/$ResizeLatentsInvocation'; -export { $RestoreFaceInvocation } from './schemas/$RestoreFaceInvocation'; -export { $ScaleLatentsInvocation } from './schemas/$ScaleLatentsInvocation'; -export { $ShowImageInvocation } from './schemas/$ShowImageInvocation'; -export { $SubtractInvocation } from './schemas/$SubtractInvocation'; -export { $TextToImageInvocation } from './schemas/$TextToImageInvocation'; -export { $TextToLatentsInvocation } from './schemas/$TextToLatentsInvocation'; -export { $UpscaleInvocation } from './schemas/$UpscaleInvocation'; -export { $VaeRepo } from './schemas/$VaeRepo'; -export { $ValidationError } from './schemas/$ValidationError'; - +export { FilesService } from './services/FilesService'; export { ImagesService } from './services/ImagesService'; export { ModelsService } from './services/ModelsService'; +export { RecordsService } from './services/RecordsService'; export { SessionsService } from './services/SessionsService'; diff --git a/invokeai/frontend/web/src/services/api/models/Graph.ts b/invokeai/frontend/web/src/services/api/models/Graph.ts index 4e4f92e6f4..4399725680 100644 --- a/invokeai/frontend/web/src/services/api/models/Graph.ts +++ b/invokeai/frontend/web/src/services/api/models/Graph.ts @@ -48,7 +48,7 @@ export type Graph = { /** * The nodes in this graph */ - nodes?: Record; + nodes?: Record; /** * The connections between nodes and their fields in this graph */ diff --git a/invokeai/frontend/web/src/services/api/models/GraphExecutionState.ts b/invokeai/frontend/web/src/services/api/models/GraphExecutionState.ts index 2e54601e7c..8c2eb05657 100644 --- a/invokeai/frontend/web/src/services/api/models/GraphExecutionState.ts +++ b/invokeai/frontend/web/src/services/api/models/GraphExecutionState.ts @@ -42,7 +42,7 @@ export type GraphExecutionState = { /** * The results of node executions */ - results: Record; + results: Record; /** * Errors raised when executing nodes */ diff --git a/invokeai/frontend/web/src/services/api/models/ImageCategory.ts b/invokeai/frontend/web/src/services/api/models/ImageCategory.ts new file mode 100644 index 0000000000..38955a0de1 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageCategory.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * The category of an image. Use ImageCategory.OTHER for non-default categories. + */ +export type ImageCategory = 'image' | 'control_image' | 'other'; diff --git a/invokeai/frontend/web/src/services/api/models/ImageResponse.ts b/invokeai/frontend/web/src/services/api/models/ImageResponse.ts deleted file mode 100644 index 688f29bfef..0000000000 --- a/invokeai/frontend/web/src/services/api/models/ImageResponse.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ - -import type { ImageResponseMetadata } from './ImageResponseMetadata'; -import type { ImageType } from './ImageType'; - -/** - * The response type for images - */ -export type ImageResponse = { - /** - * The type of the image - */ - image_type: ImageType; - /** - * The name of the image - */ - image_name: string; - /** - * The url of the image - */ - image_url: string; - /** - * The url of the image's thumbnail - */ - thumbnail_url: string; - /** - * The image's metadata - */ - metadata: ImageResponseMetadata; -}; - diff --git a/invokeai/frontend/web/src/services/api/models/ImageResponseMetadata.ts b/invokeai/frontend/web/src/services/api/models/ImageResponseMetadata.ts deleted file mode 100644 index 50acf364df..0000000000 --- a/invokeai/frontend/web/src/services/api/models/ImageResponseMetadata.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ - -import type { InvokeAIMetadata } from './InvokeAIMetadata'; - -/** - * An image's metadata. Used only in HTTP responses. - */ -export type ImageResponseMetadata = { - /** - * The creation timestamp of the image - */ - created: number; - /** - * The width of the image in pixels - */ - width: number; - /** - * The height of the image in pixels - */ - height: number; - /** - * The image's InvokeAI-specific metadata - */ - invokeai?: InvokeAIMetadata; -}; - diff --git a/invokeai/frontend/web/src/services/api/models/ImageType.ts b/invokeai/frontend/web/src/services/api/models/ImageType.ts index b6468a1ed0..bba9134e63 100644 --- a/invokeai/frontend/web/src/services/api/models/ImageType.ts +++ b/invokeai/frontend/web/src/services/api/models/ImageType.ts @@ -3,6 +3,6 @@ /* eslint-disable */ /** - * An enumeration. + * The type of an image. */ -export type ImageType = 'results' | 'intermediates' | 'uploads'; +export type ImageType = 'results' | 'uploads' | 'intermediates'; diff --git a/invokeai/frontend/web/src/services/api/models/InvokeAIMetadata.ts b/invokeai/frontend/web/src/services/api/models/InvokeAIMetadata.ts deleted file mode 100644 index ba80199f9a..0000000000 --- a/invokeai/frontend/web/src/services/api/models/InvokeAIMetadata.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ - -import type { MetadataColorField } from './MetadataColorField'; -import type { MetadataImageField } from './MetadataImageField'; -import type { MetadataLatentsField } from './MetadataLatentsField'; - -export type InvokeAIMetadata = { - session_id?: string; - node?: Record; -}; - diff --git a/invokeai/frontend/web/src/services/api/models/MetadataColorField.ts b/invokeai/frontend/web/src/services/api/models/MetadataColorField.ts deleted file mode 100644 index 897a0123dd..0000000000 --- a/invokeai/frontend/web/src/services/api/models/MetadataColorField.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ - -export type MetadataColorField = { - 'r': number; - 'g': number; - 'b': number; - 'a': number; -}; - diff --git a/invokeai/frontend/web/src/services/api/models/MetadataImageField.ts b/invokeai/frontend/web/src/services/api/models/MetadataImageField.ts deleted file mode 100644 index 0dcae1ccee..0000000000 --- a/invokeai/frontend/web/src/services/api/models/MetadataImageField.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ - -import type { ImageType } from './ImageType'; - -export type MetadataImageField = { - image_type: ImageType; - image_name: string; -}; - diff --git a/invokeai/frontend/web/src/services/api/models/MetadataLatentsField.ts b/invokeai/frontend/web/src/services/api/models/MetadataLatentsField.ts deleted file mode 100644 index 30b6aebeba..0000000000 --- a/invokeai/frontend/web/src/services/api/models/MetadataLatentsField.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ - -export type MetadataLatentsField = { - latents_name: string; -}; - diff --git a/invokeai/frontend/web/src/services/api/models/PaginatedResults_ImageResponse_.ts b/invokeai/frontend/web/src/services/api/models/PaginatedResults_ImageResponse_.ts deleted file mode 100644 index 214c7c2f57..0000000000 --- a/invokeai/frontend/web/src/services/api/models/PaginatedResults_ImageResponse_.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ - -import type { ImageResponse } from './ImageResponse'; - -/** - * Paginated results - */ -export type PaginatedResults_ImageResponse_ = { - /** - * Items - */ - items: Array; - /** - * Current Page - */ - page: number; - /** - * Total number of pages - */ - pages: number; - /** - * Number of items per page - */ - per_page: number; - /** - * Total number of items in result - */ - total: number; -}; - diff --git a/invokeai/frontend/web/src/services/api/schemas/$AddInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$AddInvocation.ts deleted file mode 100644 index 3aa74aef3e..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$AddInvocation.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $AddInvocation = { - description: `Adds two numbers`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - 'a': { - type: 'number', - description: `The first number`, - }, - 'b': { - type: 'number', - description: `The second number`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$BlurInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$BlurInvocation.ts deleted file mode 100644 index 69f5438583..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$BlurInvocation.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $BlurInvocation = { - description: `Blurs an image`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - image: { - type: 'all-of', - description: `The image to blur`, - contains: [{ - type: 'ImageField', - }], - }, - radius: { - type: 'number', - description: `The blur radius`, - }, - blur_type: { - type: 'Enum', - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$Body_upload_image.ts b/invokeai/frontend/web/src/services/api/schemas/$Body_upload_image.ts deleted file mode 100644 index 7d6adf5a84..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$Body_upload_image.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $Body_upload_image = { - properties: { - file: { - type: 'binary', - isRequired: true, - format: 'binary', - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CkptModelInfo.ts b/invokeai/frontend/web/src/services/api/schemas/$CkptModelInfo.ts deleted file mode 100644 index aeac9a4200..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$CkptModelInfo.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $CkptModelInfo = { - properties: { - description: { - type: 'string', - description: `A description of the model`, - }, - format: { - type: 'Enum', - }, - config: { - type: 'string', - description: `The path to the model config`, - isRequired: true, - }, - weights: { - type: 'string', - description: `The path to the model weights`, - isRequired: true, - }, - vae: { - type: 'string', - description: `The path to the model VAE`, - isRequired: true, - }, - width: { - type: 'number', - description: `The width of the model`, - }, - height: { - type: 'number', - description: `The height of the model`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CollectInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$CollectInvocation.ts deleted file mode 100644 index 1ab0bb0b9b..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$CollectInvocation.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $CollectInvocation = { - description: `Collects values into a collection`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - item: { - description: `The item to collect (all inputs must be of the same type)`, - properties: { - }, - }, - collection: { - type: 'array', - contains: { - properties: { - }, - }, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CollectInvocationOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$CollectInvocationOutput.ts deleted file mode 100644 index 598ad94eff..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$CollectInvocationOutput.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $CollectInvocationOutput = { - description: `Base class for all invocation outputs`, - properties: { - type: { - type: 'Enum', - isRequired: true, - }, - collection: { - type: 'array', - contains: { - properties: { - }, - }, - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ColorField.ts b/invokeai/frontend/web/src/services/api/schemas/$ColorField.ts deleted file mode 100644 index e38788dae2..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$ColorField.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $ColorField = { - properties: { - 'r': { - type: 'number', - description: `The red component`, - isRequired: true, - maximum: 255, - }, - 'g': { - type: 'number', - description: `The green component`, - isRequired: true, - maximum: 255, - }, - 'b': { - type: 'number', - description: `The blue component`, - isRequired: true, - maximum: 255, - }, - 'a': { - type: 'number', - description: `The alpha component`, - isRequired: true, - maximum: 255, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CompelInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$CompelInvocation.ts deleted file mode 100644 index 61139412ad..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$CompelInvocation.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $CompelInvocation = { - description: `Parse prompt using compel package to conditioning.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - prompt: { - type: 'string', - description: `Prompt`, - }, - model: { - type: 'string', - description: `Model to use`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CompelOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$CompelOutput.ts deleted file mode 100644 index 03a429040a..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$CompelOutput.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $CompelOutput = { - description: `Compel parser output`, - properties: { - type: { - type: 'Enum', - }, - conditioning: { - type: 'all-of', - description: `Conditioning`, - contains: [{ - type: 'ConditioningField', - }], - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ConditioningField.ts b/invokeai/frontend/web/src/services/api/schemas/$ConditioningField.ts deleted file mode 100644 index fcbd449af2..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$ConditioningField.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $ConditioningField = { - properties: { - conditioning_name: { - type: 'string', - description: `The name of conditioning data`, - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CreateModelRequest.ts b/invokeai/frontend/web/src/services/api/schemas/$CreateModelRequest.ts deleted file mode 100644 index 32593059d8..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$CreateModelRequest.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $CreateModelRequest = { - properties: { - name: { - type: 'string', - description: `The name of the model`, - isRequired: true, - }, - info: { - type: 'one-of', - description: `The model info`, - contains: [{ - type: 'CkptModelInfo', - }, { - type: 'DiffusersModelInfo', - }], - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CropImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$CropImageInvocation.ts deleted file mode 100644 index f279efe286..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$CropImageInvocation.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $CropImageInvocation = { - description: `Crops an image to a specified box. The box can be outside of the image.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - image: { - type: 'all-of', - description: `The image to crop`, - contains: [{ - type: 'ImageField', - }], - }, - 'x': { - type: 'number', - description: `The left x coordinate of the crop rectangle`, - }, - 'y': { - type: 'number', - description: `The top y coordinate of the crop rectangle`, - }, - width: { - type: 'number', - description: `The width of the crop rectangle`, - }, - height: { - type: 'number', - description: `The height of the crop rectangle`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CvInpaintInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$CvInpaintInvocation.ts deleted file mode 100644 index 959484f3ed..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$CvInpaintInvocation.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $CvInpaintInvocation = { - description: `Simple inpaint using opencv.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - image: { - type: 'all-of', - description: `The image to inpaint`, - contains: [{ - type: 'ImageField', - }], - }, - mask: { - type: 'all-of', - description: `The mask to use when inpainting`, - contains: [{ - type: 'ImageField', - }], - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$DiffusersModelInfo.ts b/invokeai/frontend/web/src/services/api/schemas/$DiffusersModelInfo.ts deleted file mode 100644 index b2e895b498..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$DiffusersModelInfo.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $DiffusersModelInfo = { - properties: { - description: { - type: 'string', - description: `A description of the model`, - }, - format: { - type: 'Enum', - }, - vae: { - type: 'all-of', - description: `The VAE repo to use for this model`, - contains: [{ - type: 'VaeRepo', - }], - }, - repo_id: { - type: 'string', - description: `The repo ID to use for this model`, - }, - path: { - type: 'string', - description: `The path to the model`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$DivideInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$DivideInvocation.ts deleted file mode 100644 index a6d5998591..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$DivideInvocation.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $DivideInvocation = { - description: `Divides two numbers`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - 'a': { - type: 'number', - description: `The first number`, - }, - 'b': { - type: 'number', - description: `The second number`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$Edge.ts b/invokeai/frontend/web/src/services/api/schemas/$Edge.ts deleted file mode 100644 index d7e7028bf1..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$Edge.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $Edge = { - properties: { - source: { - type: 'all-of', - description: `The connection for the edge's from node and field`, - contains: [{ - type: 'EdgeConnection', - }], - isRequired: true, - }, - destination: { - type: 'all-of', - description: `The connection for the edge's to node and field`, - contains: [{ - type: 'EdgeConnection', - }], - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$EdgeConnection.ts b/invokeai/frontend/web/src/services/api/schemas/$EdgeConnection.ts deleted file mode 100644 index a3f325888e..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$EdgeConnection.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $EdgeConnection = { - properties: { - node_id: { - type: 'string', - description: `The id of the node for this edge connection`, - isRequired: true, - }, - field: { - type: 'string', - description: `The field for this connection`, - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$Graph.ts b/invokeai/frontend/web/src/services/api/schemas/$Graph.ts deleted file mode 100644 index 397d753a52..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$Graph.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $Graph = { - properties: { - id: { - type: 'string', - description: `The id of this graph`, - }, - nodes: { - type: 'dictionary', - contains: { - type: 'one-of', - contains: [{ - type: 'LoadImageInvocation', - }, { - type: 'ShowImageInvocation', - }, { - type: 'CropImageInvocation', - }, { - type: 'PasteImageInvocation', - }, { - type: 'MaskFromAlphaInvocation', - }, { - type: 'BlurInvocation', - }, { - type: 'LerpInvocation', - }, { - type: 'InverseLerpInvocation', - }, { - type: 'CompelInvocation', - }, { - type: 'NoiseInvocation', - }, { - type: 'TextToLatentsInvocation', - }, { - type: 'LatentsToImageInvocation', - }, { - type: 'ResizeLatentsInvocation', - }, { - type: 'ScaleLatentsInvocation', - }, { - type: 'ImageToLatentsInvocation', - }, { - type: 'AddInvocation', - }, { - type: 'SubtractInvocation', - }, { - type: 'MultiplyInvocation', - }, { - type: 'DivideInvocation', - }, { - type: 'RandomIntInvocation', - }, { - type: 'ParamIntInvocation', - }, { - type: 'CvInpaintInvocation', - }, { - type: 'RangeInvocation', - }, { - type: 'RandomRangeInvocation', - }, { - type: 'UpscaleInvocation', - }, { - type: 'RestoreFaceInvocation', - }, { - type: 'TextToImageInvocation', - }, { - type: 'InfillColorInvocation', - }, { - type: 'InfillTileInvocation', - }, { - type: 'InfillPatchMatchInvocation', - }, { - type: 'GraphInvocation', - }, { - type: 'IterateInvocation', - }, { - type: 'CollectInvocation', - }, { - type: 'LatentsToLatentsInvocation', - }, { - type: 'ImageToImageInvocation', - }, { - type: 'InpaintInvocation', - }], - }, - }, - edges: { - type: 'array', - contains: { - type: 'Edge', - }, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$GraphExecutionState.ts b/invokeai/frontend/web/src/services/api/schemas/$GraphExecutionState.ts deleted file mode 100644 index c0a2264877..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$GraphExecutionState.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $GraphExecutionState = { - description: `Tracks the state of a graph execution`, - properties: { - id: { - type: 'string', - description: `The id of the execution state`, - isRequired: true, - }, - graph: { - type: 'all-of', - description: `The graph being executed`, - contains: [{ - type: 'Graph', - }], - isRequired: true, - }, - execution_graph: { - type: 'all-of', - description: `The expanded graph of activated and executed nodes`, - contains: [{ - type: 'Graph', - }], - isRequired: true, - }, - executed: { - type: 'array', - contains: { - type: 'string', - }, - isRequired: true, - }, - executed_history: { - type: 'array', - contains: { - type: 'string', - }, - isRequired: true, - }, - results: { - type: 'dictionary', - contains: { - type: 'one-of', - contains: [{ - type: 'ImageOutput', - }, { - type: 'MaskOutput', - }, { - type: 'CompelOutput', - }, { - type: 'LatentsOutput', - }, { - type: 'NoiseOutput', - }, { - type: 'IntOutput', - }, { - type: 'PromptOutput', - }, { - type: 'IntCollectionOutput', - }, { - type: 'GraphInvocationOutput', - }, { - type: 'IterateInvocationOutput', - }, { - type: 'CollectInvocationOutput', - }], - }, - isRequired: true, - }, - errors: { - type: 'dictionary', - contains: { - type: 'string', - }, - isRequired: true, - }, - prepared_source_mapping: { - type: 'dictionary', - contains: { - type: 'string', - }, - isRequired: true, - }, - source_prepared_mapping: { - type: 'dictionary', - contains: { - type: 'array', - contains: { - type: 'string', - }, - }, - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$GraphInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$GraphInvocation.ts deleted file mode 100644 index 0b9e4322c8..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$GraphInvocation.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $GraphInvocation = { - description: `A node to process inputs and produce outputs. - May use dependency injection in __init__ to receive providers.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - graph: { - type: 'all-of', - description: `The graph to run`, - contains: [{ - type: 'Graph', - }], - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$GraphInvocationOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$GraphInvocationOutput.ts deleted file mode 100644 index c411e65a85..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$GraphInvocationOutput.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $GraphInvocationOutput = { - description: `Base class for all invocation outputs`, - properties: { - type: { - type: 'Enum', - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$HTTPValidationError.ts b/invokeai/frontend/web/src/services/api/schemas/$HTTPValidationError.ts deleted file mode 100644 index 0d129d4b67..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$HTTPValidationError.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $HTTPValidationError = { - properties: { - detail: { - type: 'array', - contains: { - type: 'ValidationError', - }, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageField.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageField.ts deleted file mode 100644 index 968ac29a45..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$ImageField.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $ImageField = { - description: `An image field used for passing image objects between invocations`, - properties: { - image_type: { - type: 'all-of', - description: `The type of the image`, - contains: [{ - type: 'ImageType', - }], - isRequired: true, - }, - image_name: { - type: 'string', - description: `The name of the image`, - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageOutput.ts deleted file mode 100644 index 6adbe0d8c1..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$ImageOutput.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $ImageOutput = { - description: `Base class for invocations that output an image`, - properties: { - type: { - type: 'Enum', - isRequired: true, - }, - image: { - type: 'all-of', - description: `The output image`, - contains: [{ - type: 'ImageField', - }], - isRequired: true, - }, - width: { - type: 'number', - description: `The width of the image in pixels`, - isRequired: true, - }, - height: { - type: 'number', - description: `The height of the image in pixels`, - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageResponse.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageResponse.ts deleted file mode 100644 index 9a3d453536..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$ImageResponse.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $ImageResponse = { - description: `The response type for images`, - properties: { - image_type: { - type: 'all-of', - description: `The type of the image`, - contains: [{ - type: 'ImageType', - }], - isRequired: true, - }, - image_name: { - type: 'string', - description: `The name of the image`, - isRequired: true, - }, - image_url: { - type: 'string', - description: `The url of the image`, - isRequired: true, - }, - thumbnail_url: { - type: 'string', - description: `The url of the image's thumbnail`, - isRequired: true, - }, - metadata: { - type: 'all-of', - description: `The image's metadata`, - contains: [{ - type: 'ImageResponseMetadata', - }], - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageResponseMetadata.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageResponseMetadata.ts deleted file mode 100644 index d215c8de58..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$ImageResponseMetadata.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $ImageResponseMetadata = { - description: `An image's metadata. Used only in HTTP responses.`, - properties: { - created: { - type: 'number', - description: `The creation timestamp of the image`, - isRequired: true, - }, - width: { - type: 'number', - description: `The width of the image in pixels`, - isRequired: true, - }, - height: { - type: 'number', - description: `The height of the image in pixels`, - isRequired: true, - }, - invokeai: { - type: 'all-of', - description: `The image's InvokeAI-specific metadata`, - contains: [{ - type: 'InvokeAIMetadata', - }], - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageToImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageToImageInvocation.ts deleted file mode 100644 index 098009d182..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$ImageToImageInvocation.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $ImageToImageInvocation = { - description: `Generates an image using img2img.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - prompt: { - type: 'string', - description: `The prompt to generate an image from`, - }, - seed: { - type: 'number', - description: `The seed to use (omit for random)`, - maximum: 2147483647, - }, - steps: { - type: 'number', - description: `The number of steps to use to generate the image`, - }, - width: { - type: 'number', - description: `The width of the resulting image`, - multipleOf: 8, - }, - height: { - type: 'number', - description: `The height of the resulting image`, - multipleOf: 8, - }, - cfg_scale: { - type: 'number', - description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`, - minimum: 1, - }, - scheduler: { - type: 'Enum', - }, - model: { - type: 'string', - description: `The model to use (currently ignored)`, - }, - image: { - type: 'all-of', - description: `The input image`, - contains: [{ - type: 'ImageField', - }], - }, - strength: { - type: 'number', - description: `The strength of the original image`, - maximum: 1, - }, - fit: { - type: 'boolean', - description: `Whether or not the result should be fit to the aspect ratio of the input image`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageToLatentsInvocation.ts deleted file mode 100644 index 48e28f1315..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$ImageToLatentsInvocation.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $ImageToLatentsInvocation = { - description: `Encodes an image into latents.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - image: { - type: 'all-of', - description: `The image to encode`, - contains: [{ - type: 'ImageField', - }], - }, - model: { - type: 'string', - description: `The model to use`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageType.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageType.ts deleted file mode 100644 index 92e1f2b218..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$ImageType.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $ImageType = { - type: 'Enum', -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$InfillColorInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$InfillColorInvocation.ts deleted file mode 100644 index a4f639f280..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$InfillColorInvocation.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $InfillColorInvocation = { - description: `Infills transparent areas of an image with a solid color`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - image: { - type: 'all-of', - description: `The image to infill`, - contains: [{ - type: 'ImageField', - }], - }, - color: { - type: 'all-of', - description: `The color to use to infill`, - contains: [{ - type: 'ColorField', - }], - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$InfillPatchMatchInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$InfillPatchMatchInvocation.ts deleted file mode 100644 index bc62cb829f..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$InfillPatchMatchInvocation.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $InfillPatchMatchInvocation = { - description: `Infills transparent areas of an image using the PatchMatch algorithm`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - image: { - type: 'all-of', - description: `The image to infill`, - contains: [{ - type: 'ImageField', - }], - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$InfillTileInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$InfillTileInvocation.ts deleted file mode 100644 index 7a14d94e5a..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$InfillTileInvocation.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $InfillTileInvocation = { - description: `Infills transparent areas of an image with tiles of the image`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - image: { - type: 'all-of', - description: `The image to infill`, - contains: [{ - type: 'ImageField', - }], - }, - tile_size: { - type: 'number', - description: `The tile size (px)`, - minimum: 1, - }, - seed: { - type: 'number', - description: `The seed to use for tile generation (omit for random)`, - maximum: 2147483647, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$InpaintInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$InpaintInvocation.ts deleted file mode 100644 index 1225cde1b6..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$InpaintInvocation.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $InpaintInvocation = { - description: `Generates an image using inpaint.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - prompt: { - type: 'string', - description: `The prompt to generate an image from`, - }, - seed: { - type: 'number', - description: `The seed to use (omit for random)`, - maximum: 2147483647, - }, - steps: { - type: 'number', - description: `The number of steps to use to generate the image`, - }, - width: { - type: 'number', - description: `The width of the resulting image`, - multipleOf: 8, - }, - height: { - type: 'number', - description: `The height of the resulting image`, - multipleOf: 8, - }, - cfg_scale: { - type: 'number', - description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`, - minimum: 1, - }, - scheduler: { - type: 'Enum', - }, - model: { - type: 'string', - description: `The model to use (currently ignored)`, - }, - image: { - type: 'all-of', - description: `The input image`, - contains: [{ - type: 'ImageField', - }], - }, - strength: { - type: 'number', - description: `The strength of the original image`, - maximum: 1, - }, - fit: { - type: 'boolean', - description: `Whether or not the result should be fit to the aspect ratio of the input image`, - }, - mask: { - type: 'all-of', - description: `The mask`, - contains: [{ - type: 'ImageField', - }], - }, - seam_size: { - type: 'number', - description: `The seam inpaint size (px)`, - minimum: 1, - }, - seam_blur: { - type: 'number', - description: `The seam inpaint blur radius (px)`, - }, - seam_strength: { - type: 'number', - description: `The seam inpaint strength`, - maximum: 1, - }, - seam_steps: { - type: 'number', - description: `The number of steps to use for seam inpaint`, - minimum: 1, - }, - tile_size: { - type: 'number', - description: `The tile infill method size (px)`, - minimum: 1, - }, - infill_method: { - type: 'Enum', - }, - inpaint_width: { - type: 'number', - description: `The width of the inpaint region (px)`, - multipleOf: 8, - }, - inpaint_height: { - type: 'number', - description: `The height of the inpaint region (px)`, - multipleOf: 8, - }, - inpaint_fill: { - type: 'all-of', - description: `The solid infill method color`, - contains: [{ - type: 'ColorField', - }], - }, - inpaint_replace: { - type: 'number', - description: `The amount by which to replace masked areas with latent noise`, - maximum: 1, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$IntCollectionOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$IntCollectionOutput.ts deleted file mode 100644 index caffe0ac87..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$IntCollectionOutput.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $IntCollectionOutput = { - description: `A collection of integers`, - properties: { - type: { - type: 'Enum', - }, - collection: { - type: 'array', - contains: { - type: 'number', - }, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$IntOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$IntOutput.ts deleted file mode 100644 index dfb16c1473..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$IntOutput.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $IntOutput = { - description: `An integer output`, - properties: { - type: { - type: 'Enum', - }, - 'a': { - type: 'number', - description: `The output integer`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$InverseLerpInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$InverseLerpInvocation.ts deleted file mode 100644 index 43dadca876..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$InverseLerpInvocation.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $InverseLerpInvocation = { - description: `Inverse linear interpolation of all pixels of an image`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - image: { - type: 'all-of', - description: `The image to lerp`, - contains: [{ - type: 'ImageField', - }], - }, - min: { - type: 'number', - description: `The minimum input value`, - maximum: 255, - }, - max: { - type: 'number', - description: `The maximum input value`, - maximum: 255, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$InvokeAIMetadata.ts b/invokeai/frontend/web/src/services/api/schemas/$InvokeAIMetadata.ts deleted file mode 100644 index f2895f6646..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$InvokeAIMetadata.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $InvokeAIMetadata = { - properties: { - session_id: { - type: 'string', - }, - node: { - type: 'dictionary', - contains: { - type: 'any-of', - contains: [{ - type: 'string', - }, { - type: 'number', - }, { - type: 'number', - }, { - type: 'boolean', - }, { - type: 'MetadataImageField', - }, { - type: 'MetadataLatentsField', - }, { - type: 'MetadataColorField', - }], - }, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$IterateInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$IterateInvocation.ts deleted file mode 100644 index b570b889e4..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$IterateInvocation.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $IterateInvocation = { - description: `A node to process inputs and produce outputs. - May use dependency injection in __init__ to receive providers.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - collection: { - type: 'array', - contains: { - properties: { - }, - }, - }, - index: { - type: 'number', - description: `The index, will be provided on executed iterators`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$IterateInvocationOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$IterateInvocationOutput.ts deleted file mode 100644 index 826e92346d..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$IterateInvocationOutput.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $IterateInvocationOutput = { - description: `Used to connect iteration outputs. Will be expanded to a specific output.`, - properties: { - type: { - type: 'Enum', - isRequired: true, - }, - item: { - description: `The item being iterated over`, - properties: { - }, - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$LatentsField.ts b/invokeai/frontend/web/src/services/api/schemas/$LatentsField.ts deleted file mode 100644 index 6f81c42883..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$LatentsField.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $LatentsField = { - description: `A latents field used for passing latents between invocations`, - properties: { - latents_name: { - type: 'string', - description: `The name of the latents`, - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$LatentsOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$LatentsOutput.ts deleted file mode 100644 index 41a670c3aa..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$LatentsOutput.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $LatentsOutput = { - description: `Base class for invocations that output latents`, - properties: { - type: { - type: 'Enum', - }, - latents: { - type: 'all-of', - description: `The output latents`, - contains: [{ - type: 'LatentsField', - }], - }, - width: { - type: 'number', - description: `The width of the latents in pixels`, - isRequired: true, - }, - height: { - type: 'number', - description: `The height of the latents in pixels`, - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$LatentsToImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$LatentsToImageInvocation.ts deleted file mode 100644 index 971fa3b675..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$LatentsToImageInvocation.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $LatentsToImageInvocation = { - description: `Generates an image from latents.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - latents: { - type: 'all-of', - description: `The latents to generate an image from`, - contains: [{ - type: 'LatentsField', - }], - }, - model: { - type: 'string', - description: `The model to use`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts deleted file mode 100644 index 47f28bed61..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $LatentsToLatentsInvocation = { - description: `Generates latents using latents as base image.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - positive_conditioning: { - type: 'all-of', - description: `Positive conditioning for generation`, - contains: [{ - type: 'ConditioningField', - }], - }, - negative_conditioning: { - type: 'all-of', - description: `Negative conditioning for generation`, - contains: [{ - type: 'ConditioningField', - }], - }, - noise: { - type: 'all-of', - description: `The noise to use`, - contains: [{ - type: 'LatentsField', - }], - }, - steps: { - type: 'number', - description: `The number of steps to use to generate the image`, - }, - cfg_scale: { - type: 'number', - description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`, - }, - scheduler: { - type: 'Enum', - }, - model: { - type: 'string', - description: `The model to use (currently ignored)`, - }, - seamless: { - type: 'boolean', - description: `Whether or not to generate an image that can tile without seams`, - }, - seamless_axes: { - type: 'string', - description: `The axes to tile the image on, 'x' and/or 'y'`, - }, - latents: { - type: 'all-of', - description: `The latents to use as a base image`, - contains: [{ - type: 'LatentsField', - }], - }, - strength: { - type: 'number', - description: `The strength of the latents to use`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$LerpInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$LerpInvocation.ts deleted file mode 100644 index bafac85817..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$LerpInvocation.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $LerpInvocation = { - description: `Linear interpolation of all pixels of an image`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - image: { - type: 'all-of', - description: `The image to lerp`, - contains: [{ - type: 'ImageField', - }], - }, - min: { - type: 'number', - description: `The minimum output value`, - maximum: 255, - }, - max: { - type: 'number', - description: `The maximum output value`, - maximum: 255, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$LoadImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$LoadImageInvocation.ts deleted file mode 100644 index 7b7a0cdffe..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$LoadImageInvocation.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $LoadImageInvocation = { - description: `Load an image and provide it as output.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - image_type: { - type: 'all-of', - description: `The type of the image`, - contains: [{ - type: 'ImageType', - }], - isRequired: true, - }, - image_name: { - type: 'string', - description: `The name of the image`, - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$MaskFromAlphaInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$MaskFromAlphaInvocation.ts deleted file mode 100644 index 88c2089816..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$MaskFromAlphaInvocation.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $MaskFromAlphaInvocation = { - description: `Extracts the alpha channel of an image as a mask.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - image: { - type: 'all-of', - description: `The image to create the mask from`, - contains: [{ - type: 'ImageField', - }], - }, - invert: { - type: 'boolean', - description: `Whether or not to invert the mask`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$MaskOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$MaskOutput.ts deleted file mode 100644 index cc9d107ab5..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$MaskOutput.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $MaskOutput = { - description: `Base class for invocations that output a mask`, - properties: { - type: { - type: 'Enum', - isRequired: true, - }, - mask: { - type: 'all-of', - description: `The output mask`, - contains: [{ - type: 'ImageField', - }], - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$MetadataColorField.ts b/invokeai/frontend/web/src/services/api/schemas/$MetadataColorField.ts deleted file mode 100644 index 234bd3e2f6..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$MetadataColorField.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $MetadataColorField = { - properties: { - 'r': { - type: 'number', - isRequired: true, - }, - 'g': { - type: 'number', - isRequired: true, - }, - 'b': { - type: 'number', - isRequired: true, - }, - 'a': { - type: 'number', - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$MetadataImageField.ts b/invokeai/frontend/web/src/services/api/schemas/$MetadataImageField.ts deleted file mode 100644 index 5e4b1307ed..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$MetadataImageField.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $MetadataImageField = { - properties: { - image_type: { - type: 'ImageType', - isRequired: true, - }, - image_name: { - type: 'string', - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$MetadataLatentsField.ts b/invokeai/frontend/web/src/services/api/schemas/$MetadataLatentsField.ts deleted file mode 100644 index c377f26e42..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$MetadataLatentsField.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $MetadataLatentsField = { - properties: { - latents_name: { - type: 'string', - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ModelsList.ts b/invokeai/frontend/web/src/services/api/schemas/$ModelsList.ts deleted file mode 100644 index 6fa85f6329..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$ModelsList.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $ModelsList = { - properties: { - models: { - type: 'dictionary', - contains: { - type: 'one-of', - contains: [{ - type: 'CkptModelInfo', - }, { - type: 'DiffusersModelInfo', - }], - }, - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$MultiplyInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$MultiplyInvocation.ts deleted file mode 100644 index 4e8c1d4bbb..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$MultiplyInvocation.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $MultiplyInvocation = { - description: `Multiplies two numbers`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - 'a': { - type: 'number', - description: `The first number`, - }, - 'b': { - type: 'number', - description: `The second number`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$NoiseInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$NoiseInvocation.ts deleted file mode 100644 index eade3611b7..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$NoiseInvocation.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $NoiseInvocation = { - description: `Generates latent noise.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - seed: { - type: 'number', - description: `The seed to use`, - maximum: 2147483647, - }, - width: { - type: 'number', - description: `The width of the resulting noise`, - multipleOf: 8, - }, - height: { - type: 'number', - description: `The height of the resulting noise`, - multipleOf: 8, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$NoiseOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$NoiseOutput.ts deleted file mode 100644 index 8112240add..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$NoiseOutput.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $NoiseOutput = { - description: `Invocation noise output`, - properties: { - type: { - type: 'Enum', - }, - noise: { - type: 'all-of', - description: `The output noise`, - contains: [{ - type: 'LatentsField', - }], - }, - width: { - type: 'number', - description: `The width of the noise in pixels`, - isRequired: true, - }, - height: { - type: 'number', - description: `The height of the noise in pixels`, - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_GraphExecutionState_.ts b/invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_GraphExecutionState_.ts deleted file mode 100644 index ca574eb463..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_GraphExecutionState_.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $PaginatedResults_GraphExecutionState_ = { - description: `Paginated results`, - properties: { - items: { - type: 'array', - contains: { - type: 'GraphExecutionState', - }, - isRequired: true, - }, - page: { - type: 'number', - description: `Current Page`, - isRequired: true, - }, - pages: { - type: 'number', - description: `Total number of pages`, - isRequired: true, - }, - per_page: { - type: 'number', - description: `Number of items per page`, - isRequired: true, - }, - total: { - type: 'number', - description: `Total number of items in result`, - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_ImageResponse_.ts b/invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_ImageResponse_.ts deleted file mode 100644 index 113a374f85..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_ImageResponse_.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $PaginatedResults_ImageResponse_ = { - description: `Paginated results`, - properties: { - items: { - type: 'array', - contains: { - type: 'ImageResponse', - }, - isRequired: true, - }, - page: { - type: 'number', - description: `Current Page`, - isRequired: true, - }, - pages: { - type: 'number', - description: `Total number of pages`, - isRequired: true, - }, - per_page: { - type: 'number', - description: `Number of items per page`, - isRequired: true, - }, - total: { - type: 'number', - description: `Total number of items in result`, - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ParamIntInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$ParamIntInvocation.ts deleted file mode 100644 index a8eac4c450..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$ParamIntInvocation.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $ParamIntInvocation = { - description: `An integer parameter`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - 'a': { - type: 'number', - description: `The integer value`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$PasteImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$PasteImageInvocation.ts deleted file mode 100644 index 74bb1edfcb..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$PasteImageInvocation.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $PasteImageInvocation = { - description: `Pastes an image into another image.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - base_image: { - type: 'all-of', - description: `The base image`, - contains: [{ - type: 'ImageField', - }], - }, - image: { - type: 'all-of', - description: `The image to paste`, - contains: [{ - type: 'ImageField', - }], - }, - mask: { - type: 'all-of', - description: `The mask to use when pasting`, - contains: [{ - type: 'ImageField', - }], - }, - 'x': { - type: 'number', - description: `The left x coordinate at which to paste the image`, - }, - 'y': { - type: 'number', - description: `The top y coordinate at which to paste the image`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$PromptOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$PromptOutput.ts deleted file mode 100644 index 29b800452f..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$PromptOutput.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $PromptOutput = { - description: `Base class for invocations that output a prompt`, - properties: { - type: { - type: 'Enum', - isRequired: true, - }, - prompt: { - type: 'string', - description: `The output prompt`, - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$RandomIntInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$RandomIntInvocation.ts deleted file mode 100644 index e70192fae5..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$RandomIntInvocation.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $RandomIntInvocation = { - description: `Outputs a single random integer.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - low: { - type: 'number', - description: `The inclusive low value`, - }, - high: { - type: 'number', - description: `The exclusive high value`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$RandomRangeInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$RandomRangeInvocation.ts deleted file mode 100644 index a71b223ba0..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$RandomRangeInvocation.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $RandomRangeInvocation = { - description: `Creates a collection of random numbers`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - low: { - type: 'number', - description: `The inclusive low value`, - }, - high: { - type: 'number', - description: `The exclusive high value`, - }, - size: { - type: 'number', - description: `The number of values to generate`, - }, - seed: { - type: 'number', - description: `The seed for the RNG (omit for random)`, - maximum: 2147483647, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$RangeInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$RangeInvocation.ts deleted file mode 100644 index f05dae51d4..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$RangeInvocation.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $RangeInvocation = { - description: `Creates a range`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - start: { - type: 'number', - description: `The start of the range`, - }, - stop: { - type: 'number', - description: `The stop of the range`, - }, - step: { - type: 'number', - description: `The step of the range`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ResizeLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$ResizeLatentsInvocation.ts deleted file mode 100644 index 2609b1a681..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$ResizeLatentsInvocation.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $ResizeLatentsInvocation = { - description: `Resizes latents to explicit width/height (in pixels). Provided dimensions are floor-divided by 8.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - latents: { - type: 'all-of', - description: `The latents to resize`, - contains: [{ - type: 'LatentsField', - }], - }, - width: { - type: 'number', - description: `The width to resize to (px)`, - isRequired: true, - minimum: 64, - multipleOf: 8, - }, - height: { - type: 'number', - description: `The height to resize to (px)`, - isRequired: true, - minimum: 64, - multipleOf: 8, - }, - mode: { - type: 'Enum', - }, - antialias: { - type: 'boolean', - description: `Whether or not to antialias (applied in bilinear and bicubic modes only)`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$RestoreFaceInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$RestoreFaceInvocation.ts deleted file mode 100644 index a9d10c480b..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$RestoreFaceInvocation.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $RestoreFaceInvocation = { - description: `Restores faces in an image.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - image: { - type: 'all-of', - description: `The input image`, - contains: [{ - type: 'ImageField', - }], - }, - strength: { - type: 'number', - description: `The strength of the restoration`, - maximum: 1, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ScaleLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$ScaleLatentsInvocation.ts deleted file mode 100644 index 8d4d15e2e8..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$ScaleLatentsInvocation.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $ScaleLatentsInvocation = { - description: `Scales latents by a given factor.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - latents: { - type: 'all-of', - description: `The latents to scale`, - contains: [{ - type: 'LatentsField', - }], - }, - scale_factor: { - type: 'number', - description: `The factor by which to scale the latents`, - isRequired: true, - }, - mode: { - type: 'Enum', - }, - antialias: { - type: 'boolean', - description: `Whether or not to antialias (applied in bilinear and bicubic modes only)`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ShowImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$ShowImageInvocation.ts deleted file mode 100644 index 99a8ce0068..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$ShowImageInvocation.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $ShowImageInvocation = { - description: `Displays a provided image, and passes it forward in the pipeline.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - image: { - type: 'all-of', - description: `The image to show`, - contains: [{ - type: 'ImageField', - }], - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$SubtractInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$SubtractInvocation.ts deleted file mode 100644 index be835de13b..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$SubtractInvocation.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $SubtractInvocation = { - description: `Subtracts two numbers`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - 'a': { - type: 'number', - description: `The first number`, - }, - 'b': { - type: 'number', - description: `The second number`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$TextToImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$TextToImageInvocation.ts deleted file mode 100644 index 0f583dd2d0..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$TextToImageInvocation.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $TextToImageInvocation = { - description: `Generates an image using text2img.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - prompt: { - type: 'string', - description: `The prompt to generate an image from`, - }, - seed: { - type: 'number', - description: `The seed to use (omit for random)`, - maximum: 2147483647, - }, - steps: { - type: 'number', - description: `The number of steps to use to generate the image`, - }, - width: { - type: 'number', - description: `The width of the resulting image`, - multipleOf: 8, - }, - height: { - type: 'number', - description: `The height of the resulting image`, - multipleOf: 8, - }, - cfg_scale: { - type: 'number', - description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`, - minimum: 1, - }, - scheduler: { - type: 'Enum', - }, - model: { - type: 'string', - description: `The model to use (currently ignored)`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts deleted file mode 100644 index 5ff7b44129..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $TextToLatentsInvocation = { - description: `Generates latents from conditionings.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - positive_conditioning: { - type: 'all-of', - description: `Positive conditioning for generation`, - contains: [{ - type: 'ConditioningField', - }], - }, - negative_conditioning: { - type: 'all-of', - description: `Negative conditioning for generation`, - contains: [{ - type: 'ConditioningField', - }], - }, - noise: { - type: 'all-of', - description: `The noise to use`, - contains: [{ - type: 'LatentsField', - }], - }, - steps: { - type: 'number', - description: `The number of steps to use to generate the image`, - }, - cfg_scale: { - type: 'number', - description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`, - }, - scheduler: { - type: 'Enum', - }, - model: { - type: 'string', - description: `The model to use (currently ignored)`, - }, - seamless: { - type: 'boolean', - description: `Whether or not to generate an image that can tile without seams`, - }, - seamless_axes: { - type: 'string', - description: `The axes to tile the image on, 'x' and/or 'y'`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$UpscaleInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$UpscaleInvocation.ts deleted file mode 100644 index 21f87f1fb7..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$UpscaleInvocation.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $UpscaleInvocation = { - description: `Upscales an image.`, - properties: { - id: { - type: 'string', - description: `The id of this node. Must be unique among all nodes.`, - isRequired: true, - }, - type: { - type: 'Enum', - }, - image: { - type: 'all-of', - description: `The input image`, - contains: [{ - type: 'ImageField', - }], - }, - strength: { - type: 'number', - description: `The strength`, - maximum: 1, - }, - level: { - type: 'Enum', - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$VaeRepo.ts b/invokeai/frontend/web/src/services/api/schemas/$VaeRepo.ts deleted file mode 100644 index 8b8fbf0968..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$VaeRepo.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $VaeRepo = { - properties: { - repo_id: { - type: 'string', - description: `The repo ID to use for this VAE`, - isRequired: true, - }, - path: { - type: 'string', - description: `The path to the VAE`, - }, - subfolder: { - type: 'string', - description: `The subfolder to use for this VAE`, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ValidationError.ts b/invokeai/frontend/web/src/services/api/schemas/$ValidationError.ts deleted file mode 100644 index d4c5c3e471..0000000000 --- a/invokeai/frontend/web/src/services/api/schemas/$ValidationError.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $ValidationError = { - properties: { - loc: { - type: 'array', - contains: { - type: 'any-of', - contains: [{ - type: 'string', - }, { - type: 'number', - }], - }, - isRequired: true, - }, - msg: { - type: 'string', - isRequired: true, - }, - type: { - type: 'string', - isRequired: true, - }, - }, -} as const; diff --git a/invokeai/frontend/web/src/services/api/services/FilesService.ts b/invokeai/frontend/web/src/services/api/services/FilesService.ts new file mode 100644 index 0000000000..0a4d3b3a1f --- /dev/null +++ b/invokeai/frontend/web/src/services/api/services/FilesService.ts @@ -0,0 +1,76 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ImageType } from '../models/ImageType'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class FilesService { + + /** + * Get Image + * Gets an image + * @returns any Successful Response + * @throws ApiError + */ + public static getImage({ + imageType, + imageName, + }: { + /** + * The type of the image to get + */ + imageType: ImageType, + /** + * The id of the image to get + */ + imageName: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/files/images/{image_type}/{image_name}', + path: { + 'image_type': imageType, + 'image_name': imageName, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Get Thumbnail + * Gets a thumbnail + * @returns any Successful Response + * @throws ApiError + */ + public static getThumbnail({ + imageType, + imageName, + }: { + /** + * The type of the image whose thumbnail to get + */ + imageType: ImageType, + /** + * The id of the image whose thumbnail to get + */ + imageName: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/files/images/{image_type}/{image_name}/thumbnail', + path: { + 'image_type': imageType, + 'image_name': imageName, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + +} diff --git a/invokeai/frontend/web/src/services/api/services/ImagesService.ts b/invokeai/frontend/web/src/services/api/services/ImagesService.ts index 9dc63688fc..3af7d5b53e 100644 --- a/invokeai/frontend/web/src/services/api/services/ImagesService.ts +++ b/invokeai/frontend/web/src/services/api/services/ImagesService.ts @@ -2,9 +2,8 @@ /* tslint:disable */ /* eslint-disable */ import type { Body_upload_image } from '../models/Body_upload_image'; -import type { ImageResponse } from '../models/ImageResponse'; +import type { ImageCategory } from '../models/ImageCategory'; import type { ImageType } from '../models/ImageType'; -import type { PaginatedResults_ImageResponse_ } from '../models/PaginatedResults_ImageResponse_'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; @@ -23,17 +22,17 @@ export class ImagesService { imageName, }: { /** - * The type of image to get + * The type of the image to get */ imageType: ImageType, /** - * The name of the image to get + * The id of the image to get */ imageName: string, }): CancelablePromise { return __request(OpenAPI, { method: 'GET', - url: '/api/v1/images/{image_type}/{image_name}', + url: '/api/v1/files/images/{image_type}/{image_name}', path: { 'image_type': imageType, 'image_name': imageName, @@ -44,9 +43,148 @@ export class ImagesService { }); } + /** + * Get Thumbnail + * Gets a thumbnail + * @returns any Successful Response + * @throws ApiError + */ + public static getThumbnail({ + imageType, + imageName, + }: { + /** + * The type of the image whose thumbnail to get + */ + imageType: ImageType, + /** + * The id of the image whose thumbnail to get + */ + imageName: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/files/images/{image_type}/{image_name}/thumbnail', + path: { + 'image_type': imageType, + 'image_name': imageName, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Get Image Record + * Gets an image record by id + * @returns any Successful Response + * @throws ApiError + */ + public static getImageRecord({ + imageType, + imageName, + }: { + /** + * The type of the image record to get + */ + imageType: ImageType, + /** + * The id of the image record to get + */ + imageName: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/images/records/{image_type}/{image_name}', + path: { + 'image_type': imageType, + 'image_name': imageName, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * List Image Records + * Gets a list of image records by type and category + * @returns any Successful Response + * @throws ApiError + */ + public static listImageRecords({ + imageType, + imageCategory, + page, + perPage = 10, + }: { + /** + * The type of image records to get + */ + imageType: ImageType, + /** + * The kind of image records to get + */ + imageCategory: ImageCategory, + /** + * The page of image records to get + */ + page?: number, + /** + * The number of image records per page + */ + perPage?: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/images/records/', + query: { + 'image_type': imageType, + 'image_category': imageCategory, + 'page': page, + 'per_page': perPage, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Upload Image + * Uploads an image + * @returns any The image was uploaded successfully + * @throws ApiError + */ + public static uploadImage({ + imageType, + formData, + imageCategory, + }: { + imageType: ImageType, + formData: Body_upload_image, + imageCategory?: ImageCategory, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/images/', + query: { + 'image_type': imageType, + 'image_category': imageCategory, + }, + formData: formData, + mediaType: 'multipart/form-data', + errors: { + 415: `Image upload failed`, + 422: `Validation Error`, + }, + }); + } + /** * Delete Image - * Deletes an image and its thumbnail + * Deletes an image * @returns any Successful Response * @throws ApiError */ @@ -54,9 +192,6 @@ export class ImagesService { imageType, imageName, }: { - /** - * The type of image to delete - */ imageType: ImageType, /** * The name of the image to delete @@ -76,101 +211,4 @@ export class ImagesService { }); } - /** - * Get Thumbnail - * Gets a thumbnail - * @returns any Successful Response - * @throws ApiError - */ - public static getThumbnail({ - thumbnailType, - thumbnailName, - }: { - /** - * The type of thumbnail to get - */ - thumbnailType: ImageType, - /** - * The name of the thumbnail to get - */ - thumbnailName: string, - }): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/images/{thumbnail_type}/thumbnails/{thumbnail_name}', - path: { - 'thumbnail_type': thumbnailType, - 'thumbnail_name': thumbnailName, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - - /** - * Upload Image - * @returns ImageResponse The image was uploaded successfully - * @throws ApiError - */ - public static uploadImage({ - imageType, - formData, - }: { - imageType: ImageType, - formData: Body_upload_image, - }): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/v1/images/uploads/', - query: { - 'image_type': imageType, - }, - formData: formData, - mediaType: 'multipart/form-data', - errors: { - 415: `Image upload failed`, - 422: `Validation Error`, - }, - }); - } - - /** - * List Images - * Gets a list of images - * @returns PaginatedResults_ImageResponse_ Successful Response - * @throws ApiError - */ - public static listImages({ - imageType, - page, - perPage = 10, - }: { - /** - * The type of images to get - */ - imageType?: ImageType, - /** - * The page of images to get - */ - page?: number, - /** - * The number of images per page - */ - perPage?: number, - }): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/images/', - query: { - 'image_type': imageType, - 'page': page, - 'per_page': perPage, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - } diff --git a/invokeai/frontend/web/src/services/api/services/RecordsService.ts b/invokeai/frontend/web/src/services/api/services/RecordsService.ts new file mode 100644 index 0000000000..4079f60ab5 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/services/RecordsService.ts @@ -0,0 +1,89 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ImageCategory } from '../models/ImageCategory'; +import type { ImageType } from '../models/ImageType'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class RecordsService { + + /** + * Get Image Record + * Gets an image record by id + * @returns any Successful Response + * @throws ApiError + */ + public static getImageRecord({ + imageType, + imageName, + }: { + /** + * The type of the image record to get + */ + imageType: ImageType, + /** + * The id of the image record to get + */ + imageName: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/images/records/{image_type}/{image_name}', + path: { + 'image_type': imageType, + 'image_name': imageName, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * List Image Records + * Gets a list of image records by type and category + * @returns any Successful Response + * @throws ApiError + */ + public static listImageRecords({ + imageType, + imageCategory, + page, + perPage = 10, + }: { + /** + * The type of image records to get + */ + imageType: ImageType, + /** + * The kind of image records to get + */ + imageCategory: ImageCategory, + /** + * The page of image records to get + */ + page?: number, + /** + * The number of image records per page + */ + perPage?: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/images/records/', + query: { + 'image_type': imageType, + 'image_category': imageCategory, + 'page': page, + 'per_page': perPage, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + +} diff --git a/invokeai/frontend/web/src/services/api/services/SessionsService.ts b/invokeai/frontend/web/src/services/api/services/SessionsService.ts index c8c53e0bd7..1925b0800f 100644 --- a/invokeai/frontend/web/src/services/api/services/SessionsService.ts +++ b/invokeai/frontend/web/src/services/api/services/SessionsService.ts @@ -150,7 +150,7 @@ export class SessionsService { * The id of the session */ sessionId: string, - requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | CompelInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), + requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), }): CancelablePromise { return __request(OpenAPI, { method: 'POST', @@ -187,7 +187,7 @@ export class SessionsService { * The path to the node in the graph */ nodePath: string, - requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | CompelInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), + requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), }): CancelablePromise { return __request(OpenAPI, { method: 'PUT', From 11bd932cbab6c404fdb0698e047cb62a7aff0152 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 22 May 2023 12:46:16 +1000 Subject: [PATCH 22/72] feat(nodes): revert invocation_complete url hack --- invokeai/app/services/events.py | 4 ---- invokeai/app/services/processor.py | 20 +------------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/invokeai/app/services/events.py b/invokeai/app/services/events.py index dd44bcf73a..a3e7cdd5dc 100644 --- a/invokeai/app/services/events.py +++ b/invokeai/app/services/events.py @@ -50,8 +50,6 @@ class EventServiceBase: result: dict, node: dict, source_node_id: str, - image_url: Optional[str] = None, - thumbnail_url: Optional[str] = None, ) -> None: """Emitted when an invocation has completed""" self.__emit_session_event( @@ -61,8 +59,6 @@ class EventServiceBase: node=node, source_node_id=source_node_id, result=result, - image_url=image_url, - thumbnail_url=thumbnail_url ), ) diff --git a/invokeai/app/services/processor.py b/invokeai/app/services/processor.py index 250d007d06..cdd9db85de 100644 --- a/invokeai/app/services/processor.py +++ b/invokeai/app/services/processor.py @@ -91,30 +91,12 @@ class DefaultInvocationProcessor(InvocationProcessorABC): graph_execution_state ) - def is_image_output(obj: Any) -> TypeGuard[ImageOutput]: - return obj.__class__ == ImageOutput - - outputs_dict = outputs.dict() - - if is_image_output(outputs): - image_url = self.__invoker.services.images_new.get_url( - ImageType.RESULT, outputs.image.image_name - ) - thumbnail_url = self.__invoker.services.images_new.get_url( - ImageType.RESULT, outputs.image.image_name, True - ) - else: - image_url = None - thumbnail_url = None - # Send complete event self.__invoker.services.events.emit_invocation_complete( graph_execution_state_id=graph_execution_state.id, node=invocation.dict(), source_node_id=source_node_id, - result=outputs_dict, - image_url=image_url, - thumbnail_url=thumbnail_url, + result=outputs.dict(), ) except KeyboardInterrupt: From b9375186a5975aacb1b1b863fd2134397f6ad851 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 22 May 2023 12:46:37 +1000 Subject: [PATCH 23/72] feat(nodes): consolidate image routers --- invokeai/app/api/routers/image_files.py | 47 ------ invokeai/app/api/routers/image_records.py | 71 --------- invokeai/app/api/routers/images.py | 143 ++++++++++++++++--- invokeai/app/api_app.py | 6 +- invokeai/app/services/models/image_record.py | 12 +- invokeai/app/services/urls.py | 4 +- 6 files changed, 137 insertions(+), 146 deletions(-) delete mode 100644 invokeai/app/api/routers/image_files.py delete mode 100644 invokeai/app/api/routers/image_records.py diff --git a/invokeai/app/api/routers/image_files.py b/invokeai/app/api/routers/image_files.py deleted file mode 100644 index 2694df5b19..0000000000 --- a/invokeai/app/api/routers/image_files.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI Team -from fastapi import HTTPException, Path -from fastapi.responses import FileResponse -from fastapi.routing import APIRouter -from invokeai.app.models.image import ImageType - -from ..dependencies import ApiDependencies - -image_files_router = APIRouter(prefix="/v1/files/images", tags=["images", "files"]) - - -@image_files_router.get("/{image_type}/{image_name}", operation_id="get_image") -async def get_image( - image_type: ImageType = Path(description="The type of the image to get"), - image_name: str = Path(description="The id of the image to get"), -) -> FileResponse: - """Gets an image""" - - try: - path = ApiDependencies.invoker.services.images_new.get_path( - image_type=image_type, image_name=image_name - ) - - return FileResponse(path) - except Exception as e: - raise HTTPException(status_code=404) - - -@image_files_router.get( - "/{image_type}/{image_name}/thumbnail", operation_id="get_thumbnail" -) -async def get_thumbnail( - image_type: ImageType = Path( - description="The type of the image whose thumbnail to get" - ), - image_name: str = Path(description="The id of the image whose thumbnail to get"), -) -> FileResponse: - """Gets a thumbnail""" - - try: - path = ApiDependencies.invoker.services.images_new.get_path( - image_type=image_type, image_name=image_name, thumbnail=True - ) - - return FileResponse(path) - except Exception as e: - raise HTTPException(status_code=404) diff --git a/invokeai/app/api/routers/image_records.py b/invokeai/app/api/routers/image_records.py deleted file mode 100644 index 5dccccae41..0000000000 --- a/invokeai/app/api/routers/image_records.py +++ /dev/null @@ -1,71 +0,0 @@ -from fastapi import HTTPException, Path, Query -from fastapi.routing import APIRouter -from invokeai.app.models.image import ( - ImageCategory, - ImageType, -) -from invokeai.app.services.item_storage import PaginatedResults -from invokeai.app.services.models.image_record import ImageDTO - -from ..dependencies import ApiDependencies - -image_records_router = APIRouter( - prefix="/v1/images/records", tags=["images", "records"] -) - - -@image_records_router.get("/{image_type}/{image_name}", operation_id="get_image_record") -async def get_image_record( - image_type: ImageType = Path(description="The type of the image record to get"), - image_name: str = Path(description="The id of the image record to get"), -) -> ImageDTO: - """Gets an image record by id""" - - try: - return ApiDependencies.invoker.services.images_new.get_dto( - image_type=image_type, image_name=image_name - ) - except Exception as e: - raise HTTPException(status_code=404) - - -@image_records_router.get( - "/", - operation_id="list_image_records", -) -async def list_image_records( - image_type: ImageType = Query(description="The type of image records to get"), - image_category: ImageCategory = Query( - description="The kind of image records to get" - ), - page: int = Query(default=0, description="The page of image records to get"), - per_page: int = Query( - default=10, description="The number of image records per page" - ), -) -> PaginatedResults[ImageDTO]: - """Gets a list of image records by type and category""" - - image_dtos = ApiDependencies.invoker.services.images_new.get_many( - image_type=image_type, - image_category=image_category, - page=page, - per_page=per_page, - ) - - return image_dtos - - -@image_records_router.delete("/{image_type}/{image_name}", operation_id="delete_image") -async def delete_image_record( - image_type: ImageType = Query(description="The type of image to delete"), - image_name: str = Path(description="The name of the image to delete"), -) -> None: - """Deletes an image record""" - - try: - ApiDependencies.invoker.services.images_new.delete( - image_type=image_type, image_name=image_name - ) - except Exception as e: - # TODO: Does this need any exception handling at all? - pass diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index c38d99c74f..18b47b5595 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -1,15 +1,13 @@ import io -import uuid from fastapi import HTTPException, Path, Query, Request, Response, UploadFile from fastapi.routing import APIRouter +from fastapi.responses import FileResponse from PIL import Image from invokeai.app.models.image import ( ImageCategory, ImageType, ) -from invokeai.app.services.image_record_storage import ImageRecordStorageBase -from invokeai.app.services.image_file_storage import ImageFileStorageBase -from invokeai.app.services.models.image_record import ImageRecord +from invokeai.app.services.models.image_record import ImageDTO, ImageUrlsDTO from invokeai.app.services.item_storage import PaginatedResults from ..dependencies import ApiDependencies @@ -32,7 +30,7 @@ async def upload_image( request: Request, response: Response, image_category: ImageCategory = ImageCategory.IMAGE, -) -> ImageRecord: +) -> ImageDTO: """Uploads an image""" if not file.content_type.startswith("image"): raise HTTPException(status_code=415, detail="Not an image") @@ -40,38 +38,145 @@ async def upload_image( contents = await file.read() try: - img = Image.open(io.BytesIO(contents)) + pil_image = Image.open(io.BytesIO(contents)) except: # Error opening the image raise HTTPException(status_code=415, detail="Failed to read image") try: - image_record = ApiDependencies.invoker.services.images_new.create( - image=img, - image_type=image_type, - image_category=image_category, + image_dto = ApiDependencies.invoker.services.images_new.create( + pil_image, + image_type, + image_category, ) response.status_code = 201 - response.headers["Location"] = image_record.image_url + response.headers["Location"] = image_dto.image_url - return image_record + return image_dto except Exception as e: - raise HTTPException(status_code=500) - + raise HTTPException(status_code=500, detail="Failed to create image") @images_router.delete("/{image_type}/{image_name}", operation_id="delete_image") -async def delete_image_record( +async def delete_image( image_type: ImageType = Query(description="The type of image to delete"), image_name: str = Path(description="The name of the image to delete"), ) -> None: - """Deletes an image record""" + """Deletes an image""" try: - ApiDependencies.invoker.services.images_new.delete( - image_type=image_type, image_name=image_name - ) + ApiDependencies.invoker.services.images_new.delete(image_type, image_name) except Exception as e: # TODO: Does this need any exception handling at all? pass + + +@images_router.get( + "/{image_type}/{image_name}/record", + operation_id="get_image_record", + response_model=ImageDTO, +) +async def get_image_record( + image_type: ImageType = Path(description="The type of the image record to get"), + image_name: str = Path(description="The id of the image record to get"), +) -> ImageDTO: + """Gets an image record by id""" + + try: + return ApiDependencies.invoker.services.images_new.get_dto( + image_type, image_name + ) + except Exception as e: + raise HTTPException(status_code=404) + + +@images_router.get("/{image_type}/{image_name}/image", operation_id="get_image") +async def get_image( + image_type: ImageType = Path(description="The type of the image to get"), + image_name: str = Path(description="The id of the image to get"), +) -> FileResponse: + """Gets an image""" + + try: + path = ApiDependencies.invoker.services.images_new.get_path( + image_type, image_name + ) + + return FileResponse(path) + except Exception as e: + raise HTTPException(status_code=404) + + +@images_router.get("/{image_type}/{image_name}/thumbnail", operation_id="get_thumbnail") +async def get_thumbnail( + image_type: ImageType = Path( + description="The type of the image whose thumbnail to get" + ), + image_name: str = Path(description="The id of the image whose thumbnail to get"), +) -> FileResponse: + """Gets a thumbnail""" + + try: + path = ApiDependencies.invoker.services.images_new.get_path( + image_type, image_name, thumbnail=True + ) + + return FileResponse(path) + except Exception as e: + raise HTTPException(status_code=404) + + +@images_router.get( + "/{image_type}/{image_name}/urls", + operation_id="get_image_urls", + response_model=ImageUrlsDTO, +) +async def get_image_urls( + image_type: ImageType = Path(description="The type of the image whose URL to get"), + image_name: str = Path(description="The id of the image whose URL to get"), +) -> ImageUrlsDTO: + """Gets an image and thumbnail URL""" + + try: + image_url = ApiDependencies.invoker.services.images_new.get_url( + image_type, image_name + ) + thumbnail_url = ApiDependencies.invoker.services.images_new.get_url( + image_type, image_name, thumbnail=True + ) + return ImageUrlsDTO( + image_type=image_type, + image_name=image_name, + image_url=image_url, + thumbnail_url=thumbnail_url, + ) + except Exception as e: + raise HTTPException(status_code=404) + + +@images_router.get( + "/", + operation_id="list_image_records", + response_model=PaginatedResults[ImageDTO], +) +async def list_image_records( + image_type: ImageType = Query(description="The type of image records to get"), + image_category: ImageCategory = Query( + description="The kind of image records to get" + ), + page: int = Query(default=0, description="The page of image records to get"), + per_page: int = Query( + default=10, description="The number of image records per page" + ), +) -> PaginatedResults[ImageDTO]: + """Gets a list of image records by type and category""" + + image_dtos = ApiDependencies.invoker.services.images_new.get_many( + image_type, + image_category, + page, + per_page, + ) + + return image_dtos diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index aaef1d78be..964202786a 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -15,7 +15,7 @@ from fastapi_events.middleware import EventHandlerASGIMiddleware from pydantic.schema import schema from .api.dependencies import ApiDependencies -from .api.routers import image_files, image_records, sessions, models, images +from .api.routers import sessions, models, images from .api.sockets import SocketIO from .invocations.baseinvocation import BaseInvocation from .services.config import InvokeAIAppConfig @@ -73,10 +73,6 @@ app.include_router(sessions.session_router, prefix="/api") app.include_router(models.models_router, prefix="/api") -app.include_router(image_files.image_files_router, prefix="/api") - -app.include_router(image_records.image_records_router, prefix="/api") - app.include_router(images.images_router, prefix="/api") # Build a custom OpenAPI to include all outputs diff --git a/invokeai/app/services/models/image_record.py b/invokeai/app/services/models/image_record.py index cd2f3aacbc..6e15574eb9 100644 --- a/invokeai/app/services/models/image_record.py +++ b/invokeai/app/services/models/image_record.py @@ -23,13 +23,21 @@ class ImageRecord(BaseModel): ) -class ImageDTO(ImageRecord): - """Deserialized image record with URLs.""" +class ImageUrlsDTO(BaseModel): + """The URLs for an image and its thumbnaill""" + image_name: str = Field(description="The name of the image.") + image_type: ImageType = Field(description="The type of the image.") image_url: str = Field(description="The URL of the image.") thumbnail_url: str = Field(description="The thumbnail URL of the image.") +class ImageDTO(ImageRecord, ImageUrlsDTO): + """Deserialized image record with URLs.""" + + pass + + def image_record_to_dto( image_record: ImageRecord, image_url: str, thumbnail_url: str ) -> ImageDTO: diff --git a/invokeai/app/services/urls.py b/invokeai/app/services/urls.py index 989f6853c2..0e2389b7d0 100644 --- a/invokeai/app/services/urls.py +++ b/invokeai/app/services/urls.py @@ -25,6 +25,6 @@ class LocalUrlService(UrlServiceBase): ) -> str: image_basename = os.path.basename(image_name) if thumbnail: - return f"{self._base_url}/files/images/{image_type.value}/{image_basename}/thumbnail" + return f"{self._base_url}/images/{image_type.value}/{image_basename}/thumbnail" - return f"{self._base_url}/files/images/{image_type.value}/{image_basename}" + return f"{self._base_url}/images/{image_type.value}/{image_basename}/image" From f071b03cebccc5c5bcd501cfe16e587c0cc1d0cc Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 22 May 2023 12:46:52 +1000 Subject: [PATCH 24/72] chore(ui): regen api client --- .../frontend/web/src/services/api/index.ts | 6 +- .../web/src/services/api/models/ImageDTO.ts | 50 ++++ .../src/services/api/models/ImageMetadata.ts | 68 +++++ .../src/services/api/models/ImageUrlsDTO.ts | 28 +++ .../api/models/PaginatedResults_ImageDTO_.ts | 32 +++ .../src/services/api/services/FilesService.ts | 76 ------ .../services/api/services/ImagesService.ts | 233 ++++++++++-------- .../services/api/services/RecordsService.ts | 89 ------- 8 files changed, 316 insertions(+), 266 deletions(-) create mode 100644 invokeai/frontend/web/src/services/api/models/ImageDTO.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ImageMetadata.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ImageUrlsDTO.ts create mode 100644 invokeai/frontend/web/src/services/api/models/PaginatedResults_ImageDTO_.ts delete mode 100644 invokeai/frontend/web/src/services/api/services/FilesService.ts delete mode 100644 invokeai/frontend/web/src/services/api/services/RecordsService.ts diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index ada9153de5..0b97a97fb7 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -29,11 +29,14 @@ export type { GraphInvocation } from './models/GraphInvocation'; export type { GraphInvocationOutput } from './models/GraphInvocationOutput'; export type { HTTPValidationError } from './models/HTTPValidationError'; export type { ImageCategory } from './models/ImageCategory'; +export type { ImageDTO } from './models/ImageDTO'; export type { ImageField } from './models/ImageField'; +export type { ImageMetadata } from './models/ImageMetadata'; export type { ImageOutput } from './models/ImageOutput'; export type { ImageToImageInvocation } from './models/ImageToImageInvocation'; export type { ImageToLatentsInvocation } from './models/ImageToLatentsInvocation'; export type { ImageType } from './models/ImageType'; +export type { ImageUrlsDTO } from './models/ImageUrlsDTO'; export type { InfillColorInvocation } from './models/InfillColorInvocation'; export type { InfillPatchMatchInvocation } from './models/InfillPatchMatchInvocation'; export type { InfillTileInvocation } from './models/InfillTileInvocation'; @@ -56,6 +59,7 @@ export type { MultiplyInvocation } from './models/MultiplyInvocation'; export type { NoiseInvocation } from './models/NoiseInvocation'; export type { NoiseOutput } from './models/NoiseOutput'; export type { PaginatedResults_GraphExecutionState_ } from './models/PaginatedResults_GraphExecutionState_'; +export type { PaginatedResults_ImageDTO_ } from './models/PaginatedResults_ImageDTO_'; export type { ParamIntInvocation } from './models/ParamIntInvocation'; export type { PasteImageInvocation } from './models/PasteImageInvocation'; export type { PromptOutput } from './models/PromptOutput'; @@ -73,8 +77,6 @@ export type { UpscaleInvocation } from './models/UpscaleInvocation'; export type { VaeRepo } from './models/VaeRepo'; export type { ValidationError } from './models/ValidationError'; -export { FilesService } from './services/FilesService'; export { ImagesService } from './services/ImagesService'; export { ModelsService } from './services/ModelsService'; -export { RecordsService } from './services/RecordsService'; export { SessionsService } from './services/SessionsService'; diff --git a/invokeai/frontend/web/src/services/api/models/ImageDTO.ts b/invokeai/frontend/web/src/services/api/models/ImageDTO.ts new file mode 100644 index 0000000000..311a4a30a6 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageDTO.ts @@ -0,0 +1,50 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageCategory } from './ImageCategory'; +import type { ImageMetadata } from './ImageMetadata'; +import type { ImageType } from './ImageType'; + +/** + * Deserialized image record with URLs. + */ +export type ImageDTO = { + /** + * The name of the image. + */ + image_name: string; + /** + * The type of the image. + */ + image_type: ImageType; + /** + * The URL of the image. + */ + image_url: string; + /** + * The thumbnail URL of the image. + */ + thumbnail_url: string; + /** + * The category of the image. + */ + image_category: ImageCategory; + /** + * The created timestamp of the image. + */ + created_at: string; + /** + * The session ID. + */ + session_id?: string; + /** + * The node ID. + */ + node_id?: string; + /** + * The image's metadata. + */ + metadata?: ImageMetadata; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ImageMetadata.ts b/invokeai/frontend/web/src/services/api/models/ImageMetadata.ts new file mode 100644 index 0000000000..023d75fc56 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageMetadata.ts @@ -0,0 +1,68 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Core generation metadata for an image/tensor generated in InvokeAI. + * + * Also includes any metadata from the image's PNG tEXt chunks. + * + * Generated by traversing the execution graph, collecting the parameters of the nearest ancestors of a given node. + * + * Full metadata may be accessed by querying for the session in the `graph_executions` table. + */ +export type ImageMetadata = { + /** + * The positive conditioning. + */ + positive_conditioning?: string; + /** + * The negative conditioning. + */ + negative_conditioning?: string; + /** + * Width of the image/tensor in pixels. + */ + width?: number; + /** + * Height of the image/tensor in pixels. + */ + height?: number; + /** + * The seed used for noise generation. + */ + seed?: number; + /** + * The classifier-free guidance scale. + */ + cfg_scale?: number; + /** + * The number of steps used for inference. + */ + steps?: number; + /** + * The scheduler used for inference. + */ + scheduler?: string; + /** + * The model used for inference. + */ + model?: string; + /** + * The strength used for image-to-image/tensor-to-tensor. + */ + strength?: number; + /** + * The ID of the initial image. + */ + image?: string; + /** + * The ID of the initial tensor. + */ + tensor?: string; + /** + * Extra metadata, extracted from the PNG tEXt chunk. + */ + extra?: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ImageUrlsDTO.ts b/invokeai/frontend/web/src/services/api/models/ImageUrlsDTO.ts new file mode 100644 index 0000000000..ea77c8af21 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageUrlsDTO.ts @@ -0,0 +1,28 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageType } from './ImageType'; + +/** + * The URLs for an image and its thumbnaill + */ +export type ImageUrlsDTO = { + /** + * The name of the image. + */ + image_name: string; + /** + * The type of the image. + */ + image_type: ImageType; + /** + * The URL of the image. + */ + image_url: string; + /** + * The thumbnail URL of the image. + */ + thumbnail_url: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/PaginatedResults_ImageDTO_.ts b/invokeai/frontend/web/src/services/api/models/PaginatedResults_ImageDTO_.ts new file mode 100644 index 0000000000..5d2bdae5ab --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/PaginatedResults_ImageDTO_.ts @@ -0,0 +1,32 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageDTO } from './ImageDTO'; + +/** + * Paginated results + */ +export type PaginatedResults_ImageDTO_ = { + /** + * Items + */ + items: Array; + /** + * Current Page + */ + page: number; + /** + * Total number of pages + */ + pages: number; + /** + * Number of items per page + */ + per_page: number; + /** + * Total number of items in result + */ + total: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/services/FilesService.ts b/invokeai/frontend/web/src/services/api/services/FilesService.ts deleted file mode 100644 index 0a4d3b3a1f..0000000000 --- a/invokeai/frontend/web/src/services/api/services/FilesService.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ImageType } from '../models/ImageType'; - -import type { CancelablePromise } from '../core/CancelablePromise'; -import { OpenAPI } from '../core/OpenAPI'; -import { request as __request } from '../core/request'; - -export class FilesService { - - /** - * Get Image - * Gets an image - * @returns any Successful Response - * @throws ApiError - */ - public static getImage({ - imageType, - imageName, - }: { - /** - * The type of the image to get - */ - imageType: ImageType, - /** - * The id of the image to get - */ - imageName: string, - }): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/files/images/{image_type}/{image_name}', - path: { - 'image_type': imageType, - 'image_name': imageName, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - - /** - * Get Thumbnail - * Gets a thumbnail - * @returns any Successful Response - * @throws ApiError - */ - public static getThumbnail({ - imageType, - imageName, - }: { - /** - * The type of the image whose thumbnail to get - */ - imageType: ImageType, - /** - * The id of the image whose thumbnail to get - */ - imageName: string, - }): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/files/images/{image_type}/{image_name}/thumbnail', - path: { - 'image_type': imageType, - 'image_name': imageName, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - -} diff --git a/invokeai/frontend/web/src/services/api/services/ImagesService.ts b/invokeai/frontend/web/src/services/api/services/ImagesService.ts index 3af7d5b53e..7d567adc24 100644 --- a/invokeai/frontend/web/src/services/api/services/ImagesService.ts +++ b/invokeai/frontend/web/src/services/api/services/ImagesService.ts @@ -3,7 +3,10 @@ /* eslint-disable */ import type { Body_upload_image } from '../models/Body_upload_image'; import type { ImageCategory } from '../models/ImageCategory'; +import type { ImageDTO } from '../models/ImageDTO'; import type { ImageType } from '../models/ImageType'; +import type { ImageUrlsDTO } from '../models/ImageUrlsDTO'; +import type { PaginatedResults_ImageDTO_ } from '../models/PaginatedResults_ImageDTO_'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; @@ -11,106 +14,10 @@ import { request as __request } from '../core/request'; export class ImagesService { - /** - * Get Image - * Gets an image - * @returns any Successful Response - * @throws ApiError - */ - public static getImage({ - imageType, - imageName, - }: { - /** - * The type of the image to get - */ - imageType: ImageType, - /** - * The id of the image to get - */ - imageName: string, - }): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/files/images/{image_type}/{image_name}', - path: { - 'image_type': imageType, - 'image_name': imageName, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - - /** - * Get Thumbnail - * Gets a thumbnail - * @returns any Successful Response - * @throws ApiError - */ - public static getThumbnail({ - imageType, - imageName, - }: { - /** - * The type of the image whose thumbnail to get - */ - imageType: ImageType, - /** - * The id of the image whose thumbnail to get - */ - imageName: string, - }): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/files/images/{image_type}/{image_name}/thumbnail', - path: { - 'image_type': imageType, - 'image_name': imageName, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - - /** - * Get Image Record - * Gets an image record by id - * @returns any Successful Response - * @throws ApiError - */ - public static getImageRecord({ - imageType, - imageName, - }: { - /** - * The type of the image record to get - */ - imageType: ImageType, - /** - * The id of the image record to get - */ - imageName: string, - }): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/images/records/{image_type}/{image_name}', - path: { - 'image_type': imageType, - 'image_name': imageName, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - /** * List Image Records * Gets a list of image records by type and category - * @returns any Successful Response + * @returns PaginatedResults_ImageDTO_ Successful Response * @throws ApiError */ public static listImageRecords({ @@ -135,10 +42,10 @@ export class ImagesService { * The number of image records per page */ perPage?: number, - }): CancelablePromise { + }): CancelablePromise { return __request(OpenAPI, { method: 'GET', - url: '/api/v1/images/records/', + url: '/api/v1/images/', query: { 'image_type': imageType, 'image_category': imageCategory, @@ -211,4 +118,132 @@ export class ImagesService { }); } + /** + * Get Image Record + * Gets an image record by id + * @returns ImageDTO Successful Response + * @throws ApiError + */ + public static getImageRecord({ + imageType, + imageName, + }: { + /** + * The type of the image record to get + */ + imageType: ImageType, + /** + * The id of the image record to get + */ + imageName: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/images/{image_type}/{image_name}/record', + path: { + 'image_type': imageType, + 'image_name': imageName, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Get Image + * Gets an image + * @returns any Successful Response + * @throws ApiError + */ + public static getImage({ + imageType, + imageName, + }: { + /** + * The type of the image to get + */ + imageType: ImageType, + /** + * The id of the image to get + */ + imageName: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/images/{image_type}/{image_name}/image', + path: { + 'image_type': imageType, + 'image_name': imageName, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Get Thumbnail + * Gets a thumbnail + * @returns any Successful Response + * @throws ApiError + */ + public static getThumbnail({ + imageType, + imageName, + }: { + /** + * The type of the image whose thumbnail to get + */ + imageType: ImageType, + /** + * The id of the image whose thumbnail to get + */ + imageName: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/images/{image_type}/{image_name}/thumbnail', + path: { + 'image_type': imageType, + 'image_name': imageName, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Get Image Urls + * Gets an image and thumbnail URL + * @returns ImageUrlsDTO Successful Response + * @throws ApiError + */ + public static getImageUrls({ + imageType, + imageName, + }: { + /** + * The type of the image whose URL to get + */ + imageType: ImageType, + /** + * The id of the image whose URL to get + */ + imageName: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/images/{image_type}/{image_name}/urls', + path: { + 'image_type': imageType, + 'image_name': imageName, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + } diff --git a/invokeai/frontend/web/src/services/api/services/RecordsService.ts b/invokeai/frontend/web/src/services/api/services/RecordsService.ts deleted file mode 100644 index 4079f60ab5..0000000000 --- a/invokeai/frontend/web/src/services/api/services/RecordsService.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ImageCategory } from '../models/ImageCategory'; -import type { ImageType } from '../models/ImageType'; - -import type { CancelablePromise } from '../core/CancelablePromise'; -import { OpenAPI } from '../core/OpenAPI'; -import { request as __request } from '../core/request'; - -export class RecordsService { - - /** - * Get Image Record - * Gets an image record by id - * @returns any Successful Response - * @throws ApiError - */ - public static getImageRecord({ - imageType, - imageName, - }: { - /** - * The type of the image record to get - */ - imageType: ImageType, - /** - * The id of the image record to get - */ - imageName: string, - }): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/images/records/{image_type}/{image_name}', - path: { - 'image_type': imageType, - 'image_name': imageName, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - - /** - * List Image Records - * Gets a list of image records by type and category - * @returns any Successful Response - * @throws ApiError - */ - public static listImageRecords({ - imageType, - imageCategory, - page, - perPage = 10, - }: { - /** - * The type of image records to get - */ - imageType: ImageType, - /** - * The kind of image records to get - */ - imageCategory: ImageCategory, - /** - * The page of image records to get - */ - page?: number, - /** - * The number of image records per page - */ - perPage?: number, - }): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/images/records/', - query: { - 'image_type': imageType, - 'image_category': imageCategory, - 'page': page, - 'per_page': perPage, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - -} From 5de3c41d195f829db2bdcc20a4a2aacda288d23e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 22 May 2023 15:48:12 +1000 Subject: [PATCH 25/72] feat(nodes): add metadata handling --- invokeai/app/api/dependencies.py | 5 +- invokeai/app/api/models/images.py | 7 +- invokeai/app/invocations/latent.py | 33 +- invokeai/app/models/metadata.py | 44 ++- invokeai/app/services/graph.py | 7 + invokeai/app/services/image_file_storage.py | 16 +- invokeai/app/services/images.py | 76 ++-- invokeai/app/services/metadata.py | 355 ++++++------------- invokeai/app/services/models/image_record.py | 7 +- 9 files changed, 228 insertions(+), 322 deletions(-) diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index 1ad53f31ca..ae351d4476 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -4,6 +4,7 @@ from logging import Logger import os from invokeai.app.services.image_record_storage import SqliteImageRecordStorage from invokeai.app.services.images import ImageService +from invokeai.app.services.metadata import CoreMetadataService from invokeai.app.services.urls import LocalUrlService from invokeai.backend.util.logging import InvokeAILogger @@ -18,7 +19,6 @@ from ..services.invocation_services import InvocationServices from ..services.invoker import Invoker from ..services.processor import DefaultInvocationProcessor from ..services.sqlite import SqliteItemStorage -from ..services.metadata import PngMetadataService from .events import FastAPIEventService @@ -59,7 +59,7 @@ class ApiDependencies: DiskLatentsStorage(f"{output_folder}/latents") ) - metadata = PngMetadataService() + metadata = CoreMetadataService() urls = LocalUrlService() @@ -80,6 +80,7 @@ class ApiDependencies: metadata=metadata, url=urls, logger=logger, + graph_execution_manager=graph_execution_manager, ) services = InvocationServices( diff --git a/invokeai/app/api/models/images.py b/invokeai/app/api/models/images.py index 866e181561..fa04702326 100644 --- a/invokeai/app/api/models/images.py +++ b/invokeai/app/api/models/images.py @@ -2,7 +2,6 @@ from typing import Optional from pydantic import BaseModel, Field from invokeai.app.models.image import ImageType -from invokeai.app.services.metadata import InvokeAIMetadata class ImageResponseMetadata(BaseModel): @@ -11,9 +10,9 @@ class ImageResponseMetadata(BaseModel): created: int = Field(description="The creation timestamp of the image") width: int = Field(description="The width of the image in pixels") height: int = Field(description="The height of the image in pixels") - invokeai: Optional[InvokeAIMetadata] = Field( - description="The image's InvokeAI-specific metadata" - ) + # invokeai: Optional[InvokeAIMetadata] = Field( + # description="The image's InvokeAI-specific metadata" + # ) class ImageResponse(BaseModel): diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 64993e011a..7259beb1a8 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -7,6 +7,7 @@ from pydantic import BaseModel, Field import torch from invokeai.app.invocations.util.choose_model import choose_model +from invokeai.app.models.image import ImageCategory from invokeai.app.util.misc import SEED_MAX, get_random_seed from invokeai.app.util.step_callback import stable_diffusion_step_callback @@ -356,20 +357,30 @@ class LatentsToImageInvocation(BaseInvocation): np_image = model.decode_latents(latents) image = model.numpy_to_pil(np_image)[0] - image_type = ImageType.RESULT - image_name = context.services.images.create_name( - context.graph_execution_state_id, self.id + # image_type = ImageType.RESULT + # image_name = context.services.images.create_name( + # context.graph_execution_state_id, self.id + # ) + + # metadata = context.services.metadata.build_metadata( + # session_id=context.graph_execution_state_id, node=self + # ) + + # torch.cuda.empty_cache() + + # context.services.images.save(image_type, image_name, image, metadata) + image_dto = context.services.images_new.create( + image=image, + image_type=ImageType.RESULT, + image_category=ImageCategory.IMAGE, + session_id=context.graph_execution_state_id, + node_id=self.id, ) - metadata = context.services.metadata.build_metadata( - session_id=context.graph_execution_state_id, node=self - ) - - torch.cuda.empty_cache() - - context.services.images.save(image_type, image_name, image, metadata) return build_image_output( - image_type=image_type, image_name=image_name, image=image + image_type=image_dto.image_type, + image_name=image_dto.image_name, + image=image, ) diff --git a/invokeai/app/models/metadata.py b/invokeai/app/models/metadata.py index 35998fa27e..481f2c1ff6 100644 --- a/invokeai/app/models/metadata.py +++ b/invokeai/app/models/metadata.py @@ -1,5 +1,5 @@ from typing import Optional -from pydantic import BaseModel, Field, StrictFloat, StrictInt, StrictStr +from pydantic import BaseModel, Extra, Field, StrictFloat, StrictInt, StrictStr class ImageMetadata(BaseModel): @@ -8,11 +8,24 @@ class ImageMetadata(BaseModel): Also includes any metadata from the image's PNG tEXt chunks. - Generated by traversing the execution graph, collecting the parameters of the nearest ancestors of a given node. + Generated by traversing the execution graph, collecting the parameters of the nearest ancestors + of a given node. Full metadata may be accessed by querying for the session in the `graph_executions` table. """ + class Config: + extra = Extra.allow + """ + This lets the ImageMetadata class accept arbitrary additional fields. The CoreMetadataService + won't add any fields that are not already defined, but other a different metadata service + implementation might. + """ + + type: Optional[StrictStr] = Field( + default=None, + description="The type of the ancestor node of the image output node.", + ) positive_conditioning: Optional[StrictStr] = Field( default=None, description="The positive conditioning." ) @@ -20,10 +33,10 @@ class ImageMetadata(BaseModel): default=None, description="The negative conditioning." ) width: Optional[StrictInt] = Field( - default=None, description="Width of the image/tensor in pixels." + default=None, description="Width of the image/latents in pixels." ) height: Optional[StrictInt] = Field( - default=None, description="Height of the image/tensor in pixels." + default=None, description="Height of the image/latents in pixels." ) seed: Optional[StrictInt] = Field( default=None, description="The seed used for noise generation." @@ -42,18 +55,21 @@ class ImageMetadata(BaseModel): ) strength: Optional[StrictFloat] = Field( default=None, - description="The strength used for image-to-image/tensor-to-tensor.", + description="The strength used for image-to-image/latents-to-latents.", ) - image: Optional[StrictStr] = Field( - default=None, description="The ID of the initial image." + latents: Optional[StrictStr] = Field( + default=None, description="The ID of the initial latents." ) - tensor: Optional[StrictStr] = Field( - default=None, description="The ID of the initial tensor." + vae: Optional[StrictStr] = Field( + default=None, description="The VAE used for decoding." + ) + unet: Optional[StrictStr] = Field( + default=None, description="The UNet used dor inference." + ) + clip: Optional[StrictStr] = Field( + default=None, description="The CLIP Encoder used for conditioning." ) - # Pending model refactor: - # vae: Optional[str] = Field(default=None,description="The VAE used for decoding.") - # unet: Optional[str] = Field(default=None,description="The UNet used dor inference.") - # clip: Optional[str] = Field(default=None,description="The CLIP Encoder used for conditioning.") extra: Optional[StrictStr] = Field( - default=None, description="Extra metadata, extracted from the PNG tEXt chunk." + default=None, + description="Uploaded image metadata, extracted from the PNG tEXt chunk.", ) diff --git a/invokeai/app/services/graph.py b/invokeai/app/services/graph.py index ab6e4ed49d..44688ada0a 100644 --- a/invokeai/app/services/graph.py +++ b/invokeai/app/services/graph.py @@ -713,6 +713,13 @@ class Graph(BaseModel): g.add_edges_from(set([(e.source.node_id, e.destination.node_id) for e in self.edges])) return g + def nx_graph_with_data(self) -> nx.DiGraph: + """Returns a NetworkX DiGraph representing the data and layout of this graph""" + g = nx.DiGraph() + g.add_nodes_from([n for n in self.nodes.items()]) + g.add_edges_from(set([(e.source.node_id, e.destination.node_id) for e in self.edges])) + return g + def nx_graph_flat( self, nx_graph: Optional[nx.DiGraph] = None, prefix: Optional[str] = None ) -> nx.DiGraph: diff --git a/invokeai/app/services/image_file_storage.py b/invokeai/app/services/image_file_storage.py index 3a99940068..dadb9584d5 100644 --- a/invokeai/app/services/image_file_storage.py +++ b/invokeai/app/services/image_file_storage.py @@ -6,11 +6,11 @@ from queue import Queue from typing import Dict, Optional from PIL.Image import Image as PILImageType -from PIL import Image -from PIL.PngImagePlugin import PngInfo +from PIL import Image, PngImagePlugin from send2trash import send2trash from invokeai.app.models.image import ImageType +from invokeai.app.models.metadata import ImageMetadata from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail @@ -54,7 +54,7 @@ class ImageFileStorageBase(ABC): image: PILImageType, image_type: ImageType, image_name: str, - pnginfo: Optional[PngInfo] = None, + metadata: Optional[ImageMetadata] = 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.""" @@ -109,12 +109,18 @@ class DiskImageFileStorage(ImageFileStorageBase): image: PILImageType, image_type: ImageType, image_name: str, - pnginfo: Optional[PngInfo] = None, + metadata: Optional[ImageMetadata] = None, thumbnail_size: int = 256, ) -> None: try: image_path = self.get_path(image_type, image_name) - image.save(image_path, "PNG", pnginfo=pnginfo) + + if metadata is not None: + pnginfo = PngImagePlugin.PngInfo() + pnginfo.add_text("invokeai", metadata.json()) + image.save(image_path, "PNG", pnginfo=pnginfo) + else: + image.save(image_path, "PNG") thumbnail_name = get_thumbnail_name(image_name) thumbnail_path = self.get_path(image_type, thumbnail_name, thumbnail=True) diff --git a/invokeai/app/services/images.py b/invokeai/app/services/images.py index 9b46ebcc09..fc4c85fbdf 100644 --- a/invokeai/app/services/images.py +++ b/invokeai/app/services/images.py @@ -1,10 +1,9 @@ from abc import ABC, abstractmethod import json from logging import Logger -from typing import Optional, Union +from typing import Optional, TYPE_CHECKING, Union import uuid from PIL.Image import Image as PILImageType -from PIL import PngImagePlugin from invokeai.app.models.image import ImageCategory, ImageType from invokeai.app.models.metadata import ImageMetadata @@ -17,12 +16,16 @@ from invokeai.app.services.models.image_record import ( image_record_to_dto, ) from invokeai.app.services.image_file_storage import ImageFileStorageBase -from invokeai.app.services.item_storage import PaginatedResults +from invokeai.app.services.item_storage import ItemStorageABC, PaginatedResults from invokeai.app.services.metadata import MetadataServiceBase from invokeai.app.services.urls import UrlServiceBase from invokeai.app.util.misc import get_iso_timestamp +if TYPE_CHECKING: + from invokeai.app.services.graph import GraphExecutionState + + class ImageServiceABC(ABC): """ High-level service for image management. @@ -59,7 +62,9 @@ class ImageServiceABC(ABC): pass @abstractmethod - def get_url(self, image_type: ImageType, image_name: str, thumbnail: bool = False) -> str: + def get_url( + self, image_type: ImageType, image_name: str, thumbnail: bool = False + ) -> str: """Gets an image's or thumbnail's URL""" pass @@ -113,6 +118,7 @@ class ImageServiceDependencies: metadata: MetadataServiceBase urls: UrlServiceBase logger: Logger + graph_execution_manager: ItemStorageABC["GraphExecutionState"] def __init__( self, @@ -121,12 +127,14 @@ class ImageServiceDependencies: metadata: MetadataServiceBase, url: UrlServiceBase, logger: Logger, + graph_execution_manager: ItemStorageABC["GraphExecutionState"], ): self.records = image_record_storage self.files = image_file_storage self.metadata = metadata self.urls = url self.logger = logger + self.graph_execution_manager = graph_execution_manager class ImageService(ImageServiceABC): @@ -139,6 +147,7 @@ class ImageService(ImageServiceABC): metadata: MetadataServiceBase, url: UrlServiceBase, logger: Logger, + graph_execution_manager: ItemStorageABC["GraphExecutionState"], ): self._services = ImageServiceDependencies( image_record_storage=image_record_storage, @@ -146,6 +155,7 @@ class ImageService(ImageServiceABC): metadata=metadata, url=url, logger=logger, + graph_execution_manager=graph_execution_manager, ) def create( @@ -155,7 +165,6 @@ class ImageService(ImageServiceABC): image_category: ImageCategory, node_id: Optional[str] = None, session_id: Optional[str] = None, - metadata: Optional[ImageMetadata] = None, ) -> ImageDTO: image_name = self._create_image_name( image_type=image_type, @@ -165,12 +174,7 @@ class ImageService(ImageServiceABC): ) timestamp = get_iso_timestamp() - - if metadata is not None: - pnginfo = PngImagePlugin.PngInfo() - pnginfo.add_text("invokeai", json.dumps(metadata)) - else: - pnginfo = None + metadata = self._get_metadata(session_id, node_id) try: # TODO: Consider using a transaction here to ensure consistency between storage and database @@ -178,7 +182,7 @@ class ImageService(ImageServiceABC): image_type=image_type, image_name=image_name, image=image, - pnginfo=pnginfo, + metadata=metadata, ) self._services.records.save( @@ -237,24 +241,6 @@ class ImageService(ImageServiceABC): self._services.logger.error("Problem getting image record") raise e - def get_path( - self, image_type: ImageType, image_name: str, thumbnail: bool = False - ) -> str: - try: - return self._services.files.get_path(image_type, image_name, thumbnail) - except Exception as e: - self._services.logger.error("Problem getting image path") - raise e - - def get_url( - self, image_type: ImageType, image_name: str, thumbnail: bool = False - ) -> str: - try: - return self._services.urls.get_image_url(image_type, image_name, thumbnail) - except Exception as e: - self._services.logger.error("Problem getting image path") - raise e - def get_dto(self, image_type: ImageType, image_name: str) -> ImageDTO: try: image_record = self._services.records.get(image_type, image_name) @@ -273,6 +259,24 @@ class ImageService(ImageServiceABC): self._services.logger.error("Problem getting image DTO") raise e + def get_path( + self, image_type: ImageType, image_name: str, thumbnail: bool = False + ) -> str: + try: + return self._services.files.get_path(image_type, image_name, thumbnail) + except Exception as e: + self._services.logger.error("Problem getting image path") + raise e + + def get_url( + self, image_type: ImageType, image_name: str, thumbnail: bool = False + ) -> str: + try: + return self._services.urls.get_image_url(image_type, image_name, thumbnail) + except Exception as e: + self._services.logger.error("Problem getting image path") + raise e + def get_many( self, image_type: ImageType, @@ -353,3 +357,15 @@ class ImageService(ImageServiceABC): return f"{image_type.value}_{image_category.value}_{session_id}_{node_id}_{uuid_str}.png" return f"{image_type.value}_{image_category.value}_{uuid_str}.png" + + def _get_metadata( + self, session_id: Optional[str] = None, node_id: Optional[str] = None + ) -> Union[ImageMetadata, None]: + """Get the metadata for a node.""" + metadata = None + + if node_id is not None and session_id is not None: + session = self._services.graph_execution_manager.get(session_id) + metadata = self._services.metadata.create_image_metadata(session, node_id) + + return metadata diff --git a/invokeai/app/services/metadata.py b/invokeai/app/services/metadata.py index 40ec189cd0..07509f4e3c 100644 --- a/invokeai/app/services/metadata.py +++ b/invokeai/app/services/metadata.py @@ -1,295 +1,142 @@ import json from abc import ABC, abstractmethod -from typing import Any, Dict, Optional, TypedDict -from PIL import Image, PngImagePlugin -from pydantic import BaseModel +from typing import Any, Union +import networkx as nx -from invokeai.app.models.image import ImageType, is_image_type - - -class MetadataImageField(TypedDict): - """Pydantic-less ImageField, used for metadata parsing.""" - - image_type: ImageType - image_name: str - - -class MetadataLatentsField(TypedDict): - """Pydantic-less LatentsField, used for metadata parsing.""" - - latents_name: str - - -class MetadataColorField(TypedDict): - """Pydantic-less ColorField, used for metadata parsing""" - - r: int - g: int - b: int - a: int - - -# TODO: This is a placeholder for `InvocationsUnion` pending resolution of circular imports -NodeMetadata = Dict[ - str, - None - | str - | int - | float - | bool - | MetadataImageField - | MetadataLatentsField - | MetadataColorField, -] - - -class InvokeAIMetadata(TypedDict, total=False): - """InvokeAI-specific metadata format.""" - - session_id: Optional[str] - node: Optional[NodeMetadata] - - -def build_invokeai_metadata_pnginfo( - metadata: InvokeAIMetadata | None, -) -> PngImagePlugin.PngInfo: - """Builds a PngInfo object with key `"invokeai"` and value `metadata`""" - pnginfo = PngImagePlugin.PngInfo() - - if metadata is not None: - pnginfo.add_text("invokeai", json.dumps(metadata)) - - return pnginfo +from invokeai.app.models.metadata import ImageMetadata +from invokeai.app.services.graph import Edge, Graph, GraphExecutionState class MetadataServiceBase(ABC): - @abstractmethod - def get_metadata(self, image: Image.Image) -> InvokeAIMetadata | None: - """Gets the InvokeAI metadata from a PIL Image, skipping invalid values""" - pass + """Handles building metadata for nodes, images, and outputs.""" @abstractmethod - def build_metadata( - self, session_id: str, node: BaseModel - ) -> InvokeAIMetadata | None: - """Builds an InvokeAIMetadata object""" + def create_image_metadata( + self, session: GraphExecutionState, node_id: str + ) -> ImageMetadata: + """Builds an ImageMetadata object for a node.""" pass - # @abstractmethod - # def create_metadata(self, session_id: str, node_id: str) -> dict: - # """Creates metadata for a result""" - # pass -class PngMetadataService(MetadataServiceBase): - """Handles loading and building metadata for images.""" +class CoreMetadataService(MetadataServiceBase): + _ANCESTOR_TYPES = ["t2l", "l2l"] + """The ancestor types that contain the core metadata""" - # TODO: Use `InvocationsUnion` to **validate** metadata as representing a fully-functioning node - def _load_metadata(self, image: Image.Image) -> dict | None: - """Loads a specific info entry from a PIL Image.""" + _ANCESTOR_PARAMS = ["type", "steps", "model", "cfg_scale", "scheduler", "strength"] + """The core metadata parameters in the ancestor types""" - try: - info = image.info.get("invokeai") + _NOISE_FIELDS = ["seed", "width", "height"] + """The core metadata parameters in the noise node""" - if type(info) is not str: - return None - - loaded_metadata = json.loads(info) - - if type(loaded_metadata) is not dict: - return None - - if len(loaded_metadata.items()) == 0: - return None - - return loaded_metadata - except: - return None - - def get_metadata(self, image: Image.Image) -> dict | None: - """Retrieves an image's metadata as a dict""" - loaded_metadata = self._load_metadata(image) - - return loaded_metadata - - def build_metadata(self, session_id: str, node: BaseModel) -> InvokeAIMetadata: - metadata = InvokeAIMetadata(session_id=session_id, node=node.dict()) + def create_image_metadata( + self, session: GraphExecutionState, node_id: str + ) -> ImageMetadata: + metadata = self._build_metadata_from_graph(session, node_id) return metadata + def _find_nearest_ancestor(self, G: nx.DiGraph, node_id: str) -> Union[str, None]: + """ + Finds the id of the nearest ancestor (of a valid type) of a given node. -from enum import Enum + Parameters: + G (nx.DiGraph): The execution graph, converted in to a networkx DiGraph. Its nodes must + have the same data as the execution graph. + node_id (str): The ID of the node. -from abc import ABC, abstractmethod -import json -import sqlite3 -from threading import Lock -from typing import Any, Union + Returns: + str | None: The ID of the nearest ancestor, or None if there are no valid ancestors. + """ -import networkx as nx + # Retrieve the node from the graph + node = G.nodes[node_id] -from pydantic import BaseModel, Field, parse_obj_as, parse_raw_as -from invokeai.app.invocations.image import ImageOutput -from invokeai.app.services.graph import Edge, GraphExecutionState -from invokeai.app.invocations.latent import LatentsOutput -from invokeai.app.services.item_storage import PaginatedResults -from invokeai.app.util.misc import get_timestamp + # If the node type is one of the core metadata node types, return its id + if node.get("type") in self._ANCESTOR_TYPES: + return node.get("id") + # Else, look for the ancestor in the predecessor nodes + for predecessor in G.predecessors(node_id): + result = self._find_nearest_ancestor(G, predecessor) + if result: + return result -class ResultType(str, Enum): - image_output = "image_output" - latents_output = "latents_output" + # If there are no valid ancestors, return None + return None + def _get_additional_metadata( + self, graph: Graph, node_id: str + ) -> Union[dict[str, Any], None]: + """ + Returns additional metadata for a given node. -class Result(BaseModel): - """A session result""" + Parameters: + graph (Graph): The execution graph. + node_id (str): The ID of the node. - id: str = Field(description="Result ID") - session_id: str = Field(description="Session ID") - node_id: str = Field(description="Node ID") - data: Union[LatentsOutput, ImageOutput] = Field(description="The result data") + Returns: + dict[str, Any] | None: A dictionary of additional metadata. + """ + metadata = {} -class ResultWithSession(BaseModel): - """A result with its session""" + # Iterate over all edges in the graph + for edge in graph.edges: + dest_node_id = edge.destination.node_id + dest_field = edge.destination.field + source_node_dict = graph.nodes[edge.source.node_id].dict() - result: Result = Field(description="The result") - session: GraphExecutionState = Field(description="The session") + # If the destination node ID matches the given node ID, gather necessary metadata + if dest_node_id == node_id: + # If the destination field is 'positive_conditioning', add the 'prompt' from the source node + if dest_field == "positive_conditioning": + metadata["positive_conditioning"] = source_node_dict.get("prompt") + # If the destination field is 'negative_conditioning', add the 'prompt' from the source node + if dest_field == "negative_conditioning": + metadata["negative_conditioning"] = source_node_dict.get("prompt") + # If the destination field is 'noise', add the core noise fields from the source node + if dest_field == "noise": + for field in self._NOISE_FIELDS: + metadata[field] = source_node_dict.get(field) + return metadata + def _build_metadata_from_graph( + self, session: GraphExecutionState, node_id: str + ) -> ImageMetadata: + """ + Builds an ImageMetadata object for a node. -# # Create a directed graph -# from typing import Any, TypedDict, Union -# from networkx import DiGraph -# import networkx as nx -# import json + Parameters: + session (GraphExecutionState): The session. + node_id (str): The ID of the node. + Returns: + ImageMetadata: The metadata for the node. + """ -# # We need to use a loose class for nodes to allow for graceful parsing - we cannot use the stricter -# # model used by the system, because we may be a graph in an old format. We can, however, use the -# # Edge model, because the edge format does not change. -# class LooseGraph(BaseModel): -# id: str -# nodes: dict[str, dict[str, Any]] -# edges: list[Edge] + graph = session.execution_graph + # Find the nearest ancestor of the given node + ancestor_id = self._find_nearest_ancestor(graph.nx_graph_with_data(), node_id) -# # An intermediate type used during parsing -# class NearestAncestor(TypedDict): -# node_id: str -# metadata: dict[str, Any] + # If no ancestor was found, return an empty ImageMetadata object + if ancestor_id is None: + return ImageMetadata() + ancestor_node = graph.get_node(ancestor_id) -# # The ancestor types that contain the core metadata -# ANCESTOR_TYPES = ['t2l', 'l2l'] + ancestor_metadata = { + param: val + for param, val in ancestor_node.dict().items() + if param in self._ANCESTOR_PARAMS + } -# # The core metadata parameters in the ancestor types -# ANCESTOR_PARAMS = ['steps', 'model', 'cfg_scale', 'scheduler', 'strength'] + # Get additional metadata related to the ancestor + addl_metadata = self._get_additional_metadata(graph, ancestor_id) -# # The core metadata parameters in the noise node -# NOISE_FIELDS = ['seed', 'width', 'height'] + # If additional metadata was found, add it to the main metadata + if addl_metadata is not None: + ancestor_metadata.update(addl_metadata) -# # Find nearest t2l or l2l ancestor from a given l2i node -# def find_nearest_ancestor(G: DiGraph, node_id: str) -> Union[NearestAncestor, None]: -# """Returns metadata for the nearest ancestor of a given node. - -# Parameters: -# G (DiGraph): A directed graph. -# node_id (str): The ID of the starting node. - -# Returns: -# NearestAncestor | None: An object with the ID and metadata of the nearest ancestor. -# """ - -# # Retrieve the node from the graph -# node = G.nodes[node_id] - -# # If the node type is one of the core metadata node types, gather necessary metadata and return -# if node.get('type') in ANCESTOR_TYPES: -# parsed_metadata = {param: val for param, val in node.items() if param in ANCESTOR_PARAMS} -# return NearestAncestor(node_id=node_id, metadata=parsed_metadata) - - -# # Else, look for the ancestor in the predecessor nodes -# for predecessor in G.predecessors(node_id): -# result = find_nearest_ancestor(G, predecessor) -# if result: -# return result - -# # If there are no valid ancestors, return None -# return None - - -# def get_additional_metadata(graph: LooseGraph, node_id: str) -> Union[dict[str, Any], None]: -# """Collects additional metadata from nodes connected to a given node. - -# Parameters: -# graph (LooseGraph): The graph. -# node_id (str): The ID of the node. - -# Returns: -# dict | None: A dictionary containing additional metadata. -# """ - -# metadata = {} - -# # Iterate over all edges in the graph -# for edge in graph.edges: -# dest_node_id = edge.destination.node_id -# dest_field = edge.destination.field -# source_node = graph.nodes[edge.source.node_id] - -# # If the destination node ID matches the given node ID, gather necessary metadata -# if dest_node_id == node_id: -# # If the destination field is 'positive_conditioning', add the 'prompt' from the source node -# if dest_field == 'positive_conditioning': -# metadata['positive_conditioning'] = source_node.get('prompt') -# # If the destination field is 'negative_conditioning', add the 'prompt' from the source node -# if dest_field == 'negative_conditioning': -# metadata['negative_conditioning'] = source_node.get('prompt') -# # If the destination field is 'noise', add the core noise fields from the source node -# if dest_field == 'noise': -# for field in NOISE_FIELDS: -# metadata[field] = source_node.get(field) -# return metadata - -# def build_core_metadata(graph_raw: str, node_id: str) -> Union[dict, None]: -# """Builds the core metadata for a given node. - -# Parameters: -# graph_raw (str): The graph structure as a raw string. -# node_id (str): The ID of the node. - -# Returns: -# dict | None: A dictionary containing core metadata. -# """ - -# # Create a directed graph to facilitate traversal -# G = nx.DiGraph() - -# # Convert the raw graph string into a JSON object -# graph = parse_obj_as(LooseGraph, graph_raw) - -# # Add nodes and edges to the graph -# for node_id, node_data in graph.nodes.items(): -# G.add_node(node_id, **node_data) -# for edge in graph.edges: -# G.add_edge(edge.source.node_id, edge.destination.node_id) - -# # Find the nearest ancestor of the given node -# ancestor = find_nearest_ancestor(G, node_id) - -# # If no ancestor was found, return None -# if ancestor is None: -# return None - -# metadata = ancestor['metadata'] -# ancestor_id = ancestor['node_id'] - -# # Get additional metadata related to the ancestor -# addl_metadata = get_additional_metadata(graph, ancestor_id) - -# # If additional metadata was found, add it to the main metadata -# if addl_metadata is not None: -# metadata.update(addl_metadata) - -# return metadata + return ImageMetadata(**ancestor_metadata) diff --git a/invokeai/app/services/models/image_record.py b/invokeai/app/services/models/image_record.py index 6e15574eb9..29a2d71232 100644 --- a/invokeai/app/services/models/image_record.py +++ b/invokeai/app/services/models/image_record.py @@ -62,9 +62,12 @@ def deserialize_image_record(image_row: sqlite3.Row) -> ImageRecord: image_type = ImageType(image_dict.get("image_type", ImageType.RESULT.value)) - raw_metadata = image_dict.get("metadata", "{}") + raw_metadata = image_dict.get("metadata") - metadata = ImageMetadata.parse_raw(raw_metadata) + if raw_metadata is not None: + metadata = ImageMetadata.parse_raw(raw_metadata) + else: + metadata = None return ImageRecord( image_name=image_dict.get("id", "unknown"), From 3f94f81acd67edace8250012e725a85092f5cc04 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 22 May 2023 16:11:06 +1000 Subject: [PATCH 26/72] chore(ui): regen api client --- .../src/services/api/models/ImageMetadata.ts | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/models/ImageMetadata.ts b/invokeai/frontend/web/src/services/api/models/ImageMetadata.ts index 023d75fc56..76c0155e97 100644 --- a/invokeai/frontend/web/src/services/api/models/ImageMetadata.ts +++ b/invokeai/frontend/web/src/services/api/models/ImageMetadata.ts @@ -7,11 +7,16 @@ * * Also includes any metadata from the image's PNG tEXt chunks. * - * Generated by traversing the execution graph, collecting the parameters of the nearest ancestors of a given node. + * Generated by traversing the execution graph, collecting the parameters of the nearest ancestors + * of a given node. * * Full metadata may be accessed by querying for the session in the `graph_executions` table. */ export type ImageMetadata = { + /** + * The type of the ancestor node of the image output node. + */ + type?: string; /** * The positive conditioning. */ @@ -21,11 +26,11 @@ export type ImageMetadata = { */ negative_conditioning?: string; /** - * Width of the image/tensor in pixels. + * Width of the image/latents in pixels. */ width?: number; /** - * Height of the image/tensor in pixels. + * Height of the image/latents in pixels. */ height?: number; /** @@ -49,19 +54,27 @@ export type ImageMetadata = { */ model?: string; /** - * The strength used for image-to-image/tensor-to-tensor. + * The strength used for image-to-image/latents-to-latents. */ strength?: number; /** - * The ID of the initial image. + * The ID of the initial latents. */ - image?: string; + latents?: string; /** - * The ID of the initial tensor. + * The VAE used for decoding. */ - tensor?: string; + vae?: string; /** - * Extra metadata, extracted from the PNG tEXt chunk. + * The UNet used dor inference. + */ + unet?: string; + /** + * The CLIP Encoder used for conditioning. + */ + clip?: string; + /** + * Uploaded image metadata, extracted from the PNG tEXt chunk. */ extra?: string; }; From 4e29a751d892ba50e3a5466ea7d3829ca0cc3995 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 22 May 2023 16:11:28 +1000 Subject: [PATCH 27/72] feat(ui): add POC image record fetching --- .../listeners/invocationComplete.ts | 32 +++++++++++++++++-- .../frontend/web/src/services/thunks/image.ts | 32 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts index 9d84b2cbf0..b5c391afe4 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts @@ -6,7 +6,12 @@ import { } from 'services/util/deserializeImageField'; import { Image } from 'app/types/invokeai'; import { resultAdded } from 'features/gallery/store/resultsSlice'; -import { imageReceived, thumbnailReceived } from 'services/thunks/image'; +import { + imageReceived, + imageRecordReceived, + imageUrlsReceived, + thumbnailReceived, +} from 'services/thunks/image'; import { startAppListening } from '..'; import { imageSelected } from 'features/gallery/store/gallerySlice'; import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; @@ -24,7 +29,7 @@ export const addImageResultReceivedListener = () => { } return false; }, - effect: (action, { getState, dispatch }) => { + effect: async (action, { getState, dispatch, take }) => { if (!invocationComplete.match(action)) { return; } @@ -35,6 +40,29 @@ export const addImageResultReceivedListener = () => { if (isImageOutput(result) && !nodeDenylist.includes(node.type)) { const name = result.image.image_name; const type = result.image.image_type; + + dispatch(imageUrlsReceived({ imageName: name, imageType: type })); + + const [{ payload }] = await take( + (action): action is ReturnType => + imageUrlsReceived.fulfilled.match(action) && + action.payload.image_name === name + ); + + console.log(payload); + + dispatch(imageRecordReceived({ imageName: name, imageType: type })); + + const [x] = await take( + ( + action + ): action is ReturnType => + imageRecordReceived.fulfilled.match(action) && + action.payload.image_name === name + ); + + console.log(x); + const state = getState(); // if we need to refetch, set URLs to placeholder for now diff --git a/invokeai/frontend/web/src/services/thunks/image.ts b/invokeai/frontend/web/src/services/thunks/image.ts index ec2533b61b..5528e41bfc 100644 --- a/invokeai/frontend/web/src/services/thunks/image.ts +++ b/invokeai/frontend/web/src/services/thunks/image.ts @@ -6,6 +6,38 @@ import { getHeaders } from 'services/util/getHeaders'; const imagesLog = log.child({ namespace: 'image' }); +type imageUrlsReceivedArg = Parameters< + (typeof ImagesService)['getImageUrls'] +>[0]; + +/** + * `ImagesService.getImageUrls()` thunk + */ +export const imageUrlsReceived = createAppAsyncThunk( + 'api/imageUrlsReceived', + async (arg: imageUrlsReceivedArg) => { + const response = await ImagesService.getImageUrls(arg); + imagesLog.info({ arg, response }, 'Received image urls'); + return response; + } +); + +type imageRecordReceivedArg = Parameters< + (typeof ImagesService)['getImageUrls'] +>[0]; + +/** + * `ImagesService.getImageUrls()` thunk + */ +export const imageRecordReceived = createAppAsyncThunk( + 'api/imageUrlsReceived', + async (arg: imageRecordReceivedArg) => { + const response = await ImagesService.getImageRecord(arg); + imagesLog.info({ arg, response }, 'Received image record'); + return response; + } +); + type ImageReceivedArg = Parameters<(typeof ImagesService)['getImage']>[0]; /** From 5a7e611e0a8414964a18a215c31b18296e424a69 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 22 May 2023 19:42:41 +1000 Subject: [PATCH 28/72] fix(nodes): fix image url --- invokeai/app/services/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/services/urls.py b/invokeai/app/services/urls.py index 0e2389b7d0..cfc4b34012 100644 --- a/invokeai/app/services/urls.py +++ b/invokeai/app/services/urls.py @@ -27,4 +27,4 @@ class LocalUrlService(UrlServiceBase): if thumbnail: return f"{self._base_url}/images/{image_type.value}/{image_basename}/thumbnail" - return f"{self._base_url}/images/{image_type.value}/{image_basename}/image" + return f"{self._base_url}/images/{image_type.value}/{image_basename}/full" From f310a39381b18e5a0c8a04a9e2377b6f4dfeca42 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 22 May 2023 19:44:04 +1000 Subject: [PATCH 29/72] feat(nodes): finalize image routes --- invokeai/app/api/routers/images.py | 81 +++++++++++++++++++----------- 1 file changed, 51 insertions(+), 30 deletions(-) diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 18b47b5595..5914626a70 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -23,6 +23,7 @@ images_router = APIRouter(prefix="/v1/images", tags=["images"]) 415: {"description": "Image upload failed"}, }, status_code=201, + response_model=ImageDTO, ) async def upload_image( file: UploadFile, @@ -73,15 +74,15 @@ async def delete_image( @images_router.get( - "/{image_type}/{image_name}/record", - operation_id="get_image_record", + "/{image_type}/{image_name}/metadata", + operation_id="get_image_metadata", response_model=ImageDTO, ) -async def get_image_record( - image_type: ImageType = Path(description="The type of the image record to get"), - image_name: str = Path(description="The id of the image record to get"), +async def get_image_metadata( + image_type: ImageType = Path(description="The type of image to get"), + image_name: str = Path(description="The name of image to get"), ) -> ImageDTO: - """Gets an image record by id""" + """Gets an image's metadata""" try: return ApiDependencies.invoker.services.images_new.get_dto( @@ -91,38 +92,60 @@ async def get_image_record( raise HTTPException(status_code=404) -@images_router.get("/{image_type}/{image_name}/image", operation_id="get_image") -async def get_image( - image_type: ImageType = Path(description="The type of the image to get"), - image_name: str = Path(description="The id of the image to get"), +@images_router.get( + "/{image_type}/{image_name}/full", + operation_id="get_image_full", + response_class=Response, + responses={ + 200: { + "description": "Return the full-resolution image", + "content": {"image/png": {}}, + }, + 404: {"description": "Image not found"}, + }, +) +async def get_image_full( + image_type: ImageType = Path( + description="The type of full-resolution image file to get" + ), + image_name: str = Path(description="The name of full-resolution image file to get"), ) -> FileResponse: - """Gets an image""" + """Gets a full-resolution image file""" try: path = ApiDependencies.invoker.services.images_new.get_path( image_type, image_name ) - return FileResponse(path) + return FileResponse(path, media_type="image/png") except Exception as e: raise HTTPException(status_code=404) -@images_router.get("/{image_type}/{image_name}/thumbnail", operation_id="get_thumbnail") -async def get_thumbnail( - image_type: ImageType = Path( - description="The type of the image whose thumbnail to get" - ), - image_name: str = Path(description="The id of the image whose thumbnail to get"), +@images_router.get( + "/{image_type}/{image_name}/thumbnail", + operation_id="get_image_thumbnail", + response_class=Response, + responses={ + 200: { + "description": "Return the image thumbnail", + "content": {"image/webp": {}}, + }, + 404: {"description": "Image not found"}, + }, +) +async def get_image_thumbnail( + image_type: ImageType = Path(description="The type of thumbnail image file to get"), + image_name: str = Path(description="The name of thumbnail image file to get"), ) -> FileResponse: - """Gets a thumbnail""" + """Gets a thumbnail image file""" try: path = ApiDependencies.invoker.services.images_new.get_path( image_type, image_name, thumbnail=True ) - return FileResponse(path) + return FileResponse(path, media_type="image/webp") except Exception as e: raise HTTPException(status_code=404) @@ -134,7 +157,7 @@ async def get_thumbnail( ) async def get_image_urls( image_type: ImageType = Path(description="The type of the image whose URL to get"), - image_name: str = Path(description="The id of the image whose URL to get"), + image_name: str = Path(description="The name of the image whose URL to get"), ) -> ImageUrlsDTO: """Gets an image and thumbnail URL""" @@ -157,20 +180,18 @@ async def get_image_urls( @images_router.get( "/", - operation_id="list_image_records", + operation_id="list_images_with_metadata", response_model=PaginatedResults[ImageDTO], ) -async def list_image_records( - image_type: ImageType = Query(description="The type of image records to get"), - image_category: ImageCategory = Query( - description="The kind of image records to get" - ), - page: int = Query(default=0, description="The page of image records to get"), +async def list_images_with_metadata( + image_type: ImageType = Query(description="The type of images to list"), + image_category: ImageCategory = Query(description="The kind of images to list"), + page: int = Query(default=0, description="The page of image metadata to get"), per_page: int = Query( - default=10, description="The number of image records per page" + default=10, description="The number of image metadata per page" ), ) -> PaginatedResults[ImageDTO]: - """Gets a list of image records by type and category""" + """Gets a list of images with metadata""" image_dtos = ApiDependencies.invoker.services.images_new.get_many( image_type, From c31ff364abadbc8ce2e5025b505c6e945746aa1e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 22 May 2023 19:44:35 +1000 Subject: [PATCH 30/72] fix(nodes): tidy images service --- invokeai/app/services/images.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/invokeai/app/services/images.py b/invokeai/app/services/images.py index fc4c85fbdf..e018e78f09 100644 --- a/invokeai/app/services/images.py +++ b/invokeai/app/services/images.py @@ -1,5 +1,4 @@ from abc import ABC, abstractmethod -import json from logging import Logger from typing import Optional, TYPE_CHECKING, Union import uuid @@ -21,17 +20,12 @@ from invokeai.app.services.metadata import MetadataServiceBase from invokeai.app.services.urls import UrlServiceBase from invokeai.app.util.misc import get_iso_timestamp - if TYPE_CHECKING: from invokeai.app.services.graph import GraphExecutionState class ImageServiceABC(ABC): - """ - High-level service for image management. - - Provides methods for creating, retrieving, and deleting images. - """ + """High-level service for image management.""" @abstractmethod def create( From 74292eba28ee788c06461fce782db8c32d17e843 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 22 May 2023 19:44:49 +1000 Subject: [PATCH 31/72] chore(ui): regen api client --- .../services/api/services/ImagesService.ts | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/services/ImagesService.ts b/invokeai/frontend/web/src/services/api/services/ImagesService.ts index 7d567adc24..a172c022f4 100644 --- a/invokeai/frontend/web/src/services/api/services/ImagesService.ts +++ b/invokeai/frontend/web/src/services/api/services/ImagesService.ts @@ -15,31 +15,31 @@ import { request as __request } from '../core/request'; export class ImagesService { /** - * List Image Records - * Gets a list of image records by type and category + * List Images With Metadata + * Gets a list of images with metadata * @returns PaginatedResults_ImageDTO_ Successful Response * @throws ApiError */ - public static listImageRecords({ + public static listImagesWithMetadata({ imageType, imageCategory, page, perPage = 10, }: { /** - * The type of image records to get + * The type of images to list */ imageType: ImageType, /** - * The kind of image records to get + * The kind of images to list */ imageCategory: ImageCategory, /** - * The page of image records to get + * The page of image metadata to get */ page?: number, /** - * The number of image records per page + * The number of image metadata per page */ perPage?: number, }): CancelablePromise { @@ -61,7 +61,7 @@ export class ImagesService { /** * Upload Image * Uploads an image - * @returns any The image was uploaded successfully + * @returns ImageDTO The image was uploaded successfully * @throws ApiError */ public static uploadImage({ @@ -72,7 +72,7 @@ export class ImagesService { imageType: ImageType, formData: Body_upload_image, imageCategory?: ImageCategory, - }): CancelablePromise { + }): CancelablePromise { return __request(OpenAPI, { method: 'POST', url: '/api/v1/images/', @@ -119,27 +119,27 @@ export class ImagesService { } /** - * Get Image Record - * Gets an image record by id + * Get Image Metadata + * Gets an image's metadata * @returns ImageDTO Successful Response * @throws ApiError */ - public static getImageRecord({ + public static getImageMetadata({ imageType, imageName, }: { /** - * The type of the image record to get + * The type of image to get */ imageType: ImageType, /** - * The id of the image record to get + * The name of image to get */ imageName: string, }): CancelablePromise { return __request(OpenAPI, { method: 'GET', - url: '/api/v1/images/{image_type}/{image_name}/record', + url: '/api/v1/images/{image_type}/{image_name}/metadata', path: { 'image_type': imageType, 'image_name': imageName, @@ -151,53 +151,54 @@ export class ImagesService { } /** - * Get Image - * Gets an image - * @returns any Successful Response + * Get Image Full + * Gets a full-resolution image file + * @returns any Return the full-resolution image * @throws ApiError */ - public static getImage({ + public static getImageFull({ imageType, imageName, }: { /** - * The type of the image to get + * The type of full-resolution image file to get */ imageType: ImageType, /** - * The id of the image to get + * The name of full-resolution image file to get */ imageName: string, }): CancelablePromise { return __request(OpenAPI, { method: 'GET', - url: '/api/v1/images/{image_type}/{image_name}/image', + url: '/api/v1/images/{image_type}/{image_name}/full', path: { 'image_type': imageType, 'image_name': imageName, }, errors: { + 404: `Image not found`, 422: `Validation Error`, }, }); } /** - * Get Thumbnail - * Gets a thumbnail - * @returns any Successful Response + * Get Image Thumbnail + * Gets a thumbnail image file + * @returns any Return the image thumbnail * @throws ApiError */ - public static getThumbnail({ + public static getImageThumbnail({ imageType, imageName, }: { /** - * The type of the image whose thumbnail to get + * The type of thumbnail image file to get */ imageType: ImageType, /** - * The id of the image whose thumbnail to get + * The name of thumbnail image file to get */ imageName: string, }): CancelablePromise { @@ -209,6 +210,7 @@ export class ImagesService { 'image_name': imageName, }, errors: { + 404: `Image not found`, 422: `Validation Error`, }, }); @@ -229,7 +231,7 @@ export class ImagesService { */ imageType: ImageType, /** - * The id of the image whose URL to get + * The name of the image whose URL to get */ imageName: string, }): CancelablePromise { From 6aebe1614d8cf8fa18fa688a0c4f28cfd9828d5d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 22 May 2023 19:45:08 +1000 Subject: [PATCH 32/72] feat(ui): wip use new images service --- .../listeners/invocationComplete.ts | 119 ++++++----- .../components/ImageMetadataOverlay.tsx | 7 +- .../web/src/common/util/dateComparator.ts | 12 ++ .../components/CurrentImagePreview.tsx | 6 +- .../gallery/components/HoverableImage.tsx | 31 +-- .../components/ImageGalleryContent.tsx | 11 +- .../ImageMetadataViewer.tsx | 188 ++++++++++-------- .../features/gallery/store/gallerySlice.ts | 40 +--- .../features/gallery/store/resultsSlice.ts | 67 ++++--- .../features/gallery/store/uploadsSlice.ts | 39 +++- .../features/ui/store/uiPersistDenylist.ts | 2 +- .../web/src/services/thunks/gallery.ts | 6 +- .../frontend/web/src/services/thunks/image.ts | 47 +---- 13 files changed, 296 insertions(+), 279 deletions(-) create mode 100644 invokeai/frontend/web/src/common/util/dateComparator.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts index b5c391afe4..3db8490a6e 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts @@ -6,12 +6,7 @@ import { } from 'services/util/deserializeImageField'; import { Image } from 'app/types/invokeai'; import { resultAdded } from 'features/gallery/store/resultsSlice'; -import { - imageReceived, - imageRecordReceived, - imageUrlsReceived, - thumbnailReceived, -} from 'services/thunks/image'; +import { imageMetadataReceived } from 'services/thunks/image'; import { startAppListening } from '..'; import { imageSelected } from 'features/gallery/store/gallerySlice'; import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; @@ -41,75 +36,75 @@ export const addImageResultReceivedListener = () => { const name = result.image.image_name; const type = result.image.image_type; - dispatch(imageUrlsReceived({ imageName: name, imageType: type })); + // dispatch(imageUrlsReceived({ imageName: name, imageType: type })); - const [{ payload }] = await take( - (action): action is ReturnType => - imageUrlsReceived.fulfilled.match(action) && - action.payload.image_name === name - ); + // const [{ payload }] = await take( + // (action): action is ReturnType => + // imageUrlsReceived.fulfilled.match(action) && + // action.payload.image_name === name + // ); - console.log(payload); + // console.log(payload); - dispatch(imageRecordReceived({ imageName: name, imageType: type })); + dispatch(imageMetadataReceived({ imageName: name, imageType: type })); - const [x] = await take( - ( - action - ): action is ReturnType => - imageRecordReceived.fulfilled.match(action) && - action.payload.image_name === name - ); + // const [x] = await take( + // ( + // action + // ): action is ReturnType => + // imageMetadataReceived.fulfilled.match(action) && + // action.payload.image_name === name + // ); - console.log(x); + // console.log(x); - const state = getState(); + // const state = getState(); - // if we need to refetch, set URLs to placeholder for now - const { url, thumbnail } = shouldFetchImages - ? { url: '', thumbnail: '' } - : buildImageUrls(type, name); + // // if we need to refetch, set URLs to placeholder for now + // const { url, thumbnail } = shouldFetchImages + // ? { url: '', thumbnail: '' } + // : buildImageUrls(type, name); - const timestamp = extractTimestampFromImageName(name); + // const timestamp = extractTimestampFromImageName(name); - const image: Image = { - name, - type, - url, - thumbnail, - metadata: { - created: timestamp, - width: result.width, - height: result.height, - invokeai: { - session_id: graph_execution_state_id, - ...(node ? { node } : {}), - }, - }, - }; + // const image: Image = { + // name, + // type, + // url, + // thumbnail, + // metadata: { + // created: timestamp, + // width: result.width, + // height: result.height, + // invokeai: { + // session_id: graph_execution_state_id, + // ...(node ? { node } : {}), + // }, + // }, + // }; - dispatch(resultAdded(image)); + // dispatch(resultAdded(image)); - if (state.gallery.shouldAutoSwitchToNewImages) { - dispatch(imageSelected(image)); - } + // if (state.gallery.shouldAutoSwitchToNewImages) { + // dispatch(imageSelected(image)); + // } - if (state.config.shouldFetchImages) { - dispatch(imageReceived({ imageName: name, imageType: type })); - dispatch( - thumbnailReceived({ - thumbnailName: name, - thumbnailType: type, - }) - ); - } + // if (state.config.shouldFetchImages) { + // dispatch(imageReceived({ imageName: name, imageType: type })); + // dispatch( + // thumbnailReceived({ + // thumbnailName: name, + // thumbnailType: type, + // }) + // ); + // } - if ( - graph_execution_state_id === - state.canvas.layerState.stagingArea.sessionId - ) { - dispatch(addImageToStagingArea(image)); - } + // if ( + // graph_execution_state_id === + // state.canvas.layerState.stagingArea.sessionId + // ) { + // dispatch(addImageToStagingArea(image)); + // } } }, }); diff --git a/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx b/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx index 64d5e1beef..b96bc2ffe2 100644 --- a/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx +++ b/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx @@ -2,9 +2,10 @@ import { Badge, Flex } from '@chakra-ui/react'; import { Image } from 'app/types/invokeai'; import { isNumber, isString } from 'lodash-es'; import { useMemo } from 'react'; +import { ImageDTO } from 'services/api'; type ImageMetadataOverlayProps = { - image: Image; + image: ImageDTO; }; const ImageMetadataOverlay = ({ image }: ImageMetadataOverlayProps) => { @@ -17,11 +18,11 @@ const ImageMetadataOverlay = ({ image }: ImageMetadataOverlayProps) => { }, [image.metadata]); const model = useMemo(() => { - if (!isString(image.metadata?.invokeai?.node?.model)) { + if (!isString(image.metadata?.model)) { return; } - return image.metadata?.invokeai?.node?.model; + return image.metadata?.model; }, [image.metadata]); return ( diff --git a/invokeai/frontend/web/src/common/util/dateComparator.ts b/invokeai/frontend/web/src/common/util/dateComparator.ts new file mode 100644 index 0000000000..ea0dc28b6d --- /dev/null +++ b/invokeai/frontend/web/src/common/util/dateComparator.ts @@ -0,0 +1,12 @@ +/** + * Comparator function for sorting dates in ascending order + */ +export const dateComparator = (a: string, b: string) => { + const dateA = new Date(a); + const dateB = new Date(b); + + // sort in ascending order + if (dateA > dateB) return 1; + if (dateA < dateB) return -1; + return 0; +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx index 879123af2a..4562e3458d 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx @@ -61,8 +61,8 @@ const CurrentImagePreview = () => { if (!image) { return; } - e.dataTransfer.setData('invokeai/imageName', image.name); - e.dataTransfer.setData('invokeai/imageType', image.type); + e.dataTransfer.setData('invokeai/imageName', image.image_name); + e.dataTransfer.setData('invokeai/imageType', image.image_type); e.dataTransfer.effectAllowed = 'move'; }, [image] @@ -108,7 +108,7 @@ const CurrentImagePreview = () => { image && ( <> } onDragStart={handleDragStart} diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx index 8f3fff4ff3..e1e0f0458c 100644 --- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx @@ -39,6 +39,7 @@ import { sentImageToImg2Img, } from '../store/actions'; import { useAppToaster } from 'app/components/Toaster'; +import { ImageDTO } from 'services/api'; export const selector = createSelector( [gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector], @@ -70,14 +71,16 @@ export const selector = createSelector( ); interface HoverableImageProps { - image: InvokeAI.Image; + image: ImageDTO; isSelected: boolean; } const memoEqualityCheck = ( prev: HoverableImageProps, next: HoverableImageProps -) => prev.image.name === next.image.name && prev.isSelected === next.isSelected; +) => + prev.image.image_name === next.image.image_name && + prev.isSelected === next.isSelected; /** * Gallery image component with delete/use all/use seed buttons on hover. @@ -100,7 +103,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { } = useDisclosure(); const { image, isSelected } = props; - const { url, thumbnail, name } = image; + const { image_url, thumbnail_url, image_name } = image; const { getUrl } = useGetUrl(); const [isHovered, setIsHovered] = useState(false); @@ -144,8 +147,8 @@ const HoverableImage = memo((props: HoverableImageProps) => { const handleDragStart = useCallback( (e: DragEvent) => { - e.dataTransfer.setData('invokeai/imageName', image.name); - e.dataTransfer.setData('invokeai/imageType', image.type); + e.dataTransfer.setData('invokeai/imageName', image.image_name); + e.dataTransfer.setData('invokeai/imageType', image.image_type); e.dataTransfer.effectAllowed = 'move'; }, [image] @@ -153,11 +156,11 @@ const HoverableImage = memo((props: HoverableImageProps) => { // Recall parameters handlers const handleRecallPrompt = useCallback(() => { - recallPrompt(image.metadata?.invokeai?.node?.prompt); + recallPrompt(image.metadata?.positive_conditioning); }, [image, recallPrompt]); const handleRecallSeed = useCallback(() => { - recallSeed(image.metadata.invokeai?.node?.seed); + recallSeed(image.metadata?.seed); }, [image, recallSeed]); const handleSendToImageToImage = useCallback(() => { @@ -200,7 +203,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { }; const handleOpenInNewTab = () => { - window.open(getUrl(image.url), '_blank'); + window.open(getUrl(image.image_url), '_blank'); }; return ( @@ -223,7 +226,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { } onClickCapture={handleRecallPrompt} - isDisabled={image?.metadata?.invokeai?.node?.prompt === undefined} + isDisabled={image?.metadata?.positive_conditioning === undefined} > {t('parameters.usePrompt')} @@ -231,14 +234,14 @@ const HoverableImage = memo((props: HoverableImageProps) => { } onClickCapture={handleRecallSeed} - isDisabled={image?.metadata?.invokeai?.node?.seed === undefined} + isDisabled={image?.metadata?.seed === undefined} > {t('parameters.useSeed')} } onClickCapture={handleRecallInitialImage} - isDisabled={image?.metadata?.invokeai?.node?.type !== 'img2img'} + isDisabled={image?.metadata?.type !== 'img2img'} > {t('parameters.useInitImg')} @@ -247,7 +250,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { onClickCapture={handleUseAllParameters} isDisabled={ !['txt2img', 'img2img', 'inpaint'].includes( - String(image?.metadata?.invokeai?.node?.type) + String(image?.metadata?.type) ) } > @@ -278,7 +281,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { {(ref) => ( { shouldUseSingleGalleryColumn ? 'contain' : galleryImageObjectFit } rounded="md" - src={getUrl(thumbnail || url)} + src={getUrl(thumbnail_url || image_url)} fallback={} sx={{ width: '100%', diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx index 9770ed5887..ce375f9580 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx @@ -55,6 +55,7 @@ import { Image as ImageType } from 'app/types/invokeai'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import GalleryProgressImage from './GalleryProgressImage'; import { uiSelector } from 'features/ui/store/uiSelectors'; +import { ImageDTO } from 'services/api'; const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290; const PROGRESS_IMAGE_PLACEHOLDER = 'PROGRESS_IMAGE_PLACEHOLDER'; @@ -66,7 +67,7 @@ const categorySelector = createSelector( const { currentCategory } = gallery; if (currentCategory === 'results') { - const tempImages: (ImageType | typeof PROGRESS_IMAGE_PLACEHOLDER)[] = []; + const tempImages: (ImageDTO | typeof PROGRESS_IMAGE_PLACEHOLDER)[] = []; if (system.progressImage) { tempImages.push(PROGRESS_IMAGE_PLACEHOLDER); @@ -352,7 +353,7 @@ const ImageGalleryContent = () => { const isSelected = image === PROGRESS_IMAGE_PLACEHOLDER ? false - : selectedImage?.name === image?.name; + : selectedImage?.image_name === image?.image_name; return ( @@ -362,7 +363,7 @@ const ImageGalleryContent = () => { /> ) : ( @@ -385,13 +386,13 @@ const ImageGalleryContent = () => { const isSelected = image === PROGRESS_IMAGE_PLACEHOLDER ? false - : selectedImage?.name === image?.name; + : selectedImage?.image_name === image?.image_name; return image === PROGRESS_IMAGE_PLACEHOLDER ? ( ) : ( 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 c23412a87d..3ec820ade7 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx @@ -18,7 +18,9 @@ import { setCfgScale, setHeight, setImg2imgStrength, + setNegativePrompt, setPerlin, + setPrompt, setScheduler, setSeamless, setSeed, @@ -36,6 +38,9 @@ import { useTranslation } from 'react-i18next'; import { FaCopy } from 'react-icons/fa'; import { IoArrowUndoCircleOutline } from 'react-icons/io5'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; +import { ImageDTO } from 'services/api'; +import { filter } from 'lodash-es'; +import { Scheduler } from 'app/constants'; type MetadataItemProps = { isLink?: boolean; @@ -58,7 +63,6 @@ const MetadataItem = ({ withCopy = false, }: MetadataItemProps) => { const { t } = useTranslation(); - return ( {onClick && ( @@ -104,14 +108,14 @@ const MetadataItem = ({ }; type ImageMetadataViewerProps = { - image: InvokeAI.Image; + image: ImageDTO; }; // TODO: I don't know if this is needed. const memoEqualityCheck = ( prev: ImageMetadataViewerProps, next: ImageMetadataViewerProps -) => prev.image.name === next.image.name; +) => prev.image.image_name === next.image.image_name; // TODO: Show more interesting information in this component. @@ -128,8 +132,9 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { dispatch(setShouldShowImageDetails(false)); }); - const sessionId = image.metadata.invokeai?.session_id; - const node = image.metadata.invokeai?.node as Record; + const sessionId = image?.session_id; + + const metadata = image?.metadata; const { t } = useTranslation(); const { getUrl } = useGetUrl(); @@ -154,110 +159,133 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { > File: - - {image.url.length > 64 - ? image.url.substring(0, 64).concat('...') - : image.url} + + {image.image_url.length > 64 + ? image.image_url.substring(0, 64).concat('...') + : image.image_url} - {node && Object.keys(node).length > 0 ? ( + {metadata && Object.keys(metadata).length > 0 ? ( <> - {node.type && ( - + {metadata.type && ( + )} - {node.model && } - {node.prompt && ( + {metadata.width && ( + dispatch(setWidth(Number(metadata.width)))} + /> + )} + {metadata.height && ( + dispatch(setHeight(Number(metadata.height)))} + /> + )} + {metadata.model && ( + + )} + {metadata.positive_conditioning && ( setBothPrompts(node.prompt)} + onClick={() => setPrompt(metadata.positive_conditioning!)} /> )} - {node.seed !== undefined && ( + {metadata.negative_conditioning && ( + setNegativePrompt(metadata.negative_conditioning!)} + /> + )} + {metadata.seed !== undefined && ( dispatch(setSeed(Number(node.seed)))} + value={metadata.seed} + onClick={() => dispatch(setSeed(Number(metadata.seed)))} /> )} - {node.threshold !== undefined && ( + {/* {metadata.threshold !== undefined && ( dispatch(setThreshold(Number(node.threshold)))} + value={metadata.threshold} + onClick={() => dispatch(setThreshold(Number(metadata.threshold)))} /> )} - {node.perlin !== undefined && ( + {metadata.perlin !== undefined && ( dispatch(setPerlin(Number(node.perlin)))} + value={metadata.perlin} + onClick={() => dispatch(setPerlin(Number(metadata.perlin)))} /> - )} - {node.scheduler && ( + )} */} + {metadata.scheduler && ( dispatch(setScheduler(node.scheduler))} - /> - )} - {node.steps && ( - dispatch(setSteps(Number(node.steps)))} - /> - )} - {node.cfg_scale !== undefined && ( - dispatch(setCfgScale(Number(node.cfg_scale)))} - /> - )} - {node.variations && node.variations.length > 0 && ( - - dispatch(setSeedWeights(seedWeightsToString(node.variations))) + dispatch(setScheduler(metadata.scheduler as Scheduler)) } /> )} - {node.seamless && ( + {metadata.steps && ( + dispatch(setSteps(Number(metadata.steps)))} + /> + )} + {metadata.cfg_scale !== undefined && ( + dispatch(setCfgScale(Number(metadata.cfg_scale)))} + /> + )} + {/* {metadata.variations && metadata.variations.length > 0 && ( + + dispatch( + setSeedWeights(seedWeightsToString(metadata.variations)) + ) + } + /> + )} + {metadata.seamless && ( dispatch(setSeamless(node.seamless))} + value={metadata.seamless} + onClick={() => dispatch(setSeamless(metadata.seamless))} /> )} - {node.hires_fix && ( + {metadata.hires_fix && ( dispatch(setHiresFix(node.hires_fix))} + value={metadata.hires_fix} + onClick={() => dispatch(setHiresFix(metadata.hires_fix))} /> - )} - {node.width && ( - dispatch(setWidth(Number(node.width)))} - /> - )} - {node.height && ( - dispatch(setHeight(Number(node.height)))} - /> - )} + )} */} + {/* {init_image_path && ( { onClick={() => dispatch(setInitialImage(init_image_path))} /> )} */} - {node.strength && ( + {metadata.strength && ( - dispatch(setImg2imgStrength(Number(node.strength))) + dispatch(setImg2imgStrength(Number(metadata.strength))) } /> )} - {node.fit && ( + {/* {metadata.fit && ( dispatch(setShouldFitToWidthHeight(node.fit))} + value={metadata.fit} + onClick={() => dispatch(setShouldFitToWidthHeight(metadata.fit))} /> - )} + )} */} ) : (
diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 96c3486b50..9d6f5ece60 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -1,16 +1,15 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; -import { Image } from 'app/types/invokeai'; -import { imageReceived, thumbnailReceived } from 'services/thunks/image'; import { receivedResultImagesPage, receivedUploadImagesPage, } from '../../../services/thunks/gallery'; +import { ImageDTO } from 'services/api'; type GalleryImageObjectFitType = 'contain' | 'cover'; export interface GalleryState { - selectedImage?: Image; + selectedImage?: ImageDTO; galleryImageMinimumWidth: number; galleryImageObjectFit: GalleryImageObjectFitType; shouldAutoSwitchToNewImages: boolean; @@ -30,7 +29,7 @@ export const gallerySlice = createSlice({ name: 'gallery', initialState: initialGalleryState, reducers: { - imageSelected: (state, action: PayloadAction) => { + imageSelected: (state, action: PayloadAction) => { state.selectedImage = action.payload; // TODO: if the user selects an image, disable the auto switch? // state.shouldAutoSwitchToNewImages = false; @@ -61,37 +60,18 @@ export const gallerySlice = createSlice({ }, }, extraReducers(builder) { - builder.addCase(imageReceived.fulfilled, (state, action) => { - // When we get an updated URL for an image, we need to update the selectedImage in gallery, - // which is currently its own object (instead of a reference to an image in results/uploads) - const { imagePath } = action.payload; - const { imageName } = action.meta.arg; - - if (state.selectedImage?.name === imageName) { - state.selectedImage.url = imagePath; - } - }); - - builder.addCase(thumbnailReceived.fulfilled, (state, action) => { - // When we get an updated URL for an image, we need to update the selectedImage in gallery, - // which is currently its own object (instead of a reference to an image in results/uploads) - const { thumbnailPath } = action.payload; - const { thumbnailName } = action.meta.arg; - - if (state.selectedImage?.name === thumbnailName) { - state.selectedImage.thumbnail = thumbnailPath; - } - }); builder.addCase(receivedResultImagesPage.fulfilled, (state, action) => { // rehydrate selectedImage URL when results list comes in // solves case when outdated URL is in local storage const selectedImage = state.selectedImage; if (selectedImage) { const selectedImageInResults = action.payload.items.find( - (image) => image.image_name === selectedImage.name + (image) => image.image_name === selectedImage.image_name ); + if (selectedImageInResults) { - selectedImage.url = selectedImageInResults.image_url; + selectedImage.image_url = selectedImageInResults.image_url; + selectedImage.thumbnail_url = selectedImageInResults.thumbnail_url; state.selectedImage = selectedImage; } } @@ -102,10 +82,12 @@ export const gallerySlice = createSlice({ const selectedImage = state.selectedImage; if (selectedImage) { const selectedImageInResults = action.payload.items.find( - (image) => image.image_name === selectedImage.name + (image) => image.image_name === selectedImage.image_name ); + if (selectedImageInResults) { - selectedImage.url = selectedImageInResults.image_url; + selectedImage.image_url = selectedImageInResults.image_url; + selectedImage.thumbnail_url = selectedImageInResults.thumbnail_url; state.selectedImage = selectedImage; } } diff --git a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts index f1286137a9..125f4ff5d5 100644 --- a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts @@ -1,21 +1,24 @@ import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; -import { Image } from 'app/types/invokeai'; - import { RootState } from 'app/store/store'; import { receivedResultImagesPage, IMAGES_PER_PAGE, } from 'services/thunks/gallery'; -import { deserializeImageResponse } from 'services/util/deserializeImageResponse'; import { imageDeleted, - imageReceived, - thumbnailReceived, + imageMetadataReceived, + imageUrlsReceived, } from 'services/thunks/image'; +import { ImageDTO } from 'services/api'; +import { dateComparator } from 'common/util/dateComparator'; -export const resultsAdapter = createEntityAdapter({ - selectId: (image) => image.name, - sortComparer: (a, b) => b.metadata.created - a.metadata.created, +export type ResultsImageDTO = Omit & { + image_type: 'results'; +}; + +export const resultsAdapter = createEntityAdapter({ + selectId: (image) => image.image_name, + sortComparer: (a, b) => dateComparator(b.created_at, a.created_at), }); type AdditionalResultsState = { @@ -53,13 +56,12 @@ const resultsSlice = createSlice({ * Received Result Images Page - FULFILLED */ builder.addCase(receivedResultImagesPage.fulfilled, (state, action) => { - const { items, page, pages } = action.payload; + const { page, pages } = action.payload; - const resultImages = items.map((image) => - deserializeImageResponse(image) - ); + // We know these will all be of the results type, but it's not represented in the API types + const items = action.payload.items as ResultsImageDTO[]; - resultsAdapter.setMany(state, resultImages); + resultsAdapter.setMany(state, items); state.page = page; state.pages = pages; @@ -68,33 +70,32 @@ const resultsSlice = createSlice({ }); /** - * Image Received - FULFILLED + * Image Metadata Received - FULFILLED */ - builder.addCase(imageReceived.fulfilled, (state, action) => { - const { imagePath } = action.payload; - const { imageName } = action.meta.arg; + builder.addCase(imageMetadataReceived.fulfilled, (state, action) => { + const { image_type } = action.payload; - resultsAdapter.updateOne(state, { - id: imageName, - changes: { - url: imagePath, - }, - }); + if (image_type === 'results') { + resultsAdapter.upsertOne(state, action.payload as ResultsImageDTO); + } }); /** - * Thumbnail Received - FULFILLED + * Image URLs Received - FULFILLED */ - builder.addCase(thumbnailReceived.fulfilled, (state, action) => { - const { thumbnailPath } = action.payload; - const { thumbnailName } = action.meta.arg; + builder.addCase(imageUrlsReceived.fulfilled, (state, action) => { + const { image_name, image_type, image_url, thumbnail_url } = + action.payload; - resultsAdapter.updateOne(state, { - id: thumbnailName, - changes: { - thumbnail: thumbnailPath, - }, - }); + if (image_type === 'results') { + resultsAdapter.updateOne(state, { + id: image_name, + changes: { + image_url: image_url, + thumbnail_url: thumbnail_url, + }, + }); + } }); /** diff --git a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts index d0a7821d9d..5910cc087b 100644 --- a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts @@ -6,12 +6,18 @@ import { receivedUploadImagesPage, IMAGES_PER_PAGE, } from 'services/thunks/gallery'; -import { imageDeleted } from 'services/thunks/image'; +import { imageDeleted, imageUrlsReceived } from 'services/thunks/image'; import { deserializeImageResponse } from 'services/util/deserializeImageResponse'; +import { ImageDTO } from 'services/api'; +import { dateComparator } from 'common/util/dateComparator'; -export const uploadsAdapter = createEntityAdapter({ - selectId: (image) => image.name, - sortComparer: (a, b) => b.metadata.created - a.metadata.created, +export type UploadsImageDTO = Omit & { + image_type: 'uploads'; +}; + +export const uploadsAdapter = createEntityAdapter({ + selectId: (image) => image.image_category, + sortComparer: (a, b) => dateComparator(b.created_at, a.created_at), }); type AdditionalUploadsState = { @@ -49,11 +55,12 @@ const uploadsSlice = createSlice({ * Received Upload Images Page - FULFILLED */ builder.addCase(receivedUploadImagesPage.fulfilled, (state, action) => { - const { items, page, pages } = action.payload; + const { page, pages } = action.payload; - const images = items.map((image) => deserializeImageResponse(image)); + // We know these will all be of the uploads type, but it's not represented in the API types + const items = action.payload.items as UploadsImageDTO[]; - uploadsAdapter.setMany(state, images); + uploadsAdapter.setMany(state, items); state.page = page; state.pages = pages; @@ -61,6 +68,24 @@ const uploadsSlice = createSlice({ state.isLoading = false; }); + /** + * Image URLs Received - FULFILLED + */ + builder.addCase(imageUrlsReceived.fulfilled, (state, action) => { + const { image_name, image_type, image_url, thumbnail_url } = + action.payload; + + if (image_type === 'uploads') { + uploadsAdapter.updateOne(state, { + id: image_name, + changes: { + image_url: image_url, + thumbnail_url: thumbnail_url, + }, + }); + } + }); + /** * Delete Image - pending * Pre-emptively remove the image from the gallery diff --git a/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts b/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts index 9f6bd2dd73..b485d71bdd 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts @@ -3,4 +3,4 @@ import { UIState } from './uiTypes'; /** * UI slice persist denylist */ -export const uiPersistDenylist: (keyof UIState)[] = []; +export const uiPersistDenylist: (keyof UIState)[] = ['shouldShowImageDetails']; diff --git a/invokeai/frontend/web/src/services/thunks/gallery.ts b/invokeai/frontend/web/src/services/thunks/gallery.ts index f908cbddcb..694d44db3e 100644 --- a/invokeai/frontend/web/src/services/thunks/gallery.ts +++ b/invokeai/frontend/web/src/services/thunks/gallery.ts @@ -9,8 +9,9 @@ const galleryLog = log.child({ namespace: 'gallery' }); export const receivedResultImagesPage = createAppAsyncThunk( 'results/receivedResultImagesPage', async (_arg, { getState }) => { - const response = await ImagesService.listImages({ + const response = await ImagesService.listImagesWithMetadata({ imageType: 'results', + imageCategory: 'image', page: getState().results.nextPage, perPage: IMAGES_PER_PAGE, }); @@ -24,8 +25,9 @@ export const receivedResultImagesPage = createAppAsyncThunk( export const receivedUploadImagesPage = createAppAsyncThunk( 'uploads/receivedUploadImagesPage', async (_arg, { getState }) => { - const response = await ImagesService.listImages({ + const response = await ImagesService.listImagesWithMetadata({ imageType: 'uploads', + imageCategory: 'image', page: getState().uploads.nextPage, perPage: IMAGES_PER_PAGE, }); diff --git a/invokeai/frontend/web/src/services/thunks/image.ts b/invokeai/frontend/web/src/services/thunks/image.ts index 5528e41bfc..bf6cc40e2e 100644 --- a/invokeai/frontend/web/src/services/thunks/image.ts +++ b/invokeai/frontend/web/src/services/thunks/image.ts @@ -1,3 +1,4 @@ +import { AnyAction } from '@reduxjs/toolkit'; import { log } from 'app/logging/useLogger'; import { createAppAsyncThunk } from 'app/store/storeUtils'; import { InvokeTabName } from 'features/ui/store/tabMap'; @@ -22,56 +23,22 @@ export const imageUrlsReceived = createAppAsyncThunk( } ); -type imageRecordReceivedArg = Parameters< - (typeof ImagesService)['getImageUrls'] +type imageMetadataReceivedArg = Parameters< + (typeof ImagesService)['getImageMetadata'] >[0]; /** * `ImagesService.getImageUrls()` thunk */ -export const imageRecordReceived = createAppAsyncThunk( - 'api/imageUrlsReceived', - async (arg: imageRecordReceivedArg) => { - const response = await ImagesService.getImageRecord(arg); +export const imageMetadataReceived = createAppAsyncThunk( + 'api/imageMetadataReceived', + async (arg: imageMetadataReceivedArg) => { + const response = await ImagesService.getImageMetadata(arg); imagesLog.info({ arg, response }, 'Received image record'); return response; } ); -type ImageReceivedArg = Parameters<(typeof ImagesService)['getImage']>[0]; - -/** - * `ImagesService.getImage()` thunk - */ -export const imageReceived = createAppAsyncThunk( - 'api/imageReceived', - async (arg: ImageReceivedArg) => { - const response = await ImagesService.getImage(arg); - - imagesLog.info({ arg, response }, 'Received image'); - - return response; - } -); - -type ThumbnailReceivedArg = Parameters< - (typeof ImagesService)['getThumbnail'] ->[0]; - -/** - * `ImagesService.getThumbnail()` thunk - */ -export const thumbnailReceived = createAppAsyncThunk( - 'api/thumbnailReceived', - async (arg: ThumbnailReceivedArg) => { - const response = await ImagesService.getThumbnail(arg); - - imagesLog.info({ arg, response }, 'Received thumbnail'); - - return response; - } -); - type ImageUploadedArg = Parameters<(typeof ImagesService)['uploadImage']>[0] & { // extra arg to determine post-upload actions - we check for this when the image is uploaded // to determine if we should set the init image From 4a7a5234df60c3f57b1f51455a9891fab624e0b1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 22 May 2023 20:05:20 +1000 Subject: [PATCH 33/72] fix(ui): fix image nodes losing image --- .../components/fields/ImageInputFieldComponent.tsx | 9 +++------ .../web/src/features/nodes/store/nodesSlice.ts | 10 ++-------- .../frontend/web/src/features/nodes/types/types.ts | 6 +++--- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx index b43338f930..18be021625 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx @@ -21,7 +21,7 @@ const ImageInputFieldComponent = ( const getImageByNameAndType = useGetImageByNameAndType(); const dispatch = useAppDispatch(); - const [url, setUrl] = useState(); + const [url, setUrl] = useState(field.value?.image_url); const { getUrl } = useGetUrl(); const handleDrop = useCallback( @@ -39,16 +39,13 @@ const ImageInputFieldComponent = ( return; } - setUrl(image.url); + setUrl(image.image_url); dispatch( fieldValueChanged({ nodeId, fieldName: field.name, - value: { - image_name: name, - image_type: type, - }, + value: image, }) ); }, diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 4ce0120c21..3c93be7ac5 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -11,7 +11,7 @@ import { NodeChange, OnConnectStartParams, } from 'reactflow'; -import { ImageField } from 'services/api'; +import { ImageDTO } from 'services/api'; import { receivedOpenAPISchema } from 'services/thunks/schema'; import { InvocationTemplate, InvocationValue } from '../types/types'; import { parseSchema } from '../util/parseSchema'; @@ -65,13 +65,7 @@ const nodesSlice = createSlice({ action: PayloadAction<{ nodeId: string; fieldName: string; - value: - | string - | number - | boolean - | Pick - | RgbaColor - | undefined; + value: string | number | boolean | ImageDTO | RgbaColor | undefined; }> ) => { const { nodeId, fieldName, value } = action.payload; diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts index 876ba95cac..d2c7c7e704 100644 --- a/invokeai/frontend/web/src/features/nodes/types/types.ts +++ b/invokeai/frontend/web/src/features/nodes/types/types.ts @@ -1,6 +1,6 @@ import { OpenAPIV3 } from 'openapi-types'; import { RgbaColor } from 'react-colorful'; -import { ImageField } from 'services/api'; +import { ImageDTO } from 'services/api'; import { AnyInvocationType } from 'services/events/types'; export type InvocationValue = { @@ -179,7 +179,7 @@ export type ConditioningInputFieldValue = FieldValueBase & { export type ImageInputFieldValue = FieldValueBase & { type: 'image'; - value?: Pick; + value?: ImageDTO; }; export type ModelInputFieldValue = FieldValueBase & { @@ -245,7 +245,7 @@ export type BooleanInputFieldTemplate = InputFieldTemplateBase & { }; export type ImageInputFieldTemplate = InputFieldTemplateBase & { - default: Pick; + default: ImageDTO; type: 'image'; }; From 7a1de3887e0d4eaaa96950b3be642a125908a63b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 22 May 2023 22:21:57 +1000 Subject: [PATCH 34/72] feat(ui): wip update UI for migration --- .../listeners/initialImageSelected.ts | 21 +++++++----- .../listeners/invocationComplete.ts | 33 ++++++++----------- .../frontend/web/src/app/types/invokeai.ts | 26 +++++++-------- .../components/ImageMetadataOverlay.tsx | 1 - .../src/features/canvas/store/canvasSlice.ts | 6 ++-- .../src/features/canvas/store/canvasTypes.ts | 3 +- .../ImageActionButtons/DeleteImageButton.tsx | 4 +-- .../components/ImageGalleryContent.tsx | 2 -- .../web/src/features/gallery/store/actions.ts | 6 ++-- .../features/gallery/store/uploadsSlice.ts | 2 -- .../lightbox/components/ReactPanZoomImage.tsx | 6 ++-- .../parameters/hooks/useParameters.ts | 18 +++++----- .../src/features/parameters/store/actions.ts | 31 +++++++++++++---- .../parameters/store/generationSlice.ts | 5 +-- .../store/setAllParametersReducer.ts | 5 ++- .../frontend/web/src/services/types/guards.ts | 1 - 16 files changed, 89 insertions(+), 81 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts index ae3a35f537..d6cfc260f3 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts @@ -1,12 +1,15 @@ import { initialImageChanged } from 'features/parameters/store/generationSlice'; -import { Image, isInvokeAIImage } from 'app/types/invokeai'; import { selectResultsById } from 'features/gallery/store/resultsSlice'; import { selectUploadsById } from 'features/gallery/store/uploadsSlice'; import { t } from 'i18next'; import { addToast } from 'features/system/store/systemSlice'; import { startAppListening } from '..'; -import { initialImageSelected } from 'features/parameters/store/actions'; +import { + initialImageSelected, + isImageDTO, +} from 'features/parameters/store/actions'; import { makeToast } from 'app/components/Toaster'; +import { ImageDTO } from 'services/api'; export const addInitialImageSelectedListener = () => { startAppListening({ @@ -21,21 +24,21 @@ export const addInitialImageSelectedListener = () => { return; } - if (isInvokeAIImage(action.payload)) { + if (isImageDTO(action.payload)) { dispatch(initialImageChanged(action.payload)); dispatch(addToast(makeToast(t('toast.sentToImageToImage')))); return; } - const { name, type } = action.payload; + const { image_name, image_type } = action.payload; - let image: Image | undefined; + let image: ImageDTO | undefined; const state = getState(); - if (type === 'results') { - image = selectResultsById(state, name); - } else if (type === 'uploads') { - image = selectUploadsById(state, name); + if (image_type === 'results') { + image = selectResultsById(state, image_name); + } else if (image_type === 'uploads') { + image = selectUploadsById(state, image_name); } if (!image) { diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts index 3db8490a6e..3755b38d41 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts @@ -1,15 +1,10 @@ import { invocationComplete } from 'services/events/actions'; import { isImageOutput } from 'services/types/guards'; import { - buildImageUrls, - extractTimestampFromImageName, -} from 'services/util/deserializeImageField'; -import { Image } from 'app/types/invokeai'; -import { resultAdded } from 'features/gallery/store/resultsSlice'; -import { imageMetadataReceived } from 'services/thunks/image'; + imageMetadataReceived, + imageUrlsReceived, +} from 'services/thunks/image'; import { startAppListening } from '..'; -import { imageSelected } from 'features/gallery/store/gallerySlice'; -import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; const nodeDenylist = ['dataURL_image']; @@ -33,20 +28,18 @@ export const addImageResultReceivedListener = () => { const { result, node, graph_execution_state_id } = data; if (isImageOutput(result) && !nodeDenylist.includes(node.type)) { - const name = result.image.image_name; - const type = result.image.image_type; + const { image_name, image_type } = result.image; - // dispatch(imageUrlsReceived({ imageName: name, imageType: type })); + dispatch( + imageUrlsReceived({ imageName: image_name, imageType: image_type }) + ); - // const [{ payload }] = await take( - // (action): action is ReturnType => - // imageUrlsReceived.fulfilled.match(action) && - // action.payload.image_name === name - // ); - - // console.log(payload); - - dispatch(imageMetadataReceived({ imageName: name, imageType: type })); + dispatch( + imageMetadataReceived({ + imageName: image_name, + imageType: image_type, + }) + ); // const [x] = await take( // ( diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index f684dc1ccf..9cd2028984 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -122,21 +122,21 @@ export type PostProcessedImageMetadata = ESRGANMetadata | FacetoolMetadata; /** * ResultImage */ -export type Image = { - name: string; - type: ImageType; - url: string; - thumbnail: string; - metadata: ImageResponseMetadata; -}; +// export ty`pe Image = { +// name: string; +// type: ImageType; +// url: string; +// thumbnail: string; +// metadata: ImageResponseMetadata; +// }; -export const isInvokeAIImage = (obj: Image | SelectedImage): obj is Image => { - if ('url' in obj && 'thumbnail' in obj) { - return true; - } +// export const isInvokeAIImage = (obj: Image | SelectedImage): obj is Image => { +// if ('url' in obj && 'thumbnail' in obj) { +// return true; +// } - return false; -}; +// return false; +// }; /** * Types related to the system status. diff --git a/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx b/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx index b96bc2ffe2..bed0a26831 100644 --- a/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx +++ b/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx @@ -1,5 +1,4 @@ import { Badge, Flex } from '@chakra-ui/react'; -import { Image } from 'app/types/invokeai'; import { isNumber, isString } from 'lodash-es'; import { useMemo } from 'react'; import { ImageDTO } from 'services/api'; diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index 037d353f42..67074b8953 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -1,6 +1,5 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; -import * as InvokeAI from 'app/types/invokeai'; import { roundDownToMultiple, roundToMultiple, @@ -29,6 +28,7 @@ import { isCanvasBaseImage, isCanvasMaskLine, } from './canvasTypes'; +import { ImageDTO } from 'services/api'; export const initialLayerState: CanvasLayerState = { objects: [], @@ -157,7 +157,7 @@ export const canvasSlice = createSlice({ setCursorPosition: (state, action: PayloadAction) => { state.cursorPosition = action.payload; }, - setInitialCanvasImage: (state, action: PayloadAction) => { + setInitialCanvasImage: (state, action: PayloadAction) => { const image = action.payload; const { width, height } = image.metadata; const { stageDimensions } = state; @@ -302,7 +302,7 @@ export const canvasSlice = createSlice({ selectedImageIndex: -1, }; }, - addImageToStagingArea: (state, action: PayloadAction) => { + addImageToStagingArea: (state, action: PayloadAction) => { const image = action.payload; if (!image || !state.layerState.stagingArea.boundingBox) { diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts b/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts index 2a6461aaf6..804e06f88f 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts @@ -1,6 +1,7 @@ import * as InvokeAI from 'app/types/invokeai'; import { IRect, Vector2d } from 'konva/lib/types'; import { RgbaColor } from 'react-colorful'; +import { ImageDTO } from 'services/api'; export const LAYER_NAMES_DICT = [ { key: 'Base', value: 'base' }, @@ -37,7 +38,7 @@ export type CanvasImage = { y: number; width: number; height: number; - image: InvokeAI.Image; + image: ImageDTO; }; export type CanvasMaskLine = { diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageActionButtons/DeleteImageButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageActionButtons/DeleteImageButton.tsx index 6e35ccd63b..4b0f6e60dd 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageActionButtons/DeleteImageButton.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageActionButtons/DeleteImageButton.tsx @@ -12,7 +12,7 @@ import { memo, useCallback } from 'react'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import DeleteImageModal from '../DeleteImageModal'; import { requestedImageDeletion } from 'features/gallery/store/actions'; -import { Image } from 'app/types/invokeai'; +import { ImageDTO } from 'services/api'; const selector = createSelector( [systemSelector], @@ -30,7 +30,7 @@ const selector = createSelector( ); type DeleteImageButtonProps = { - image: Image | undefined; + image: ImageDTO | undefined; }; const DeleteImageButton = (props: DeleteImageButtonProps) => { diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx index ce375f9580..468dfd694f 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx @@ -5,7 +5,6 @@ import { FlexProps, Grid, Icon, - Image, Text, forwardRef, } from '@chakra-ui/react'; @@ -51,7 +50,6 @@ import { uploadsAdapter } from '../store/uploadsSlice'; import { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'app/store/store'; import { Virtuoso, VirtuosoGrid } from 'react-virtuoso'; -import { Image as ImageType } from 'app/types/invokeai'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import GalleryProgressImage from './GalleryProgressImage'; import { uiSelector } from 'features/ui/store/uiSelectors'; diff --git a/invokeai/frontend/web/src/features/gallery/store/actions.ts b/invokeai/frontend/web/src/features/gallery/store/actions.ts index a7454047b1..7e071f279d 100644 --- a/invokeai/frontend/web/src/features/gallery/store/actions.ts +++ b/invokeai/frontend/web/src/features/gallery/store/actions.ts @@ -1,9 +1,9 @@ import { createAction } from '@reduxjs/toolkit'; -import { Image } from 'app/types/invokeai'; -import { SelectedImage } from 'features/parameters/store/actions'; +import { ImageNameAndType } from 'features/parameters/store/actions'; +import { ImageDTO } from 'services/api'; export const requestedImageDeletion = createAction< - Image | SelectedImage | undefined + ImageDTO | ImageNameAndType | undefined >('gallery/requestedImageDeletion'); export const sentImageToCanvas = createAction('gallery/sentImageToCanvas'); diff --git a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts index 5910cc087b..2864db2660 100644 --- a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts @@ -1,5 +1,4 @@ import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; -import { Image } from 'app/types/invokeai'; import { RootState } from 'app/store/store'; import { @@ -7,7 +6,6 @@ import { IMAGES_PER_PAGE, } from 'services/thunks/gallery'; import { imageDeleted, imageUrlsReceived } from 'services/thunks/image'; -import { deserializeImageResponse } from 'services/util/deserializeImageResponse'; import { ImageDTO } from 'services/api'; import { dateComparator } from 'common/util/dateComparator'; diff --git a/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx b/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx index 9781625949..b1e822c309 100644 --- a/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx +++ b/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import { TransformComponent, useTransformContext } from 'react-zoom-pan-pinch'; -import * as InvokeAI from 'app/types/invokeai'; import { useGetUrl } from 'common/util/getUrl'; +import { ImageDTO } from 'services/api'; type ReactPanZoomProps = { - image: InvokeAI.Image; + image: ImageDTO; styleClass?: string; alt?: string; ref?: React.Ref; @@ -37,7 +37,7 @@ export default function ReactPanZoomImage({ transform: `rotate(${rotation}deg) scaleX(${scaleX}) scaleY(${scaleY})`, width: '100%', }} - src={getUrl(image.url)} + src={getUrl(image.image_url)} alt={alt} ref={ref} className={styleClass ? styleClass : ''} diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts b/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts index 138d54402c..ad9985b5de 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts @@ -7,9 +7,9 @@ import { allParametersSet, setSeed } from '../store/generationSlice'; import { isImageField } from 'services/types/guards'; import { NUMPY_RAND_MAX } from 'app/constants'; import { initialImageSelected } from '../store/actions'; -import { Image } from 'app/types/invokeai'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { useAppToaster } from 'app/components/Toaster'; +import { ImageDTO } from 'services/api'; export const useParameters = () => { const dispatch = useAppDispatch(); @@ -88,9 +88,7 @@ export const useParameters = () => { return; } - dispatch( - initialImageSelected({ name: image.image_name, type: image.image_type }) - ); + dispatch(initialImageSelected(image)); toaster({ title: t('toast.initialImageSet'), status: 'info', @@ -105,21 +103,21 @@ export const useParameters = () => { * Sets image as initial image with toast */ const sendToImageToImage = useCallback( - (image: Image) => { - dispatch(initialImageSelected({ name: image.name, type: image.type })); + (image: ImageDTO) => { + dispatch(initialImageSelected(image)); }, [dispatch] ); const recallAllParameters = useCallback( - (image: Image | undefined) => { - const type = image?.metadata?.invokeai?.node?.type; + (image: ImageDTO | undefined) => { + const type = image?.metadata?.type; if (['txt2img', 'img2img', 'inpaint'].includes(String(type))) { dispatch(allParametersSet(image)); - if (image?.metadata?.invokeai?.node?.type === 'img2img') { + if (image?.metadata?.type === 'img2img') { dispatch(setActiveTab('img2img')); - } else if (image?.metadata?.invokeai?.node?.type === 'txt2img') { + } else if (image?.metadata?.type === 'txt2img') { dispatch(setActiveTab('txt2img')); } diff --git a/invokeai/frontend/web/src/features/parameters/store/actions.ts b/invokeai/frontend/web/src/features/parameters/store/actions.ts index 4b261d7783..853597c809 100644 --- a/invokeai/frontend/web/src/features/parameters/store/actions.ts +++ b/invokeai/frontend/web/src/features/parameters/store/actions.ts @@ -1,12 +1,31 @@ import { createAction } from '@reduxjs/toolkit'; -import { Image } from 'app/types/invokeai'; -import { ImageType } from 'services/api'; +import { isObject } from 'lodash-es'; +import { ImageDTO, ImageType } from 'services/api'; -export type SelectedImage = { - name: string; - type: ImageType; +export type ImageNameAndType = { + image_name: string; + image_type: ImageType; +}; + +export const isImageDTO = (image: any): image is ImageDTO => { + return ( + image && + isObject(image) && + 'image_name' in image && + image?.image_name !== undefined && + 'image_type' in image && + image?.image_type !== undefined && + 'image_url' in image && + image?.image_url !== undefined && + 'thumbnail_url' in image && + image?.thumbnail_url !== undefined && + 'image_category' in image && + image?.image_category !== undefined && + 'created_at' in image && + image?.created_at !== undefined + ); }; export const initialImageSelected = createAction< - Image | SelectedImage | undefined + ImageDTO | ImageNameAndType | undefined >('generation/initialImageSelected'); diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index f9e857e7e3..b471ffc783 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -6,13 +6,14 @@ import { clamp, sample } from 'lodash-es'; import { setAllParametersReducer } from './setAllParametersReducer'; import { receivedModels } from 'services/thunks/model'; import { Scheduler } from 'app/constants'; +import { ImageDTO } from 'services/api'; export interface GenerationState { cfgScale: number; height: number; img2imgStrength: number; infillMethod: string; - initialImage?: InvokeAI.Image; + initialImage?: ImageDTO; iterations: number; perlin: number; prompt: string; @@ -213,7 +214,7 @@ export const generationSlice = createSlice({ setShouldUseNoiseSettings: (state, action: PayloadAction) => { state.shouldUseNoiseSettings = action.payload; }, - initialImageChanged: (state, action: PayloadAction) => { + initialImageChanged: (state, action: PayloadAction) => { state.initialImage = action.payload; }, modelSelected: (state, action: PayloadAction) => { diff --git a/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts b/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts index a816d358ce..dc147090b4 100644 --- a/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts +++ b/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts @@ -1,12 +1,11 @@ import { Draft, PayloadAction } from '@reduxjs/toolkit'; -import { Image } from 'app/types/invokeai'; import { GenerationState } from './generationSlice'; -import { ImageToImageInvocation } from 'services/api'; +import { ImageDTO, ImageToImageInvocation } from 'services/api'; import { isScheduler } from 'app/constants'; export const setAllParametersReducer = ( state: Draft, - action: PayloadAction + action: PayloadAction ) => { const node = action.payload?.metadata.invokeai?.node; diff --git a/invokeai/frontend/web/src/services/types/guards.ts b/invokeai/frontend/web/src/services/types/guards.ts index 5065290220..f7eba6e9d6 100644 --- a/invokeai/frontend/web/src/services/types/guards.ts +++ b/invokeai/frontend/web/src/services/types/guards.ts @@ -1,4 +1,3 @@ -import { Image } from 'app/types/invokeai'; import { get, isObject, isString } from 'lodash-es'; import { GraphExecutionState, From 021e5a2aa3e01dd4038490be1ebab0c29b1b4e23 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 23 May 2023 18:43:06 +1000 Subject: [PATCH 35/72] feat(nodes): improve metadata service comments --- invokeai/app/services/metadata.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/invokeai/app/services/metadata.py b/invokeai/app/services/metadata.py index 07509f4e3c..aa7bc2291a 100644 --- a/invokeai/app/services/metadata.py +++ b/invokeai/app/services/metadata.py @@ -1,10 +1,9 @@ -import json from abc import ABC, abstractmethod from typing import Any, Union import networkx as nx from invokeai.app.models.metadata import ImageMetadata -from invokeai.app.services.graph import Edge, Graph, GraphExecutionState +from invokeai.app.services.graph import Graph, GraphExecutionState class MetadataServiceBase(ABC): @@ -18,7 +17,6 @@ class MetadataServiceBase(ABC): pass - class CoreMetadataService(MetadataServiceBase): _ANCESTOR_TYPES = ["t2l", "l2l"] """The ancestor types that contain the core metadata""" @@ -89,13 +87,13 @@ class CoreMetadataService(MetadataServiceBase): # If the destination node ID matches the given node ID, gather necessary metadata if dest_node_id == node_id: - # If the destination field is 'positive_conditioning', add the 'prompt' from the source node + # Prompt if dest_field == "positive_conditioning": metadata["positive_conditioning"] = source_node_dict.get("prompt") - # If the destination field is 'negative_conditioning', add the 'prompt' from the source node + # Negative prompt if dest_field == "negative_conditioning": metadata["negative_conditioning"] = source_node_dict.get("prompt") - # If the destination field is 'noise', add the core noise fields from the source node + # Seed, width and height if dest_field == "noise": for field in self._NOISE_FIELDS: metadata[field] = source_node_dict.get(field) @@ -115,9 +113,10 @@ class CoreMetadataService(MetadataServiceBase): ImageMetadata: The metadata for the node. """ + # We need to do all the traversal on the execution graph graph = session.execution_graph - # Find the nearest ancestor of the given node + # Find the nearest `t2l`/`l2l` ancestor of the given node ancestor_id = self._find_nearest_ancestor(graph.nx_graph_with_data(), node_id) # If no ancestor was found, return an empty ImageMetadata object @@ -126,13 +125,14 @@ class CoreMetadataService(MetadataServiceBase): ancestor_node = graph.get_node(ancestor_id) + # Grab all the core metadata from the ancestor node ancestor_metadata = { param: val for param, val in ancestor_node.dict().items() if param in self._ANCESTOR_PARAMS } - # Get additional metadata related to the ancestor + # Get this image's prompts and noise parameters addl_metadata = self._get_additional_metadata(graph, ancestor_id) # If additional metadata was found, add it to the main metadata From 035425ef24eaff9ad368221091f9b28730112110 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 23 May 2023 18:59:43 +1000 Subject: [PATCH 36/72] feat(nodes): address feedback - Address database feedback: - Remove all the extraneous tables. Only an `images` table now: - `image_type` and `image_category` are unrestricted strings. When creating images, the provided values are checked to ensure they are a valid type and category. - Add `updated_at` and `deleted_at` columns. `deleted_at` is currently unused. - Use SQLite's built-in timestamp features to populate these. Add a trigger to update `updated_at` when the row is updated. Currently no way to update a row. - Rename the `id` column in `images` to `image_name` - Rename `ImageCategory.IMAGE` to `ImageCategory.GENERAL` - Move all exceptions outside their base classes to make them more portable. - Add `width` and `height` columns to the database. These store the actual dimensions of the image file, whereas the metadata's `width` and `height` refer to the respective generation parameters and are nullable. - Make `deserialize_image_record` take a `dict` instead of `sqlite3.Row` - Improve comments throughout - Tidy up unused code/files and some minor organisation --- invokeai/app/api/routers/images.py | 2 +- invokeai/app/invocations/generate.py | 4 +- invokeai/app/invocations/latent.py | 2 +- invokeai/app/models/image.py | 30 +- invokeai/app/models/metadata.py | 16 + invokeai/app/models/resources.py | 28 - invokeai/app/services/db.ipynb | 578 ------------------ invokeai/app/services/image_file_storage.py | 46 +- invokeai/app/services/image_record_storage.py | 202 +++--- invokeai/app/services/images.py | 116 ++-- invokeai/app/services/models/image_record.py | 90 ++- invokeai/app/util/{enum.py => metaenum.py} | 5 +- 12 files changed, 273 insertions(+), 846 deletions(-) delete mode 100644 invokeai/app/models/resources.py delete mode 100644 invokeai/app/services/db.ipynb rename invokeai/app/util/{enum.py => metaenum.py} (61%) diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 5914626a70..123774b721 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -30,7 +30,7 @@ async def upload_image( image_type: ImageType, request: Request, response: Response, - image_category: ImageCategory = ImageCategory.IMAGE, + image_category: ImageCategory = ImageCategory.GENERAL, ) -> ImageDTO: """Uploads an image""" if not file.content_type.startswith("image"): diff --git a/invokeai/app/invocations/generate.py b/invokeai/app/invocations/generate.py index 64c5662137..3b3e5512c7 100644 --- a/invokeai/app/invocations/generate.py +++ b/invokeai/app/invocations/generate.py @@ -95,7 +95,7 @@ class TextToImageInvocation(BaseInvocation, SDImageInvocation): image_dto = context.services.images_new.create( image=generate_output.image, image_type=ImageType.RESULT, - image_category=ImageCategory.IMAGE, + image_category=ImageCategory.GENERAL, session_id=context.graph_execution_state_id, node_id=self.id, ) @@ -119,7 +119,7 @@ class TextToImageInvocation(BaseInvocation, SDImageInvocation): # context.services.images_db.set( # id=image_name, # image_type=ImageType.RESULT, - # image_category=ImageCategory.IMAGE, + # image_category=ImageCategory.GENERAL, # session_id=context.graph_execution_state_id, # node_id=self.id, # metadata=GeneratedImageOrLatentsMetadata(), diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 7259beb1a8..adba88274f 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -372,7 +372,7 @@ class LatentsToImageInvocation(BaseInvocation): image_dto = context.services.images_new.create( image=image, image_type=ImageType.RESULT, - image_category=ImageCategory.IMAGE, + image_category=ImageCategory.GENERAL, session_id=context.graph_execution_state_id, node_id=self.id, ) diff --git a/invokeai/app/models/image.py b/invokeai/app/models/image.py index f364abdb71..1d493e65c2 100644 --- a/invokeai/app/models/image.py +++ b/invokeai/app/models/image.py @@ -2,7 +2,7 @@ from enum import Enum from typing import Optional, Tuple from pydantic import BaseModel, Field -from invokeai.app.util.enum import MetaEnum +from invokeai.app.util.metaenum import MetaEnum class ImageType(str, Enum, metaclass=MetaEnum): @@ -13,20 +13,32 @@ class ImageType(str, Enum, metaclass=MetaEnum): INTERMEDIATE = "intermediates" +class InvalidImageTypeException(ValueError): + """Raised when a provided value is not a valid ImageType. + + Subclasses `ValueError`. + """ + + def __init__(self, message="Invalid image type."): + super().__init__(message) + + class ImageCategory(str, Enum, metaclass=MetaEnum): """The category of an image. Use ImageCategory.OTHER for non-default categories.""" - IMAGE = "image" - CONTROL_IMAGE = "control_image" + GENERAL = "general" + CONTROL = "control" OTHER = "other" -def is_image_type(obj): - try: - ImageType(obj) - except ValueError: - return False - return True +class InvalidImageCategoryException(ValueError): + """Raised when a provided value is not a valid ImageCategory. + + Subclasses `ValueError`. + """ + + def __init__(self, message="Invalid image category."): + super().__init__(message) class ImageField(BaseModel): diff --git a/invokeai/app/models/metadata.py b/invokeai/app/models/metadata.py index 481f2c1ff6..ac87405423 100644 --- a/invokeai/app/models/metadata.py +++ b/invokeai/app/models/metadata.py @@ -26,50 +26,66 @@ class ImageMetadata(BaseModel): default=None, description="The type of the ancestor node of the image output node.", ) + """The type of the ancestor node of the image output node.""" positive_conditioning: Optional[StrictStr] = Field( default=None, description="The positive conditioning." ) + """The positive conditioning""" negative_conditioning: Optional[StrictStr] = Field( default=None, description="The negative conditioning." ) + """The negative conditioning""" width: Optional[StrictInt] = Field( default=None, description="Width of the image/latents in pixels." ) + """Width of the image/latents in pixels""" height: Optional[StrictInt] = Field( default=None, description="Height of the image/latents in pixels." ) + """Height of the image/latents in pixels""" seed: Optional[StrictInt] = Field( default=None, description="The seed used for noise generation." ) + """The seed used for noise generation""" cfg_scale: Optional[StrictFloat] = Field( default=None, description="The classifier-free guidance scale." ) + """The classifier-free guidance scale""" steps: Optional[StrictInt] = Field( default=None, description="The number of steps used for inference." ) + """The number of steps used for inference""" scheduler: Optional[StrictStr] = Field( default=None, description="The scheduler used for inference." ) + """The scheduler used for inference""" model: Optional[StrictStr] = Field( default=None, description="The model used for inference." ) + """The model used for inference""" strength: Optional[StrictFloat] = Field( default=None, description="The strength used for image-to-image/latents-to-latents.", ) + """The strength used for image-to-image/latents-to-latents.""" latents: Optional[StrictStr] = Field( default=None, description="The ID of the initial latents." ) + """The ID of the initial latents""" vae: Optional[StrictStr] = Field( default=None, description="The VAE used for decoding." ) + """The VAE used for decoding""" unet: Optional[StrictStr] = Field( default=None, description="The UNet used dor inference." ) + """The UNet used dor inference""" clip: Optional[StrictStr] = Field( default=None, description="The CLIP Encoder used for conditioning." ) + """The CLIP Encoder used for conditioning""" extra: Optional[StrictStr] = Field( default=None, description="Uploaded image metadata, extracted from the PNG tEXt chunk.", ) + """Uploaded image metadata, extracted from the PNG tEXt chunk.""" diff --git a/invokeai/app/models/resources.py b/invokeai/app/models/resources.py deleted file mode 100644 index 1cd22e4550..0000000000 --- a/invokeai/app/models/resources.py +++ /dev/null @@ -1,28 +0,0 @@ -# TODO: Make a new model for this -from enum import Enum - -from invokeai.app.util.enum import MetaEnum - - -class ResourceType(str, Enum, metaclass=MetaEnum): - """The type of a resource.""" - - IMAGES = "images" - TENSORS = "tensors" - - -# class ResourceOrigin(str, Enum, metaclass=MetaEnum): -# """The origin of a resource (eg image or tensor).""" - -# RESULTS = "results" -# UPLOADS = "uploads" -# INTERMEDIATES = "intermediates" - - - -class TensorKind(str, Enum, metaclass=MetaEnum): - """The kind of a tensor. Use TensorKind.OTHER for non-default kinds.""" - - IMAGE_LATENTS = "image_latents" - CONDITIONING = "conditioning" - OTHER = "other" diff --git a/invokeai/app/services/db.ipynb b/invokeai/app/services/db.ipynb deleted file mode 100644 index 67dfe22128..0000000000 --- a/invokeai/app/services/db.ipynb +++ /dev/null @@ -1,578 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 40, - "metadata": {}, - "outputs": [], - "source": [ - "from abc import ABC, abstractmethod\n", - "from enum import Enum\n", - "import enum\n", - "import sqlite3\n", - "import threading\n", - "from typing import Optional, Type, TypeVar, Union\n", - "from PIL.Image import Image as PILImage\n", - "from pydantic import BaseModel, Field\n", - "from torch import Tensor" - ] - }, - { - "cell_type": "code", - "execution_count": 41, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "class ResourceOrigin(str, Enum):\n", - " \"\"\"The origin of a resource (eg image or tensor).\"\"\"\n", - "\n", - " RESULTS = \"results\"\n", - " UPLOADS = \"uploads\"\n", - " INTERMEDIATES = \"intermediates\"\n", - "\n", - "\n", - "class ImageKind(str, Enum):\n", - " \"\"\"The kind of an image. Use ImageKind.OTHER for non-default kinds.\"\"\"\n", - "\n", - " IMAGE = \"image\"\n", - " CONTROL_IMAGE = \"control_image\"\n", - " OTHER = \"other\"\n", - "\n", - "\n", - "class TensorKind(str, Enum):\n", - " \"\"\"The kind of a tensor. Use TensorKind.OTHER for non-default kinds.\"\"\"\n", - "\n", - " IMAGE_LATENTS = \"image_latents\"\n", - " CONDITIONING = \"conditioning\"\n", - " OTHER = \"other\"\n" - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "def create_sql_values_string_from_string_enum(enum: Type[Enum]):\n", - " \"\"\"\n", - " Creates a string of the form \"('value1'), ('value2'), ..., ('valueN')\" from a StrEnum.\n", - " \"\"\"\n", - "\n", - " delimiter = \", \"\n", - " values = [f\"('{e.value}')\" for e in enum]\n", - " return delimiter.join(values)\n", - "\n", - "\n", - "def create_sql_table_from_enum(\n", - " enum: Type[Enum],\n", - " table_name: str,\n", - " primary_key_name: str,\n", - " conn: sqlite3.Connection,\n", - " cursor: sqlite3.Cursor,\n", - " lock: threading.Lock,\n", - "):\n", - " \"\"\"\n", - " Creates and populates a table to be used as a functional enum.\n", - " \"\"\"\n", - "\n", - " try:\n", - " lock.acquire()\n", - "\n", - " values_string = create_sql_values_string_from_string_enum(enum)\n", - "\n", - " cursor.execute(\n", - " f\"\"\"--sql\n", - " CREATE TABLE IF NOT EXISTS {table_name} (\n", - " {primary_key_name} TEXT PRIMARY KEY\n", - " );\n", - " \"\"\"\n", - " )\n", - " cursor.execute(\n", - " f\"\"\"--sql\n", - " INSERT OR IGNORE INTO {table_name} ({primary_key_name}) VALUES {values_string};\n", - " \"\"\"\n", - " )\n", - " conn.commit()\n", - " finally:\n", - " lock.release()\n", - "\n", - "\n", - "\"\"\"\n", - "`resource_origins` functions as an enum for the ResourceOrigin model.\n", - "\"\"\"\n", - "\n", - "\n", - "# def create_resource_origins_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", - "# create_sql_table_from_enum(\n", - "# enum=ResourceOrigin,\n", - "# table_name=\"resource_origins\",\n", - "# primary_key_name=\"origin_name\",\n", - "# conn=conn,\n", - "# cursor=cursor,\n", - "# lock=lock,\n", - "# )\n", - "\n", - "\n", - "\"\"\"\n", - "`image_kinds` functions as an enum for the ImageType model.\n", - "\"\"\"\n", - "\n", - "\n", - "# def create_image_kinds_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", - " # create_sql_table_from_enum(\n", - " # enum=ImageKind,\n", - " # table_name=\"image_kinds\",\n", - " # primary_key_name=\"kind_name\",\n", - " # conn=conn,\n", - " # cursor=cursor,\n", - " # lock=lock,\n", - " # )\n", - "\n", - "\n", - "\"\"\"\n", - "`tensor_kinds` functions as an enum for the TensorType model.\n", - "\"\"\"\n", - "\n", - "\n", - "# def create_tensor_kinds_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", - " # create_sql_table_from_enum(\n", - " # enum=TensorKind,\n", - " # table_name=\"tensor_kinds\",\n", - " # primary_key_name=\"kind_name\",\n", - " # conn=conn,\n", - " # cursor=cursor,\n", - " # lock=lock,\n", - " # )\n", - "\n", - "\n", - "\"\"\"\n", - "`images` stores all images, regardless of type\n", - "\"\"\"\n", - "\n", - "\n", - "def create_images_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", - " try:\n", - " lock.acquire()\n", - "\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE TABLE IF NOT EXISTS images (\n", - " id TEXT PRIMARY KEY,\n", - " origin TEXT,\n", - " image_kind TEXT,\n", - " created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n", - " FOREIGN KEY(origin) REFERENCES resource_origins(origin_name),\n", - " FOREIGN KEY(image_kind) REFERENCES image_kinds(kind_name)\n", - " );\n", - " \"\"\"\n", - " )\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE UNIQUE INDEX IF NOT EXISTS idx_images_id ON images(id);\n", - " \"\"\"\n", - " )\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE INDEX IF NOT EXISTS idx_images_origin ON images(origin);\n", - " \"\"\"\n", - " )\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE INDEX IF NOT EXISTS idx_images_image_kind ON images(image_kind);\n", - " \"\"\"\n", - " )\n", - " conn.commit()\n", - " finally:\n", - " lock.release()\n", - "\n", - "\n", - "\"\"\"\n", - "`images_results` stores additional data specific to `results` images.\n", - "\"\"\"\n", - "\n", - "\n", - "def create_images_results_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", - " try:\n", - " lock.acquire()\n", - "\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE TABLE IF NOT EXISTS images_results (\n", - " images_id TEXT PRIMARY KEY,\n", - " session_id TEXT NOT NULL,\n", - " node_id TEXT NOT NULL,\n", - " FOREIGN KEY(images_id) REFERENCES images(id) ON DELETE CASCADE\n", - " );\n", - " \"\"\"\n", - " )\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE UNIQUE INDEX IF NOT EXISTS idx_images_results_images_id ON images_results(images_id);\n", - " \"\"\"\n", - " )\n", - " conn.commit()\n", - " finally:\n", - " lock.release()\n", - "\n", - "\n", - "\"\"\"\n", - "`images_intermediates` stores additional data specific to `intermediates` images\n", - "\"\"\"\n", - "\n", - "\n", - "def create_images_intermediates_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", - " try:\n", - " lock.acquire()\n", - "\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE TABLE IF NOT EXISTS images_intermediates (\n", - " images_id TEXT PRIMARY KEY,\n", - " session_id TEXT NOT NULL,\n", - " node_id TEXT NOT NULL,\n", - " FOREIGN KEY(images_id) REFERENCES images(id) ON DELETE CASCADE\n", - " );\n", - " \"\"\"\n", - " )\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE UNIQUE INDEX IF NOT EXISTS idx_images_intermediates_images_id ON images_intermediates(images_id);\n", - " \"\"\"\n", - " )\n", - " conn.commit()\n", - " finally:\n", - " lock.release()\n", - "\n", - "\n", - "\"\"\"\n", - "`images_metadata` stores basic metadata for any image type\n", - "\"\"\"\n", - "\n", - "\n", - "def create_images_metadata_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", - " try:\n", - " lock.acquire()\n", - "\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE TABLE IF NOT EXISTS images_metadata (\n", - " images_id TEXT PRIMARY KEY,\n", - " metadata TEXT,\n", - " FOREIGN KEY(images_id) REFERENCES images(id) ON DELETE CASCADE\n", - " );\n", - " \"\"\"\n", - " )\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE UNIQUE INDEX IF NOT EXISTS idx_images_metadata_images_id ON images_metadata(images_id);\n", - " \"\"\"\n", - " )\n", - " conn.commit()\n", - " finally:\n", - " lock.release()\n", - "\n", - "\n", - "# `tensors` table: stores references to tensor\n", - "\n", - "\n", - "def create_tensors_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", - " try:\n", - " lock.acquire()\n", - "\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE TABLE IF NOT EXISTS tensors (\n", - " id TEXT PRIMARY KEY,\n", - " origin TEXT,\n", - " tensor_kind TEXT,\n", - " created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n", - " FOREIGN KEY(origin) REFERENCES resource_origins(origin_name),\n", - " FOREIGN KEY(tensor_kind) REFERENCES tensor_kinds(kind_name)\n", - " );\n", - " \"\"\"\n", - " )\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE UNIQUE INDEX IF NOT EXISTS idx_tensors_id ON tensors(id);\n", - " \"\"\"\n", - " )\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE INDEX IF NOT EXISTS idx_tensors_origin ON tensors(origin);\n", - " \"\"\"\n", - " )\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE INDEX IF NOT EXISTS idx_tensors_tensor_kind ON tensors(tensor_kind);\n", - " \"\"\"\n", - " )\n", - " conn.commit()\n", - " finally:\n", - " lock.release()\n", - "\n", - "\n", - "# `tensors_results` stores additional data specific to `result` tensor\n", - "\n", - "\n", - "def create_tensors_results_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", - " try:\n", - " lock.acquire()\n", - "\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE TABLE IF NOT EXISTS tensors_results (\n", - " tensors_id TEXT PRIMARY KEY,\n", - " session_id TEXT NOT NULL,\n", - " node_id TEXT NOT NULL,\n", - " FOREIGN KEY(tensors_id) REFERENCES tensors(id) ON DELETE CASCADE\n", - " );\n", - " \"\"\"\n", - " )\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE UNIQUE INDEX IF NOT EXISTS idx_tensors_results_tensors_id ON tensors_results(tensors_id);\n", - " \"\"\"\n", - " )\n", - " conn.commit()\n", - " finally:\n", - " lock.release()\n", - "\n", - "\n", - "# `tensors_intermediates` stores additional data specific to `intermediate` tensor\n", - "\n", - "\n", - "def create_tensors_intermediates_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", - " try:\n", - " lock.acquire()\n", - "\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE TABLE IF NOT EXISTS tensors_intermediates (\n", - " tensors_id TEXT PRIMARY KEY,\n", - " session_id TEXT NOT NULL,\n", - " node_id TEXT NOT NULL,\n", - " FOREIGN KEY(tensors_id) REFERENCES tensors(id) ON DELETE CASCADE\n", - " );\n", - " \"\"\"\n", - " )\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE UNIQUE INDEX IF NOT EXISTS idx_tensors_intermediates_tensors_id ON tensors_intermediates(tensors_id);\n", - " \"\"\"\n", - " )\n", - " conn.commit()\n", - " finally:\n", - " lock.release()\n", - "\n", - "\n", - "# `tensors_metadata` table: stores generated/transformed metadata for tensor\n", - "\n", - "\n", - "def create_tensors_metadata_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor, lock: threading.Lock):\n", - " try:\n", - " lock.acquire()\n", - "\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE TABLE IF NOT EXISTS tensors_metadata (\n", - " tensors_id TEXT PRIMARY KEY,\n", - " metadata TEXT,\n", - " FOREIGN KEY(tensors_id) REFERENCES tensors(id) ON DELETE CASCADE\n", - " );\n", - " \"\"\"\n", - " )\n", - " cursor.execute(\n", - " \"\"\"--sql\n", - " CREATE UNIQUE INDEX IF NOT EXISTS idx_tensors_metadata_tensors_id ON tensors_metadata(tensors_id);\n", - " \"\"\"\n", - " )\n", - " conn.commit()\n", - " finally:\n", - " lock.release()\n" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "db_path = '/home/bat/Documents/Code/outputs/test.db'\n", - "if (os.path.exists(db_path)):\n", - " os.remove(db_path)\n", - "\n", - "conn = sqlite3.connect(\n", - " db_path, check_same_thread=False\n", - ")\n", - "cursor = conn.cursor()\n", - "lock = threading.Lock()" - ] - }, - { - "cell_type": "code", - "execution_count": 44, - "metadata": {}, - "outputs": [], - "source": [ - "create_sql_table_from_enum(\n", - " enum=ResourceOrigin,\n", - " table_name=\"resource_origins\",\n", - " primary_key_name=\"origin_name\",\n", - " conn=conn,\n", - " cursor=cursor,\n", - " lock=lock,\n", - ")\n", - "\n", - "create_sql_table_from_enum(\n", - " enum=ImageKind,\n", - " table_name=\"image_kinds\",\n", - " primary_key_name=\"kind_name\",\n", - " conn=conn,\n", - " cursor=cursor,\n", - " lock=lock,\n", - ")\n", - "\n", - "create_sql_table_from_enum(\n", - " enum=TensorKind,\n", - " table_name=\"tensor_kinds\",\n", - " primary_key_name=\"kind_name\",\n", - " conn=conn,\n", - " cursor=cursor,\n", - " lock=lock,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "metadata": {}, - "outputs": [], - "source": [ - "create_images_table(conn, cursor, lock)\n", - "create_images_results_table(conn, cursor, lock)\n", - "create_images_intermediates_table(conn, cursor, lock)\n", - "create_images_metadata_table(conn, cursor, lock)" - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "metadata": {}, - "outputs": [], - "source": [ - "create_tensors_table(conn, cursor, lock)\n", - "create_tensors_results_table(conn, cursor, lock)\n", - "create_tensors_intermediates_table(conn, cursor, lock)\n", - "create_tensors_metadata_table(conn, cursor, lock)" - ] - }, - { - "cell_type": "code", - "execution_count": 59, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "from pydantic import StrictStr\n", - "\n", - "\n", - "class GeneratedImageOrLatentsMetadata(BaseModel):\n", - " \"\"\"Core generation metadata for an image/tensor generated in InvokeAI.\n", - "\n", - " Generated by traversing the execution graph, collecting the parameters of the nearest ancestors of a given node.\n", - "\n", - " Full metadata may be accessed by querying for the session in the `graph_executions` table.\n", - " \"\"\"\n", - "\n", - " positive_conditioning: Optional[StrictStr] = Field(\n", - " default=None, description=\"The positive conditioning.\"\n", - " )\n", - " negative_conditioning: Optional[str] = Field(\n", - " default=None, description=\"The negative conditioning.\"\n", - " )\n", - " width: Optional[int] = Field(\n", - " default=None, description=\"Width of the image/tensor in pixels.\"\n", - " )\n", - " height: Optional[int] = Field(\n", - " default=None, description=\"Height of the image/tensor in pixels.\"\n", - " )\n", - " seed: Optional[int] = Field(\n", - " default=None, description=\"The seed used for noise generation.\"\n", - " )\n", - " cfg_scale: Optional[float] = Field(\n", - " default=None, description=\"The classifier-free guidance scale.\"\n", - " )\n", - " steps: Optional[int] = Field(\n", - " default=None, description=\"The number of steps used for inference.\"\n", - " )\n", - " scheduler: Optional[str] = Field(\n", - " default=None, description=\"The scheduler used for inference.\"\n", - " )\n", - " model: Optional[str] = Field(\n", - " default=None, description=\"The model used for inference.\"\n", - " )\n", - " strength: Optional[float] = Field(\n", - " default=None,\n", - " description=\"The strength used for image-to-image/tensor-to-tensor.\",\n", - " )\n", - " image: Optional[str] = Field(\n", - " default=None, description=\"The ID of the initial image.\"\n", - " )\n", - " tensor: Optional[str] = Field(\n", - " default=None, description=\"The ID of the initial tensor.\"\n", - " )\n", - " # Pending model refactor:\n", - " # vae: Optional[str] = Field(default=None,description=\"The VAE used for decoding.\")\n", - " # unet: Optional[str] = Field(default=None,description=\"The UNet used dor inference.\")\n", - " # clip: Optional[str] = Field(default=None,description=\"The CLIP Encoder used for conditioning.\")\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 61, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "GeneratedImageOrLatentsMetadata(positive_conditioning='123', negative_conditioning=None, width=None, height=None, seed=None, cfg_scale=None, steps=None, scheduler=None, model=None, strength=None, image=None, tensor=None)" - ] - }, - "execution_count": 61, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "GeneratedImageOrLatentsMetadata(positive_conditioning='123')" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.6" - }, - "orig_nbformat": 4 - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/invokeai/app/services/image_file_storage.py b/invokeai/app/services/image_file_storage.py index dadb9584d5..1b4466e06e 100644 --- a/invokeai/app/services/image_file_storage.py +++ b/invokeai/app/services/image_file_storage.py @@ -14,27 +14,31 @@ from invokeai.app.models.metadata import ImageMetadata from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail +# TODO: Should these excpetions subclass existing python exceptions? +class ImageFileNotFoundException(Exception): + """Raised when an image file is not found in storage.""" + + def __init__(self, message="Image file not found"): + super().__init__(message) + + +class ImageFileSaveException(Exception): + """Raised when an image cannot be saved.""" + + def __init__(self, message="Image file not saved"): + super().__init__(message) + + +class ImageFileDeleteException(Exception): + """Raised when an image cannot be deleted.""" + + def __init__(self, message="Image file not deleted"): + super().__init__(message) + + class ImageFileStorageBase(ABC): """Low-level service responsible for storing and retrieving image files.""" - class ImageFileNotFoundException(Exception): - """Raised when an image file is not found in storage.""" - - def __init__(self, message="Image file not found"): - super().__init__(message) - - class ImageFileSaveException(Exception): - """Raised when an image cannot be saved.""" - - def __init__(self, message="Image file not saved"): - super().__init__(message) - - class ImageFileDeleteException(Exception): - """Raised when an image cannot be deleted.""" - - def __init__(self, message="Image file not deleted"): - super().__init__(message) - @abstractmethod def get(self, image_type: ImageType, image_name: str) -> PILImageType: """Retrieves an image as PIL Image.""" @@ -102,7 +106,7 @@ class DiskImageFileStorage(ImageFileStorageBase): self.__set_cache(image_path, image) return image except FileNotFoundError as e: - raise ImageFileStorageBase.ImageFileNotFoundException from e + raise ImageFileNotFoundException from e def save( self, @@ -130,7 +134,7 @@ class DiskImageFileStorage(ImageFileStorageBase): self.__set_cache(image_path, image) self.__set_cache(thumbnail_path, thumbnail_image) except Exception as e: - raise ImageFileStorageBase.ImageFileSaveException from e + raise ImageFileSaveException from e def delete(self, image_type: ImageType, image_name: str) -> None: try: @@ -150,7 +154,7 @@ class DiskImageFileStorage(ImageFileStorageBase): if thumbnail_path in self.__cache: del self.__cache[thumbnail_path] except Exception as e: - raise ImageFileStorageBase.ImageFileDeleteException from e + raise ImageFileDeleteException from e # TODO: make this a bit more flexible for e.g. cloud storage def get_path( diff --git a/invokeai/app/services/image_record_storage.py b/invokeai/app/services/image_record_storage.py index d6b421a094..4e1f73978b 100644 --- a/invokeai/app/services/image_record_storage.py +++ b/invokeai/app/services/image_record_storage.py @@ -1,7 +1,6 @@ from abc import ABC, abstractmethod -import datetime -from enum import Enum -from typing import Optional, Type +from datetime import datetime +from typing import Optional, cast import sqlite3 import threading from typing import Optional, Union @@ -18,62 +17,32 @@ from invokeai.app.services.models.image_record import ( from invokeai.app.services.item_storage import PaginatedResults -def create_sql_values_string_from_string_enum(enum: Type[Enum]): - """ - Creates a string of the form "('value1'), ('value2'), ..., ('valueN')" from a StrEnum. - """ +# TODO: Should these excpetions subclass existing python exceptions? +class ImageRecordNotFoundException(Exception): + """Raised when an image record is not found.""" - delimiter = ", " - values = [f"('{e.value}')" for e in enum] - return delimiter.join(values) + def __init__(self, message="Image record not found"): + super().__init__(message) -def create_enum_table( - enum: Type[Enum], - table_name: str, - primary_key_name: str, - cursor: sqlite3.Cursor, -): - """ - Creates and populates a table to be used as a functional enum. - """ +class ImageRecordSaveException(Exception): + """Raised when an image record cannot be saved.""" - values_string = create_sql_values_string_from_string_enum(enum) + def __init__(self, message="Image record not saved"): + super().__init__(message) - cursor.execute( - f"""--sql - CREATE TABLE IF NOT EXISTS {table_name} ( - {primary_key_name} TEXT PRIMARY KEY - ); - """ - ) - cursor.execute( - f"""--sql - INSERT OR IGNORE INTO {table_name} ({primary_key_name}) VALUES {values_string}; - """ - ) + +class ImageRecordDeleteException(Exception): + """Raised when an image record cannot be deleted.""" + + def __init__(self, message="Image record not deleted"): + super().__init__(message) class ImageRecordStorageBase(ABC): """Low-level service responsible for interfacing with the image record store.""" - class ImageRecordNotFoundException(Exception): - """Raised when an image record is not found.""" - - def __init__(self, message="Image record not found"): - super().__init__(message) - - class ImageRecordSaveException(Exception): - """Raised when an image record cannot be saved.""" - - def __init__(self, message="Image record not saved"): - super().__init__(message) - - class ImageRecordDeleteException(Exception): - """Raised when an image record cannot be deleted.""" - - def __init__(self, message="Image record not deleted"): - super().__init__(message) + # TODO: Implement an `update()` method @abstractmethod def get(self, image_type: ImageType, image_name: str) -> ImageRecord: @@ -91,6 +60,8 @@ class ImageRecordStorageBase(ABC): """Gets a page of image records.""" pass + # TODO: The database has a nullable `deleted_at` column, currently unused. + # Should we implement soft deletes? Would need coordination with ImageFileStorage. @abstractmethod def delete(self, image_type: ImageType, image_name: str) -> None: """Deletes an image record.""" @@ -102,11 +73,12 @@ class ImageRecordStorageBase(ABC): image_name: str, image_type: ImageType, image_category: ImageCategory, + width: int, + height: int, session_id: Optional[str], node_id: Optional[str], metadata: Optional[ImageMetadata], - created_at: str = datetime.datetime.utcnow().isoformat(), - ) -> None: + ) -> datetime: """Saves an image record.""" pass @@ -141,17 +113,23 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): # Create the `images` table. self._cursor.execute( - f"""--sql + """--sql CREATE TABLE IF NOT EXISTS images ( - id TEXT PRIMARY KEY, - image_type TEXT, -- non-nullable via foreign key constraint - image_category TEXT, -- non-nullable via foreign key constraint - session_id TEXT, -- nullable - node_id TEXT, -- nullable - metadata TEXT, -- nullable - created_at TEXT NOT NULL, - FOREIGN KEY(image_type) REFERENCES image_types(type_name), - FOREIGN KEY(image_category) REFERENCES image_categories(category_name) + image_name TEXT NOT NULL PRIMARY KEY, + -- This is an enum in python, unrestricted string here for flexibility + image_type TEXT NOT NULL, + -- This is an enum in python, unrestricted string here for flexibility + image_category TEXT NOT NULL, + width INTEGER NOT NULL, + height INTEGER NOT NULL, + session_id TEXT, + node_id TEXT, + metadata TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + -- Updated via trigger + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + -- Soft delete, currently unused + deleted_at DATETIME ); """ ) @@ -159,7 +137,7 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): # Create the `images` table indices. self._cursor.execute( """--sql - CREATE UNIQUE INDEX IF NOT EXISTS idx_images_id ON images(id); + CREATE UNIQUE INDEX IF NOT EXISTS idx_images_image_name ON images(image_name); """ ) self._cursor.execute( @@ -172,53 +150,22 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): CREATE INDEX IF NOT EXISTS idx_images_image_category ON images(image_category); """ ) - - # Create the tables for image-related enums - create_enum_table( - enum=ImageType, - table_name="image_types", - primary_key_name="type_name", - cursor=self._cursor, - ) - - create_enum_table( - enum=ImageCategory, - table_name="image_categories", - primary_key_name="category_name", - cursor=self._cursor, - ) - - # Create the `tags` table. TODO: do this elsewhere, shouldn't be in images db service self._cursor.execute( """--sql - CREATE TABLE IF NOT EXISTS tags ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - tag_name TEXT UNIQUE NOT NULL - ); + CREATE INDEX IF NOT EXISTS idx_images_created_at ON images(created_at); """ ) - # Create the `images_tags` junction table. + # Add trigger for `updated_at`. self._cursor.execute( """--sql - CREATE TABLE IF NOT EXISTS images_tags ( - image_id TEXT, - tag_id INTEGER, - PRIMARY KEY (image_id, tag_id), - FOREIGN KEY(image_id) REFERENCES images(id) ON DELETE CASCADE, - FOREIGN KEY(tag_id) REFERENCES tags(id) ON DELETE CASCADE - ); - """ - ) - - # Create the `images_favorites` table. - self._cursor.execute( - """--sql - CREATE TABLE IF NOT EXISTS images_favorites ( - image_id TEXT PRIMARY KEY, - favorited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY(image_id) REFERENCES images(id) ON DELETE CASCADE - ); + CREATE TRIGGER IF NOT EXISTS tg_images_updated_at + AFTER UPDATE + ON images FOR EACH ROW + BEGIN + UPDATE images SET updated_at = current_timestamp + WHERE image_name = old.image_name; + END; """ ) @@ -229,22 +176,22 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): self._cursor.execute( f"""--sql SELECT * FROM images - WHERE id = ?; + WHERE image_name = ?; """, (image_name,), ) - result = self._cursor.fetchone() + result = cast(Union[sqlite3.Row, None], self._cursor.fetchone()) except sqlite3.Error as e: self._conn.rollback() - raise self.ImageRecordNotFoundException from e + raise ImageRecordNotFoundException from e finally: self._lock.release() if not result: - raise self.ImageRecordNotFoundException + raise ImageRecordNotFoundException - return deserialize_image_record(result) + return deserialize_image_record(dict(result)) def get_many( self, @@ -260,14 +207,15 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): f"""--sql SELECT * FROM images WHERE image_type = ? AND image_category = ? + ORDER BY created_at DESC LIMIT ? OFFSET ?; """, (image_type.value, image_category.value, per_page, page * per_page), ) - result = self._cursor.fetchall() + result = cast(list[sqlite3.Row], self._cursor.fetchall()) - images = list(map(lambda r: deserialize_image_record(r), result)) + images = list(map(lambda r: deserialize_image_record(dict(r)), result)) self._cursor.execute( """--sql @@ -296,14 +244,14 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): self._cursor.execute( """--sql DELETE FROM images - WHERE id = ?; + WHERE image_name = ?; """, (image_name,), ) self._conn.commit() except sqlite3.Error as e: self._conn.rollback() - raise ImageRecordStorageBase.ImageRecordDeleteException from e + raise ImageRecordDeleteException from e finally: self._lock.release() @@ -313,10 +261,11 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): image_type: ImageType, image_category: ImageCategory, session_id: Optional[str], + width: int, + height: int, node_id: Optional[str], metadata: Optional[ImageMetadata], - created_at: str, - ) -> None: + ) -> datetime: try: metadata_json = ( None if metadata is None else metadata.json(exclude_none=True) @@ -325,29 +274,44 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): self._cursor.execute( """--sql INSERT OR IGNORE INTO images ( - id, + image_name, image_type, image_category, + width, + height, node_id, session_id, - metadata, - created_at + metadata ) - VALUES (?, ?, ?, ?, ?, ?, ?); + VALUES (?, ?, ?, ?, ?, ?, ?, ?); """, ( image_name, image_type.value, image_category.value, + width, + height, node_id, session_id, metadata_json, - created_at, ), ) self._conn.commit() + + self._cursor.execute( + """--sql + SELECT created_at + FROM images + WHERE image_name = ?; + """, + (image_name,), + ) + + created_at = datetime.fromisoformat(self._cursor.fetchone()[0]) + + return created_at except sqlite3.Error as e: self._conn.rollback() - raise ImageRecordStorageBase.ImageRecordNotFoundException from e + raise ImageRecordSaveException from e finally: self._lock.release() diff --git a/invokeai/app/services/images.py b/invokeai/app/services/images.py index e018e78f09..2b2322085d 100644 --- a/invokeai/app/services/images.py +++ b/invokeai/app/services/images.py @@ -4,9 +4,17 @@ from typing import Optional, TYPE_CHECKING, Union import uuid from PIL.Image import Image as PILImageType -from invokeai.app.models.image import ImageCategory, ImageType +from invokeai.app.models.image import ( + ImageCategory, + ImageType, + InvalidImageCategoryException, + InvalidImageTypeException, +) from invokeai.app.models.metadata import ImageMetadata from invokeai.app.services.image_record_storage import ( + ImageRecordDeleteException, + ImageRecordNotFoundException, + ImageRecordSaveException, ImageRecordStorageBase, ) from invokeai.app.services.models.image_record import ( @@ -14,7 +22,12 @@ from invokeai.app.services.models.image_record import ( ImageDTO, image_record_to_dto, ) -from invokeai.app.services.image_file_storage import ImageFileStorageBase +from invokeai.app.services.image_file_storage import ( + ImageFileDeleteException, + ImageFileNotFoundException, + ImageFileSaveException, + ImageFileStorageBase, +) from invokeai.app.services.item_storage import ItemStorageABC, PaginatedResults from invokeai.app.services.metadata import MetadataServiceBase from invokeai.app.services.urls import UrlServiceBase @@ -50,6 +63,11 @@ class ImageServiceABC(ABC): """Gets an image record.""" pass + @abstractmethod + def get_dto(self, image_type: ImageType, image_name: str) -> ImageDTO: + """Gets an image DTO.""" + pass + @abstractmethod def get_path(self, image_type: ImageType, image_name: str) -> str: """Gets an image's path""" @@ -62,11 +80,6 @@ class ImageServiceABC(ABC): """Gets an image's or thumbnail's URL""" pass - @abstractmethod - def get_dto(self, image_type: ImageType, image_name: str) -> ImageDTO: - """Gets an image DTO.""" - pass - @abstractmethod def get_many( self, @@ -83,26 +96,6 @@ class ImageServiceABC(ABC): """Deletes an image.""" pass - @abstractmethod - def add_tag(self, image_type: ImageType, image_id: str, tag: str) -> None: - """Adds a tag to an image.""" - pass - - @abstractmethod - def remove_tag(self, image_type: ImageType, image_id: str, tag: str) -> None: - """Removes a tag from an image.""" - pass - - @abstractmethod - def favorite(self, image_type: ImageType, image_id: str) -> None: - """Favorites an image.""" - pass - - @abstractmethod - def unfavorite(self, image_type: ImageType, image_id: str) -> None: - """Unfavorites an image.""" - pass - class ImageServiceDependencies: """Service dependencies for the ImageService.""" @@ -160,6 +153,12 @@ class ImageService(ImageServiceABC): node_id: Optional[str] = None, session_id: Optional[str] = None, ) -> ImageDTO: + if image_type not in ImageType: + raise InvalidImageTypeException + + if image_category not in ImageCategory: + raise InvalidImageCategoryException + image_name = self._create_image_name( image_type=image_type, image_category=image_category, @@ -167,11 +166,25 @@ class ImageService(ImageServiceABC): session_id=session_id, ) - timestamp = get_iso_timestamp() metadata = self._get_metadata(session_id, node_id) + (width, height) = image.size + try: # TODO: Consider using a transaction here to ensure consistency between storage and database + created_at = self._services.records.save( + # Non-nullable fields + image_name=image_name, + image_type=image_type, + image_category=image_category, + width=width, + height=height, + # Nullable fields + node_id=node_id, + session_id=session_id, + metadata=metadata, + ) + self._services.files.save( image_type=image_type, image_name=image_name, @@ -179,36 +192,34 @@ class ImageService(ImageServiceABC): metadata=metadata, ) - self._services.records.save( - image_name=image_name, - image_type=image_type, - image_category=image_category, - node_id=node_id, - session_id=session_id, - metadata=metadata, - created_at=timestamp, - ) - image_url = self._services.urls.get_image_url(image_type, image_name) thumbnail_url = self._services.urls.get_image_url( image_type, image_name, True ) return ImageDTO( + # Non-nullable fields image_name=image_name, image_type=image_type, image_category=image_category, + width=width, + height=height, + # Nullable fields node_id=node_id, session_id=session_id, metadata=metadata, - created_at=timestamp, + # Meta fields + created_at=created_at, + updated_at=created_at, # this is always the same as the created_at at this time + deleted_at=None, + # Extra non-nullable fields for DTO image_url=image_url, thumbnail_url=thumbnail_url, ) - except ImageRecordStorageBase.ImageRecordSaveException: + except ImageRecordSaveException: self._services.logger.error("Failed to save image record") raise - except ImageFileStorageBase.ImageFileSaveException: + except ImageFileSaveException: self._services.logger.error("Failed to save image file") raise except Exception as e: @@ -218,7 +229,7 @@ class ImageService(ImageServiceABC): def get_pil_image(self, image_type: ImageType, image_name: str) -> PILImageType: try: return self._services.files.get(image_type, image_name) - except ImageFileStorageBase.ImageFileNotFoundException: + except ImageFileNotFoundException: self._services.logger.error("Failed to get image file") raise except Exception as e: @@ -228,7 +239,7 @@ class ImageService(ImageServiceABC): def get_record(self, image_type: ImageType, image_name: str) -> ImageRecord: try: return self._services.records.get(image_type, image_name) - except ImageRecordStorageBase.ImageRecordNotFoundException: + except ImageRecordNotFoundException: self._services.logger.error("Image record not found") raise except Exception as e: @@ -246,7 +257,7 @@ class ImageService(ImageServiceABC): ) return image_dto - except ImageRecordStorageBase.ImageRecordNotFoundException: + except ImageRecordNotFoundException: self._services.logger.error("Image record not found") raise except Exception as e: @@ -311,32 +322,19 @@ class ImageService(ImageServiceABC): raise e def delete(self, image_type: ImageType, image_name: str): - # TODO: Consider using a transaction here to ensure consistency between storage and database try: self._services.files.delete(image_type, image_name) self._services.records.delete(image_type, image_name) - except ImageRecordStorageBase.ImageRecordDeleteException: + except ImageRecordDeleteException: self._services.logger.error(f"Failed to delete image record") raise - except ImageFileStorageBase.ImageFileDeleteException: + except ImageFileDeleteException: self._services.logger.error(f"Failed to delete image file") raise except Exception as e: self._services.logger.error("Problem deleting image record and file") raise e - def add_tag(self, image_type: ImageType, image_id: str, tag: str) -> None: - raise NotImplementedError("The 'add_tag' method is not implemented yet.") - - def remove_tag(self, image_type: ImageType, image_id: str, tag: str) -> None: - raise NotImplementedError("The 'remove_tag' method is not implemented yet.") - - def favorite(self, image_type: ImageType, image_id: str) -> None: - raise NotImplementedError("The 'favorite' method is not implemented yet.") - - def unfavorite(self, image_type: ImageType, image_id: str) -> None: - raise NotImplementedError("The 'unfavorite' method is not implemented yet.") - def _create_image_name( self, image_type: ImageType, diff --git a/invokeai/app/services/models/image_record.py b/invokeai/app/services/models/image_record.py index 29a2d71232..c1155ff73e 100644 --- a/invokeai/app/services/models/image_record.py +++ b/invokeai/app/services/models/image_record.py @@ -1,5 +1,4 @@ import datetime -import sqlite3 from typing import Optional, Union from pydantic import BaseModel, Field from invokeai.app.models.image import ImageCategory, ImageType @@ -10,30 +9,60 @@ from invokeai.app.util.misc import get_iso_timestamp class ImageRecord(BaseModel): """Deserialized image record.""" - image_name: str = Field(description="The name of the image.") + image_name: str = Field(description="The unique name of the image.") + """The unique name of the image.""" image_type: ImageType = Field(description="The type of the image.") + """The type of the image.""" image_category: ImageCategory = Field(description="The category of the image.") + """The category of the image.""" + width: int = Field(description="The width of the image in px.") + """The actual width of the image in px. This may be different from the width in metadata.""" + height: int = Field(description="The height of the image in px.") + """The actual height of the image in px. This may be different from the height in metadata.""" created_at: Union[datetime.datetime, str] = Field( description="The created timestamp of the image." ) - session_id: Optional[str] = Field(default=None, description="The session ID.") - node_id: Optional[str] = Field(default=None, description="The node ID.") - metadata: Optional[ImageMetadata] = Field( - default=None, description="The image's metadata." + """The created timestamp of the image.""" + updated_at: Union[datetime.datetime, str] = Field( + description="The updated timestamp of the image." ) + """The updated timestamp of the image.""" + deleted_at: Union[datetime.datetime, str, None] = Field( + description="The deleted timestamp of the image." + ) + """The deleted timestamp of the image.""" + session_id: Optional[str] = Field( + default=None, + 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.""" + node_id: Optional[str] = Field( + default=None, + description="The node ID that generated this image, if it is a generated image.", + ) + """The node ID that generated this image, if it is a generated image.""" + metadata: Optional[ImageMetadata] = Field( + default=None, + description="A limited subset of the image's generation metadata. Retrieve the image's session for full metadata.", + ) + """A limited subset of the image's generation metadata. Retrieve the image's session for full metadata.""" class ImageUrlsDTO(BaseModel): - """The URLs for an image and its thumbnaill""" + """The URLs for an image and its thumbnail.""" - image_name: str = Field(description="The name of the image.") + image_name: str = Field(description="The unique name of the image.") + """The unique name of the image.""" image_type: ImageType = Field(description="The type of the image.") + """The type of the image.""" image_url: str = Field(description="The URL of the image.") - thumbnail_url: str = Field(description="The thumbnail URL of the image.") + """The URL of the image.""" + thumbnail_url: str = Field(description="The URL of the image's thumbnail.") + """The URL of the image's thumbnail.""" class ImageDTO(ImageRecord, ImageUrlsDTO): - """Deserialized image record with URLs.""" + """Deserialized image record, enriched for the frontend with URLs.""" pass @@ -43,24 +72,29 @@ def image_record_to_dto( ) -> ImageDTO: """Converts an image record to an image DTO.""" return ImageDTO( - image_name=image_record.image_name, - image_type=image_record.image_type, - image_category=image_record.image_category, - created_at=image_record.created_at, - session_id=image_record.session_id, - node_id=image_record.node_id, - metadata=image_record.metadata, + **image_record.dict(), image_url=image_url, thumbnail_url=thumbnail_url, ) -def deserialize_image_record(image_row: sqlite3.Row) -> ImageRecord: +def deserialize_image_record(image_dict: dict) -> ImageRecord: """Deserializes an image record.""" - image_dict = dict(image_row) + # Retrieve all the values, setting "reasonable" defaults if they are not present. + image_name = image_dict.get("image_name", "unknown") image_type = ImageType(image_dict.get("image_type", ImageType.RESULT.value)) + image_category = ImageCategory( + image_dict.get("image_category", ImageCategory.GENERAL.value) + ) + width = image_dict.get("width", 0) + height = image_dict.get("height", 0) + session_id = image_dict.get("session_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()) + deleted_at = image_dict.get("deleted_at", get_iso_timestamp()) raw_metadata = image_dict.get("metadata") @@ -70,13 +104,15 @@ def deserialize_image_record(image_row: sqlite3.Row) -> ImageRecord: metadata = None return ImageRecord( - image_name=image_dict.get("id", "unknown"), - session_id=image_dict.get("session_id", None), - node_id=image_dict.get("node_id", None), - metadata=metadata, + image_name=image_name, image_type=image_type, - image_category=ImageCategory( - image_dict.get("image_category", ImageCategory.IMAGE.value) - ), - created_at=image_dict.get("created_at", get_iso_timestamp()), + image_category=image_category, + width=width, + height=height, + session_id=session_id, + node_id=node_id, + metadata=metadata, + created_at=created_at, + updated_at=updated_at, + deleted_at=deleted_at, ) diff --git a/invokeai/app/util/enum.py b/invokeai/app/util/metaenum.py similarity index 61% rename from invokeai/app/util/enum.py rename to invokeai/app/util/metaenum.py index 5bba5712c5..462238f775 100644 --- a/invokeai/app/util/enum.py +++ b/invokeai/app/util/metaenum.py @@ -2,7 +2,10 @@ from enum import EnumMeta class MetaEnum(EnumMeta): - """Metaclass to support `in` syntax value checking in String Enums""" + """Metaclass to support additional features in Enums. + + - `in` operator support: `'value' in MyEnum -> bool` + """ def __contains__(cls, item): try: From 4c331a5d7e65388b364b07f04fa409ca5f63a7fc Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 23 May 2023 22:11:10 +1000 Subject: [PATCH 37/72] chore(ui): regen api client --- .../src/services/api/models/ImageCategory.ts | 2 +- .../web/src/services/api/models/ImageDTO.ts | 28 +++++++++++++++---- .../src/services/api/models/ImageUrlsDTO.ts | 6 ++-- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/models/ImageCategory.ts b/invokeai/frontend/web/src/services/api/models/ImageCategory.ts index 38955a0de1..c4edf90fd3 100644 --- a/invokeai/frontend/web/src/services/api/models/ImageCategory.ts +++ b/invokeai/frontend/web/src/services/api/models/ImageCategory.ts @@ -5,4 +5,4 @@ /** * The category of an image. Use ImageCategory.OTHER for non-default categories. */ -export type ImageCategory = 'image' | 'control_image' | 'other'; +export type ImageCategory = 'general' | 'control' | 'other'; diff --git a/invokeai/frontend/web/src/services/api/models/ImageDTO.ts b/invokeai/frontend/web/src/services/api/models/ImageDTO.ts index 311a4a30a6..c5fad70d8a 100644 --- a/invokeai/frontend/web/src/services/api/models/ImageDTO.ts +++ b/invokeai/frontend/web/src/services/api/models/ImageDTO.ts @@ -7,11 +7,11 @@ import type { ImageMetadata } from './ImageMetadata'; import type { ImageType } from './ImageType'; /** - * Deserialized image record with URLs. + * Deserialized image record, enriched for the frontend with URLs. */ export type ImageDTO = { /** - * The name of the image. + * The unique name of the image. */ image_name: string; /** @@ -23,27 +23,43 @@ export type ImageDTO = { */ image_url: string; /** - * The thumbnail URL of the image. + * The URL of the image's thumbnail. */ thumbnail_url: string; /** * The category of the image. */ image_category: ImageCategory; + /** + * The width of the image in px. + */ + width: number; + /** + * The height of the image in px. + */ + height: number; /** * The created timestamp of the image. */ created_at: string; /** - * The session ID. + * The updated timestamp of the image. + */ + updated_at: string; + /** + * The deleted timestamp of the image. + */ + deleted_at?: string; + /** + * The session ID that generated this image, if it is a generated image. */ session_id?: string; /** - * The node ID. + * The node ID that generated this image, if it is a generated image. */ node_id?: string; /** - * The image's metadata. + * A limited subset of the image's metadata. Retrieve the image's session for full metadata. */ metadata?: ImageMetadata; }; diff --git a/invokeai/frontend/web/src/services/api/models/ImageUrlsDTO.ts b/invokeai/frontend/web/src/services/api/models/ImageUrlsDTO.ts index ea77c8af21..af80519ef2 100644 --- a/invokeai/frontend/web/src/services/api/models/ImageUrlsDTO.ts +++ b/invokeai/frontend/web/src/services/api/models/ImageUrlsDTO.ts @@ -5,11 +5,11 @@ import type { ImageType } from './ImageType'; /** - * The URLs for an image and its thumbnaill + * The URLs for an image and its thumbnail. */ export type ImageUrlsDTO = { /** - * The name of the image. + * The unique name of the image. */ image_name: string; /** @@ -21,7 +21,7 @@ export type ImageUrlsDTO = { */ image_url: string; /** - * The thumbnail URL of the image. + * The URL of the image's thumbnail. */ thumbnail_url: string; }; From 23d9d58c08974749593ca150b8fe0b5e037c9d27 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 23 May 2023 22:57:29 +1000 Subject: [PATCH 38/72] fix(nodes): fix bugs with serving images When returning a `FileResponse`, we must provide a valid path, else an exception is raised outside the route handler. Add the `validate_path` method back to the service so we can validate paths before returning the file. I don't like this but apparently this is just how `starlette` and `fastapi` works with `FileResponse`. --- invokeai/app/api/routers/images.py | 18 +++++++++++++++--- invokeai/app/api_app.py | 4 ++-- invokeai/app/services/image_file_storage.py | 16 +++++++++++++++- invokeai/app/services/images.py | 16 ++++++++++++++-- invokeai/app/services/urls.py | 10 +++++++--- 5 files changed, 53 insertions(+), 11 deletions(-) diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 123774b721..602b539da1 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -93,7 +93,7 @@ async def get_image_metadata( @images_router.get( - "/{image_type}/{image_name}/full", + "/{image_type}/{image_name}", operation_id="get_image_full", response_class=Response, responses={ @@ -117,7 +117,15 @@ async def get_image_full( image_type, image_name ) - return FileResponse(path, media_type="image/png") + if not ApiDependencies.invoker.services.images_new.validate_path(path): + raise HTTPException(status_code=404) + + return FileResponse( + path, + media_type="image/png", + filename=image_name, + content_disposition_type="inline", + ) except Exception as e: raise HTTPException(status_code=404) @@ -144,8 +152,12 @@ async def get_image_thumbnail( path = ApiDependencies.invoker.services.images_new.get_path( image_type, image_name, thumbnail=True ) + if not ApiDependencies.invoker.services.images_new.validate_path(path): + raise HTTPException(status_code=404) - return FileResponse(path, media_type="image/webp") + return FileResponse( + path, media_type="image/webp", content_disposition_type="inline" + ) except Exception as e: raise HTTPException(status_code=404) diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index 964202786a..69d322578d 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -3,8 +3,7 @@ import asyncio from inspect import signature import uvicorn -from invokeai.app.models import resources -import invokeai.backend.util.logging as logger +from invokeai.backend.util.logging import InvokeAILogger from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html @@ -20,6 +19,7 @@ from .api.sockets import SocketIO from .invocations.baseinvocation import BaseInvocation from .services.config import InvokeAIAppConfig +logger = InvokeAILogger.getLogger() # Create the app # TODO: create this all in a method so configuration/etc. can be passed in? diff --git a/invokeai/app/services/image_file_storage.py b/invokeai/app/services/image_file_storage.py index 1b4466e06e..46070b3bf2 100644 --- a/invokeai/app/services/image_file_storage.py +++ b/invokeai/app/services/image_file_storage.py @@ -44,7 +44,6 @@ class ImageFileStorageBase(ABC): """Retrieves an image as PIL Image.""" pass - # # TODO: make this a bit more flexible for e.g. cloud storage @abstractmethod def get_path( self, image_type: ImageType, image_name: str, thumbnail: bool = False @@ -52,6 +51,13 @@ class ImageFileStorageBase(ABC): """Gets the internal path to an image or thumbnail.""" pass + # TODO: We need to validate paths before starlette makes the FileResponse, else we get a + # 500 internal server error. I don't like having this method on the service. + @abstractmethod + def validate_path(self, path: str) -> bool: + """Validates the path given for an image or thumbnail.""" + pass + @abstractmethod def save( self, @@ -175,6 +181,14 @@ class DiskImageFileStorage(ImageFileStorageBase): return abspath + def validate_path(self, path: str) -> bool: + """Validates the path given for an image or thumbnail.""" + try: + os.stat(path) + return True + except: + return False + def __get_cache(self, image_name: str) -> PILImageType | None: return None if image_name not in self.__cache else self.__cache[image_name] diff --git a/invokeai/app/services/images.py b/invokeai/app/services/images.py index 2b2322085d..914dd3b6d3 100644 --- a/invokeai/app/services/images.py +++ b/invokeai/app/services/images.py @@ -70,14 +70,19 @@ class ImageServiceABC(ABC): @abstractmethod def get_path(self, image_type: ImageType, image_name: str) -> str: - """Gets an image's path""" + """Gets an image's path.""" + pass + + @abstractmethod + def validate_path(self, path: str) -> bool: + """Validates an image's path.""" pass @abstractmethod def get_url( self, image_type: ImageType, image_name: str, thumbnail: bool = False ) -> str: - """Gets an image's or thumbnail's URL""" + """Gets an image's or thumbnail's URL.""" pass @abstractmethod @@ -273,6 +278,13 @@ class ImageService(ImageServiceABC): self._services.logger.error("Problem getting image path") raise e + def validate_path(self, path: str) -> bool: + try: + return self._services.files.validate_path(path) + except Exception as e: + self._services.logger.error("Problem validating image path") + raise e + def get_url( self, image_type: ImageType, image_name: str, thumbnail: bool = False ) -> str: diff --git a/invokeai/app/services/urls.py b/invokeai/app/services/urls.py index cfc4b34012..2716da60ad 100644 --- a/invokeai/app/services/urls.py +++ b/invokeai/app/services/urls.py @@ -24,7 +24,11 @@ class LocalUrlService(UrlServiceBase): self, image_type: ImageType, image_name: str, thumbnail: bool = False ) -> str: image_basename = os.path.basename(image_name) - if thumbnail: - return f"{self._base_url}/images/{image_type.value}/{image_basename}/thumbnail" - return f"{self._base_url}/images/{image_type.value}/{image_basename}/full" + # These paths are determined by the routes in invokeai/app/api/routers/images.py + if thumbnail: + return ( + f"{self._base_url}/images/{image_type.value}/{image_basename}/thumbnail" + ) + + return f"{self._base_url}/images/{image_type.value}/{image_basename}" From aeaf3737aac2a8c1cb9e69999aac5d2c4e6ee4f2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 23 May 2023 22:58:32 +1000 Subject: [PATCH 39/72] fix(ui): fix gallery bugs --- invokeai/frontend/web/src/services/thunks/gallery.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/services/thunks/gallery.ts b/invokeai/frontend/web/src/services/thunks/gallery.ts index 694d44db3e..01e8a986b2 100644 --- a/invokeai/frontend/web/src/services/thunks/gallery.ts +++ b/invokeai/frontend/web/src/services/thunks/gallery.ts @@ -8,10 +8,16 @@ const galleryLog = log.child({ namespace: 'gallery' }); export const receivedResultImagesPage = createAppAsyncThunk( 'results/receivedResultImagesPage', - async (_arg, { getState }) => { + async (_arg, { getState, rejectWithValue }) => { + const { page, pages, nextPage } = getState().results; + + if (nextPage === page) { + rejectWithValue([]); + } + const response = await ImagesService.listImagesWithMetadata({ imageType: 'results', - imageCategory: 'image', + imageCategory: 'general', page: getState().results.nextPage, perPage: IMAGES_PER_PAGE, }); @@ -27,7 +33,7 @@ export const receivedUploadImagesPage = createAppAsyncThunk( async (_arg, { getState }) => { const response = await ImagesService.listImagesWithMetadata({ imageType: 'uploads', - imageCategory: 'image', + imageCategory: 'general', page: getState().uploads.nextPage, perPage: IMAGES_PER_PAGE, }); From c406be6f4fdb79d36d961c5eed81f9d05d1b192d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 23 May 2023 22:58:56 +1000 Subject: [PATCH 40/72] fix(ui): fix image deletion --- .../listeners/imageDeleted.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts index 00cbf86527..42a62b3d80 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts @@ -17,24 +17,24 @@ export const addRequestedImageDeletionListener = () => { return; } - const { name, type } = image; + const { image_name, image_type } = image; - if (type !== 'uploads' && type !== 'results') { - moduleLog.warn({ data: image }, `Invalid image type ${type}`); + if (image_type !== 'uploads' && image_type !== 'results') { + moduleLog.warn({ data: image }, `Invalid image type ${image_type}`); return; } - const selectedImageName = getState().gallery.selectedImage?.name; + const selectedImageName = getState().gallery.selectedImage?.image_name; - if (selectedImageName === name) { - const allIds = getState()[type].ids; - const allEntities = getState()[type].entities; + if (selectedImageName === image_name) { + const allIds = getState()[image_type].ids; + const allEntities = getState()[image_type].entities; const deletedImageIndex = allIds.findIndex( - (result) => result.toString() === name + (result) => result.toString() === image_name ); - const filteredIds = allIds.filter((id) => id.toString() !== name); + const filteredIds = allIds.filter((id) => id.toString() !== image_name); const newSelectedImageIndex = clamp( deletedImageIndex, @@ -53,7 +53,7 @@ export const addRequestedImageDeletionListener = () => { } } - dispatch(imageDeleted({ imageName: name, imageType: type })); + dispatch(imageDeleted({ imageName: image_name, imageType: image_type })); }, }); }; From 6f78c073ed3ed3f4468241a299b057ea3d8a735f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 23 May 2023 23:46:52 +1000 Subject: [PATCH 41/72] fix(ui): fix uploads & other bugs --- .../listeners/canvasMerged.ts | 3 +- .../listeners/imageUploaded.ts | 10 +++---- .../gallery/components/HoverableImage.tsx | 11 ++++--- .../ImageMetadataViewer.tsx | 4 +-- .../frontend/web/src/services/thunks/image.ts | 11 ++----- .../frontend/web/src/services/types/guards.ts | 14 +++++---- .../services/util/deserializeImageField.ts | 27 ----------------- .../services/util/deserializeImageResponse.ts | 29 ------------------- 8 files changed, 22 insertions(+), 87 deletions(-) delete mode 100644 invokeai/frontend/web/src/services/util/deserializeImageField.ts delete mode 100644 invokeai/frontend/web/src/services/util/deserializeImageResponse.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts index d7a58c2050..1e2d99541c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts @@ -5,7 +5,6 @@ import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; import { addToast } from 'features/system/store/systemSlice'; import { imageUploaded } from 'services/thunks/image'; import { v4 as uuidv4 } from 'uuid'; -import { deserializeImageResponse } from 'services/util/deserializeImageResponse'; import { setMergedCanvas } from 'features/canvas/store/canvasSlice'; import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider'; @@ -66,7 +65,7 @@ export const addCanvasMergedListener = () => { action.meta.arg.formData.file.name === filename ); - const mergedCanvasImage = deserializeImageResponse(payload.response); + const mergedCanvasImage = payload.response; dispatch( setMergedCanvas({ diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index de06220ecd..1d66166c12 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -1,4 +1,3 @@ -import { deserializeImageResponse } from 'services/util/deserializeImageResponse'; import { startAppListening } from '..'; import { uploadAdded } from 'features/gallery/store/uploadsSlice'; import { imageSelected } from 'features/gallery/store/gallerySlice'; @@ -7,6 +6,7 @@ import { addToast } from 'features/system/store/systemSlice'; import { initialImageSelected } from 'features/parameters/store/actions'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { resultAdded } from 'features/gallery/store/resultsSlice'; +import { isResultsImageDTO, isUploadsImageDTO } from 'services/types/guards'; export const addImageUploadedListener = () => { startAppListening({ @@ -14,13 +14,11 @@ export const addImageUploadedListener = () => { imageUploaded.fulfilled.match(action) && action.payload.response.image_type !== 'intermediates', effect: (action, { dispatch, getState }) => { - const { response } = action.payload; - const { imageType } = action.meta.arg; + const { response: image } = action.payload; const state = getState(); - const image = deserializeImageResponse(response); - if (imageType === 'uploads') { + if (isUploadsImageDTO(image)) { dispatch(uploadAdded(image)); dispatch(addToast({ title: 'Image Uploaded', status: 'success' })); @@ -38,7 +36,7 @@ export const addImageUploadedListener = () => { } } - if (imageType === 'results') { + if (isResultsImageDTO(image)) { dispatch(resultAdded(image)); } }, diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx index e1e0f0458c..04fecac463 100644 --- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx @@ -13,7 +13,6 @@ import { DragEvent, MouseEvent, memo, useCallback, useState } from 'react'; import { FaCheck, FaExpand, FaImage, FaShare, FaTrash } from 'react-icons/fa'; import DeleteImageModal from './DeleteImageModal'; import { ContextMenu } from 'chakra-ui-contextmenu'; -import * as InvokeAI from 'app/types/invokeai'; import { resizeAndScaleCanvas, setInitialCanvasImage, @@ -168,9 +167,9 @@ const HoverableImage = memo((props: HoverableImageProps) => { dispatch(initialImageSelected(image)); }, [dispatch, image]); - const handleRecallInitialImage = useCallback(() => { - recallInitialImage(image.metadata.invokeai?.node?.image); - }, [image, recallInitialImage]); + // const handleRecallInitialImage = useCallback(() => { + // recallInitialImage(image.metadata.invokeai?.node?.image); + // }, [image, recallInitialImage]); /** * TODO: the rest of these @@ -238,13 +237,13 @@ const HoverableImage = memo((props: HoverableImageProps) => { > {t('parameters.useSeed')} - } onClickCapture={handleRecallInitialImage} isDisabled={image?.metadata?.type !== 'img2img'} > {t('parameters.useInitImg')} - + */} } onClickCapture={handleUseAllParameters} 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 3ec820ade7..b87fdcb90e 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx @@ -164,9 +164,7 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { isExternal maxW="calc(100% - 3rem)" > - {image.image_url.length > 64 - ? image.image_url.substring(0, 64).concat('...') - : image.image_url} + {image.image_name} diff --git a/invokeai/frontend/web/src/services/thunks/image.ts b/invokeai/frontend/web/src/services/thunks/image.ts index bf6cc40e2e..6831eb647d 100644 --- a/invokeai/frontend/web/src/services/thunks/image.ts +++ b/invokeai/frontend/web/src/services/thunks/image.ts @@ -1,4 +1,3 @@ -import { AnyAction } from '@reduxjs/toolkit'; import { log } from 'app/logging/useLogger'; import { createAppAsyncThunk } from 'app/store/storeUtils'; import { InvokeTabName } from 'features/ui/store/tabMap'; @@ -56,10 +55,7 @@ export const imageUploaded = createAppAsyncThunk( const response = await ImagesService.uploadImage(rest); const { location } = getHeaders(response); - imagesLog.info( - { arg: '', response, location }, - `Image uploaded (${response.image_name})` - ); + imagesLog.debug({ arg: '', response, location }, 'Image uploaded'); return { response, location }; } @@ -75,10 +71,7 @@ export const imageDeleted = createAppAsyncThunk( async (arg: ImageDeletedArg) => { const response = await ImagesService.deleteImage(arg); - imagesLog.info( - { arg, response }, - `Image deleted (${arg.imageType} - ${arg.imageName})` - ); + imagesLog.debug({ arg, response }, 'Image deleted'); return response; } diff --git a/invokeai/frontend/web/src/services/types/guards.ts b/invokeai/frontend/web/src/services/types/guards.ts index f7eba6e9d6..266e991f4d 100644 --- a/invokeai/frontend/web/src/services/types/guards.ts +++ b/invokeai/frontend/web/src/services/types/guards.ts @@ -1,3 +1,5 @@ +import { ResultsImageDTO } from 'features/gallery/store/resultsSlice'; +import { UploadsImageDTO } from 'features/gallery/store/uploadsSlice'; import { get, isObject, isString } from 'lodash-es'; import { GraphExecutionState, @@ -10,8 +12,15 @@ import { ImageType, ImageField, LatentsOutput, + ImageDTO, } from 'services/api'; +export const isUploadsImageDTO = (image: ImageDTO): image is UploadsImageDTO => + image.image_type === 'uploads'; + +export const isResultsImageDTO = (image: ImageDTO): image is ResultsImageDTO => + image.image_type === 'results'; + export const isImageOutput = ( output: GraphExecutionState['results'][string] ): output is ImageOutput => output.type === 'image_output'; @@ -43,11 +52,6 @@ export const isCollectOutput = ( export const isImageType = (t: unknown): t is ImageType => isString(t) && ['results', 'uploads', 'intermediates'].includes(t); -export const isImage = (image: unknown): image is Image => - isObject(image) && - isString(get(image, 'name')) && - isImageType(get(image, 'type')); - export const isImageField = (imageField: unknown): imageField is ImageField => isObject(imageField) && isString(get(imageField, 'image_name')) && diff --git a/invokeai/frontend/web/src/services/util/deserializeImageField.ts b/invokeai/frontend/web/src/services/util/deserializeImageField.ts deleted file mode 100644 index 74d63117a4..0000000000 --- a/invokeai/frontend/web/src/services/util/deserializeImageField.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ImageType } from 'services/api'; - -export const buildImageUrls = ( - imageType: ImageType, - imageName: string -): { url: string; thumbnail: string } => { - const url = `api/v1/images/${imageType}/${imageName}`; - - const thumbnail = `api/v1/images/${imageType}/thumbnails/${ - imageName.split('.')[0] - }.webp`; - - return { - url, - thumbnail, - }; -}; - -export const extractTimestampFromImageName = (imageName: string) => { - const timestamp = imageName.split('_')?.pop()?.split('.')[0]; - - if (timestamp === undefined) { - return 0; - } - - return Number(timestamp); -}; diff --git a/invokeai/frontend/web/src/services/util/deserializeImageResponse.ts b/invokeai/frontend/web/src/services/util/deserializeImageResponse.ts deleted file mode 100644 index 8d2a6df49e..0000000000 --- a/invokeai/frontend/web/src/services/util/deserializeImageResponse.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Image } from 'app/types/invokeai'; -import { parseInvokeAIMetadata } from 'common/util/parseMetadata'; -import { ImageResponse } from 'services/api'; - -/** - * Process ImageReponse objects, which we get from the `list_images` endpoint. - */ -export const deserializeImageResponse = ( - imageResponse: ImageResponse -): Image => { - const { image_name, image_type, image_url, metadata, thumbnail_url } = - imageResponse; - - // TODO: parse metadata - just leaving it as-is for now - const { invokeai, ...rest } = metadata; - - const parsedMetadata = parseInvokeAIMetadata(invokeai); - - return { - name: image_name, - type: image_type, - url: image_url, - thumbnail: thumbnail_url, - metadata: { - ...rest, - ...(invokeai ? { invokeai: parsedMetadata } : {}), - }, - }; -}; From 55b31936295d3584eb7b94450072d0a7626edba5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 24 May 2023 13:59:36 +1000 Subject: [PATCH 42/72] fix(nodes): add RangeInvocation validator `stop` must be greater than `start`. --- invokeai/app/invocations/collections.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/invokeai/app/invocations/collections.py b/invokeai/app/invocations/collections.py index 93130bfaad..0884958fb5 100644 --- a/invokeai/app/invocations/collections.py +++ b/invokeai/app/invocations/collections.py @@ -3,7 +3,7 @@ from typing import Literal, Optional import numpy as np -from pydantic import Field +from pydantic import Field, validator from invokeai.app.util.misc import SEED_MAX, get_random_seed @@ -33,6 +33,12 @@ class RangeInvocation(BaseInvocation): stop: int = Field(default=10, description="The stop of the range") step: int = Field(default=1, description="The step of the range") + @validator("stop") + def stop_gt_start(cls, v, values): + if "start" in values and v <= values["start"]: + raise ValueError("stop must be greater than start") + return v + def invoke(self, context: InvocationContext) -> IntCollectionOutput: return IntCollectionOutput( collection=list(range(self.start, self.stop, self.step)) From 8f393b64b890f8321f5b946af1114af584b069c6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 24 May 2023 15:00:32 +1000 Subject: [PATCH 43/72] feat(nodes): add seed validator If `seed>SEED_MAX`, we can still continue if we parse the seed as `seed % SEED_MAX`. --- invokeai/app/invocations/latent.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index adba88274f..40ba67861a 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -140,6 +140,11 @@ class NoiseInvocation(BaseInvocation): }, } + @validator("seed", pre=True) + def modulo_seed(cls, v): + """Returns the seed modulo SEED_MAX to ensure it is within the valid range.""" + return v % SEED_MAX + def invoke(self, context: InvocationContext) -> NoiseOutput: device = torch.device(choose_torch_device()) noise = get_noise(self.width, self.height, device, self.seed) From b25c1af018e1060b84a7246a80a27821ec57d12d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 24 May 2023 15:04:04 +1000 Subject: [PATCH 44/72] feat(nodes): add RangeOfSizeInvocation The `RangeInvocation` is a simple wrapper around `range()`, but you must provide `stop > start`. `RangeOfSizeInvocation` replaces the `stop` parameter with `size`, so that you can just provide the `start` and `step` and get a range of `size` length. --- invokeai/app/invocations/collections.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/invokeai/app/invocations/collections.py b/invokeai/app/invocations/collections.py index 0884958fb5..d97947e160 100644 --- a/invokeai/app/invocations/collections.py +++ b/invokeai/app/invocations/collections.py @@ -1,6 +1,6 @@ -# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654) +# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI Team -from typing import Literal, Optional +from typing import Literal import numpy as np from pydantic import Field, validator @@ -24,7 +24,7 @@ class IntCollectionOutput(BaseInvocationOutput): class RangeInvocation(BaseInvocation): - """Creates a range""" + """Creates a range of numbers from start to stop with step""" type: Literal["range"] = "range" @@ -45,6 +45,22 @@ class RangeInvocation(BaseInvocation): ) +class RangeOfSizeInvocation(BaseInvocation): + """Creates a range from start to start + size with step""" + + type: Literal["range_of_size"] = "range_of_size" + + # Inputs + start: int = Field(default=0, description="The start of the range") + size: int = Field(default=1, description="The number of values") + step: int = Field(default=1, description="The step of the range") + + def invoke(self, context: InvocationContext) -> IntCollectionOutput: + return IntCollectionOutput( + collection=list(range(self.start, self.start + self.size + 1, self.step)) + ) + + class RandomRangeInvocation(BaseInvocation): """Creates a collection of random numbers""" From dd16f788edf680c3c549f8ebffcd35ed39fea252 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 24 May 2023 15:05:46 +1000 Subject: [PATCH 45/72] fix(nodes): fix RangeOfSizeInvocation off-by-one error --- invokeai/app/invocations/collections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/invocations/collections.py b/invokeai/app/invocations/collections.py index d97947e160..475b6028a9 100644 --- a/invokeai/app/invocations/collections.py +++ b/invokeai/app/invocations/collections.py @@ -57,7 +57,7 @@ class RangeOfSizeInvocation(BaseInvocation): def invoke(self, context: InvocationContext) -> IntCollectionOutput: return IntCollectionOutput( - collection=list(range(self.start, self.start + self.size + 1, self.step)) + collection=list(range(self.start, self.start + self.size, self.step)) ) From d2c223de8f19f56ac1f4ea0b6c66eba2cef06152 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 24 May 2023 15:50:55 +1000 Subject: [PATCH 46/72] feat(nodes): move fully* to new images service * except i haven't rebuilt inpaint in latents --- invokeai/app/api/dependencies.py | 22 +- invokeai/app/api/routers/images.py | 20 +- invokeai/app/invocations/cv.py | 42 ++-- invokeai/app/invocations/image.py | 226 ++++++++++--------- invokeai/app/invocations/infill.py | 105 +++++---- invokeai/app/invocations/latent.py | 32 ++- invokeai/app/invocations/reconstruct.py | 39 ++-- invokeai/app/invocations/upscale.py | 37 +-- invokeai/app/services/invocation_services.py | 8 +- 9 files changed, 273 insertions(+), 258 deletions(-) diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index ae351d4476..99e0f7238f 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -55,16 +55,6 @@ class ApiDependencies: os.path.join(os.path.dirname(__file__), "../../../../outputs") ) - latents = ForwardCacheLatentsStorage( - DiskLatentsStorage(f"{output_folder}/latents") - ) - - metadata = CoreMetadataService() - - urls = LocalUrlService() - - image_file_storage = DiskImageFileStorage(f"{output_folder}/images") - # TODO: build a file/path manager? db_location = os.path.join(output_folder, "invokeai.db") @@ -72,9 +62,16 @@ class ApiDependencies: filename=db_location, table_name="graph_executions" ) + urls = LocalUrlService() + metadata = CoreMetadataService() image_record_storage = SqliteImageRecordStorage(db_location) + image_file_storage = DiskImageFileStorage(f"{output_folder}/images") - images_new = ImageService( + latents = ForwardCacheLatentsStorage( + DiskLatentsStorage(f"{output_folder}/latents") + ) + + images = ImageService( image_record_storage=image_record_storage, image_file_storage=image_file_storage, metadata=metadata, @@ -87,8 +84,7 @@ class ApiDependencies: model_manager=get_model_manager(config, logger), events=events, latents=latents, - images=image_file_storage, - images_new=images_new, + images=images, queue=MemoryInvocationQueue(), graph_library=SqliteItemStorage[LibraryGraph]( filename=db_location, table_name="graphs" diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 602b539da1..0615ff187e 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -45,7 +45,7 @@ async def upload_image( raise HTTPException(status_code=415, detail="Failed to read image") try: - image_dto = ApiDependencies.invoker.services.images_new.create( + image_dto = ApiDependencies.invoker.services.images.create( pil_image, image_type, image_category, @@ -67,7 +67,7 @@ async def delete_image( """Deletes an image""" try: - ApiDependencies.invoker.services.images_new.delete(image_type, image_name) + ApiDependencies.invoker.services.images.delete(image_type, image_name) except Exception as e: # TODO: Does this need any exception handling at all? pass @@ -85,7 +85,7 @@ async def get_image_metadata( """Gets an image's metadata""" try: - return ApiDependencies.invoker.services.images_new.get_dto( + return ApiDependencies.invoker.services.images.get_dto( image_type, image_name ) except Exception as e: @@ -113,11 +113,11 @@ async def get_image_full( """Gets a full-resolution image file""" try: - path = ApiDependencies.invoker.services.images_new.get_path( + path = ApiDependencies.invoker.services.images.get_path( image_type, image_name ) - if not ApiDependencies.invoker.services.images_new.validate_path(path): + if not ApiDependencies.invoker.services.images.validate_path(path): raise HTTPException(status_code=404) return FileResponse( @@ -149,10 +149,10 @@ async def get_image_thumbnail( """Gets a thumbnail image file""" try: - path = ApiDependencies.invoker.services.images_new.get_path( + path = ApiDependencies.invoker.services.images.get_path( image_type, image_name, thumbnail=True ) - if not ApiDependencies.invoker.services.images_new.validate_path(path): + if not ApiDependencies.invoker.services.images.validate_path(path): raise HTTPException(status_code=404) return FileResponse( @@ -174,10 +174,10 @@ async def get_image_urls( """Gets an image and thumbnail URL""" try: - image_url = ApiDependencies.invoker.services.images_new.get_url( + image_url = ApiDependencies.invoker.services.images.get_url( image_type, image_name ) - thumbnail_url = ApiDependencies.invoker.services.images_new.get_url( + thumbnail_url = ApiDependencies.invoker.services.images.get_url( image_type, image_name, thumbnail=True ) return ImageUrlsDTO( @@ -205,7 +205,7 @@ async def list_images_with_metadata( ) -> PaginatedResults[ImageDTO]: """Gets a list of images with metadata""" - image_dtos = ApiDependencies.invoker.services.images_new.get_many( + image_dtos = ApiDependencies.invoker.services.images.get_many( image_type, image_category, page, diff --git a/invokeai/app/invocations/cv.py b/invokeai/app/invocations/cv.py index 5a6d703d83..26e06a2af8 100644 --- a/invokeai/app/invocations/cv.py +++ b/invokeai/app/invocations/cv.py @@ -7,9 +7,9 @@ import numpy from PIL import Image, ImageOps from pydantic import BaseModel, Field -from invokeai.app.models.image import ImageField, ImageType +from invokeai.app.models.image import ImageCategory, ImageField, ImageType from .baseinvocation import BaseInvocation, InvocationContext, InvocationConfig -from .image import ImageOutput, build_image_output +from .image import ImageOutput class CvInvocationConfig(BaseModel): @@ -26,24 +26,27 @@ class CvInvocationConfig(BaseModel): class CvInpaintInvocation(BaseInvocation, CvInvocationConfig): """Simple inpaint using opencv.""" - #fmt: off + + # fmt: off type: Literal["cv_inpaint"] = "cv_inpaint" # Inputs image: ImageField = Field(default=None, description="The image to inpaint") mask: ImageField = Field(default=None, description="The mask to use when inpainting") - #fmt: on + # fmt: on def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get( + image = context.services.images.get_pil_image( self.image.image_type, self.image.image_name ) - mask = context.services.images.get(self.mask.image_type, self.mask.image_name) + mask = context.services.images.get_pil_image( + self.mask.image_type, self.mask.image_name + ) # Convert to cv image/mask # TODO: consider making these utility functions cv_image = cv.cvtColor(numpy.array(image.convert("RGB")), cv.COLOR_RGB2BGR) - cv_mask = numpy.array(ImageOps.invert(mask)) + cv_mask = numpy.array(ImageOps.invert(mask.convert("L"))) # Inpaint cv_inpainted = cv.inpaint(cv_image, cv_mask, 3, cv.INPAINT_TELEA) @@ -52,18 +55,19 @@ class CvInpaintInvocation(BaseInvocation, CvInvocationConfig): # TODO: consider making a utility function image_inpainted = Image.fromarray(cv.cvtColor(cv_inpainted, cv.COLOR_BGR2RGB)) - image_type = ImageType.INTERMEDIATE - image_name = context.services.images.create_name( - context.graph_execution_state_id, self.id + image_dto = context.services.images.create( + image=image_inpainted, + image_type=ImageType.INTERMEDIATE, + image_category=ImageCategory.GENERAL, + node_id=self.id, + session_id=context.graph_execution_state_id, ) - metadata = context.services.metadata.build_metadata( - session_id=context.graph_execution_state_id, node=self + return ImageOutput( + image=ImageField( + image_name=image_dto.image_name, + image_type=image_dto.image_type, + ), + width=image_dto.width, + height=image_dto.height, ) - - context.services.images.save(image_type, image_name, image_inpainted, metadata) - return build_image_output( - image_type=image_type, - image_name=image_name, - image=image_inpainted, - ) \ No newline at end of file diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 56141cbb0e..8f789853ac 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -1,13 +1,13 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) import io -from typing import Literal, Optional +from typing import Literal, Optional, Union import numpy from PIL import Image, ImageFilter, ImageOps from pydantic import BaseModel, Field -from ..models.image import ImageField, ImageType +from ..models.image import ImageCategory, ImageField, ImageType from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, @@ -41,27 +41,14 @@ class ImageOutput(BaseInvocationOutput): schema_extra = {"required": ["type", "image", "width", "height"]} -def build_image_output( - image_type: ImageType, image_name: str, image: Image.Image -) -> ImageOutput: - """Builds an ImageOutput and its ImageField""" - image_field = ImageField( - image_name=image_name, - image_type=image_type, - ) - return ImageOutput( - image=image_field, - width=image.width, - height=image.height, - ) - - class MaskOutput(BaseInvocationOutput): """Base class for invocations that output a mask""" # fmt: off type: Literal["mask"] = "mask" mask: ImageField = Field(default=None, description="The output mask") + width: int = Field(description="The width of the mask in pixels") + height: int = Field(description="The height of the mask in pixels") # fmt: on class Config: @@ -84,12 +71,15 @@ class LoadImageInvocation(BaseInvocation): image_name: str = Field(description="The name of the image") # fmt: on def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get(self.image_type, self.image_name) + image = context.services.images.get_pil_image(self.image_type, self.image_name) - return build_image_output( - image_type=self.image_type, - image_name=self.image_name, - image=image, + return ImageOutput( + image=ImageField( + image_name=self.image_name, + image_type=self.image_type, + ), + width=image.width, + height=image.height, ) @@ -99,10 +89,12 @@ class ShowImageInvocation(BaseInvocation): type: Literal["show_image"] = "show_image" # Inputs - image: ImageField = Field(default=None, description="The image to show") + image: Union[ImageField, None] = Field( + default=None, description="The image to show" + ) def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get( + image = context.services.images.get_pil_image( self.image.image_type, self.image.image_name ) if image: @@ -110,10 +102,13 @@ class ShowImageInvocation(BaseInvocation): # TODO: how to handle failure? - return build_image_output( - image_type=self.image.image_type, - image_name=self.image.image_name, - image=image, + return ImageOutput( + image=ImageField( + image_name=self.image.image_name, + image_type=self.image.image_type, + ), + width=image.width, + height=image.height, ) @@ -124,7 +119,7 @@ class CropImageInvocation(BaseInvocation, PILInvocationConfig): type: Literal["crop"] = "crop" # Inputs - image: ImageField = Field(default=None, description="The image to crop") + image: Union[ImageField, None] = Field(default=None, description="The image to crop") x: int = Field(default=0, description="The left x coordinate of the crop rectangle") y: int = Field(default=0, description="The top y coordinate of the crop rectangle") width: int = Field(default=512, gt=0, description="The width of the crop rectangle") @@ -132,7 +127,7 @@ class CropImageInvocation(BaseInvocation, PILInvocationConfig): # fmt: on def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get( + image = context.services.images.get_pil_image( self.image.image_type, self.image.image_name ) @@ -141,20 +136,21 @@ class CropImageInvocation(BaseInvocation, PILInvocationConfig): ) image_crop.paste(image, (-self.x, -self.y)) - image_type = ImageType.INTERMEDIATE - image_name = context.services.images.create_name( - context.graph_execution_state_id, self.id - ) - - metadata = context.services.metadata.build_metadata( - session_id=context.graph_execution_state_id, node=self - ) - - context.services.images.save(image_type, image_name, image_crop, metadata) - return build_image_output( - image_type=image_type, - image_name=image_name, + image_dto = context.services.images.create( image=image_crop, + image_type=ImageType.INTERMEDIATE, + image_category=ImageCategory.GENERAL, + node_id=self.id, + session_id=context.graph_execution_state_id, + ) + + return ImageOutput( + image=ImageField( + image_name=image_dto.image_name, + image_type=image_dto.image_type, + ), + width=image_dto.width, + height=image_dto.height, ) @@ -165,25 +161,27 @@ class PasteImageInvocation(BaseInvocation, PILInvocationConfig): type: Literal["paste"] = "paste" # Inputs - base_image: ImageField = Field(default=None, description="The base image") - image: ImageField = Field(default=None, description="The image to paste") + base_image: Union[ImageField, None] = Field(default=None, description="The base image") + image: Union[ImageField, None] = Field(default=None, description="The image to paste") mask: Optional[ImageField] = Field(default=None, description="The mask to use when pasting") x: int = Field(default=0, description="The left x coordinate at which to paste the image") y: int = Field(default=0, description="The top y coordinate at which to paste the image") # fmt: on def invoke(self, context: InvocationContext) -> ImageOutput: - base_image = context.services.images.get( + base_image = context.services.images.get_pil_image( self.base_image.image_type, self.base_image.image_name ) - image = context.services.images.get( + image = context.services.images.get_pil_image( self.image.image_type, self.image.image_name ) mask = ( None if self.mask is None else ImageOps.invert( - context.services.images.get(self.mask.image_type, self.mask.image_name) + context.services.images.get_pil_image( + self.mask.image_type, self.mask.image_name + ) ) ) # TODO: probably shouldn't invert mask here... should user be required to do it? @@ -199,20 +197,21 @@ class PasteImageInvocation(BaseInvocation, PILInvocationConfig): new_image.paste(base_image, (abs(min_x), abs(min_y))) new_image.paste(image, (max(0, self.x), max(0, self.y)), mask=mask) - image_type = ImageType.RESULT - image_name = context.services.images.create_name( - context.graph_execution_state_id, self.id - ) - - metadata = context.services.metadata.build_metadata( - session_id=context.graph_execution_state_id, node=self - ) - - context.services.images.save(image_type, image_name, new_image, metadata) - return build_image_output( - image_type=image_type, - image_name=image_name, + image_dto = context.services.images.create( image=new_image, + image_type=ImageType.RESULT, + image_category=ImageCategory.GENERAL, + node_id=self.id, + session_id=context.graph_execution_state_id, + ) + + return ImageOutput( + image=ImageField( + image_name=image_dto.image_name, + image_type=image_dto.image_type, + ), + width=image_dto.width, + height=image_dto.height, ) @@ -223,12 +222,12 @@ class MaskFromAlphaInvocation(BaseInvocation, PILInvocationConfig): type: Literal["tomask"] = "tomask" # Inputs - image: ImageField = Field(default=None, description="The image to create the mask from") + image: Union[ImageField, None] = Field(default=None, description="The image to create the mask from") invert: bool = Field(default=False, description="Whether or not to invert the mask") # fmt: on def invoke(self, context: InvocationContext) -> MaskOutput: - image = context.services.images.get( + image = context.services.images.get_pil_image( self.image.image_type, self.image.image_name ) @@ -236,18 +235,22 @@ class MaskFromAlphaInvocation(BaseInvocation, PILInvocationConfig): if self.invert: image_mask = ImageOps.invert(image_mask) - image_type = ImageType.INTERMEDIATE - image_name = context.services.images.create_name( - context.graph_execution_state_id, self.id + image_dto = context.services.images.create( + image=image_mask, + image_type=ImageType.INTERMEDIATE, + image_category=ImageCategory.GENERAL, + node_id=self.id, + session_id=context.graph_execution_state_id, ) - metadata = context.services.metadata.build_metadata( - session_id=context.graph_execution_state_id, node=self + return MaskOutput( + mask=ImageField( + image_type=image_dto.image_type, image_name=image_dto.image_name + ), + width=image_dto.width, + height=image_dto.height, ) - context.services.images.save(image_type, image_name, image_mask, metadata) - return MaskOutput(mask=ImageField(image_type=image_type, image_name=image_name)) - class BlurInvocation(BaseInvocation, PILInvocationConfig): """Blurs an image""" @@ -256,13 +259,13 @@ class BlurInvocation(BaseInvocation, PILInvocationConfig): type: Literal["blur"] = "blur" # Inputs - image: ImageField = Field(default=None, description="The image to blur") + image: Union[ImageField, None] = Field(default=None, description="The image to blur") radius: float = Field(default=8.0, ge=0, description="The blur radius") blur_type: Literal["gaussian", "box"] = Field(default="gaussian", description="The type of blur") # fmt: on def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get( + image = context.services.images.get_pil_image( self.image.image_type, self.image.image_name ) @@ -273,18 +276,21 @@ class BlurInvocation(BaseInvocation, PILInvocationConfig): ) blur_image = image.filter(blur) - image_type = ImageType.INTERMEDIATE - image_name = context.services.images.create_name( - context.graph_execution_state_id, self.id + image_dto = context.services.images.create( + image=blur_image, + image_type=ImageType.INTERMEDIATE, + image_category=ImageCategory.GENERAL, + node_id=self.id, + session_id=context.graph_execution_state_id, ) - metadata = context.services.metadata.build_metadata( - session_id=context.graph_execution_state_id, node=self - ) - - context.services.images.save(image_type, image_name, blur_image, metadata) - return build_image_output( - image_type=image_type, image_name=image_name, image=blur_image + return ImageOutput( + image=ImageField( + image_name=image_dto.image_name, + image_type=image_dto.image_type, + ), + width=image_dto.width, + height=image_dto.height, ) @@ -295,13 +301,13 @@ class LerpInvocation(BaseInvocation, PILInvocationConfig): type: Literal["lerp"] = "lerp" # Inputs - image: ImageField = Field(default=None, description="The image to lerp") + image: Union[ImageField, None] = Field(default=None, description="The image to lerp") min: int = Field(default=0, ge=0, le=255, description="The minimum output value") max: int = Field(default=255, ge=0, le=255, description="The maximum output value") # fmt: on def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get( + image = context.services.images.get_pil_image( self.image.image_type, self.image.image_name ) @@ -310,18 +316,21 @@ class LerpInvocation(BaseInvocation, PILInvocationConfig): lerp_image = Image.fromarray(numpy.uint8(image_arr)) - image_type = ImageType.INTERMEDIATE - image_name = context.services.images.create_name( - context.graph_execution_state_id, self.id + image_dto = context.services.images.create( + image=lerp_image, + image_type=ImageType.INTERMEDIATE, + image_category=ImageCategory.GENERAL, + node_id=self.id, + session_id=context.graph_execution_state_id, ) - metadata = context.services.metadata.build_metadata( - session_id=context.graph_execution_state_id, node=self - ) - - context.services.images.save(image_type, image_name, lerp_image, metadata) - return build_image_output( - image_type=image_type, image_name=image_name, image=lerp_image + return ImageOutput( + image=ImageField( + image_name=image_dto.image_name, + image_type=image_dto.image_type, + ), + width=image_dto.width, + height=image_dto.height, ) @@ -332,13 +341,13 @@ class InverseLerpInvocation(BaseInvocation, PILInvocationConfig): type: Literal["ilerp"] = "ilerp" # Inputs - image: ImageField = Field(default=None, description="The image to lerp") + image: Union[ImageField, None] = Field(default=None, description="The image to lerp") min: int = Field(default=0, ge=0, le=255, description="The minimum input value") max: int = Field(default=255, ge=0, le=255, description="The maximum input value") # fmt: on def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get( + image = context.services.images.get_pil_image( self.image.image_type, self.image.image_name ) @@ -352,16 +361,19 @@ class InverseLerpInvocation(BaseInvocation, PILInvocationConfig): ilerp_image = Image.fromarray(numpy.uint8(image_arr)) - image_type = ImageType.INTERMEDIATE - image_name = context.services.images.create_name( - context.graph_execution_state_id, self.id + image_dto = context.services.images.create( + image=ilerp_image, + image_type=ImageType.INTERMEDIATE, + image_category=ImageCategory.GENERAL, + node_id=self.id, + session_id=context.graph_execution_state_id, ) - metadata = context.services.metadata.build_metadata( - session_id=context.graph_execution_state_id, node=self - ) - - context.services.images.save(image_type, image_name, ilerp_image, metadata) - return build_image_output( - image_type=image_type, image_name=image_name, image=ilerp_image + return ImageOutput( + image=ImageField( + image_name=image_dto.image_name, + image_type=image_dto.image_type, + ), + width=image_dto.width, + height=image_dto.height, ) diff --git a/invokeai/app/invocations/infill.py b/invokeai/app/invocations/infill.py index ac055cef5b..17a43dbdac 100644 --- a/invokeai/app/invocations/infill.py +++ b/invokeai/app/invocations/infill.py @@ -1,17 +1,17 @@ -# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) +# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI Team -from typing import Literal, Optional, Union, get_args +from typing import Literal, Union, get_args import numpy as np import math from PIL import Image, ImageOps from pydantic import Field -from invokeai.app.invocations.image import ImageOutput, build_image_output +from invokeai.app.invocations.image import ImageOutput from invokeai.app.util.misc import SEED_MAX, get_random_seed from invokeai.backend.image_util.patchmatch import PatchMatch -from ..models.image import ColorField, ImageField, ImageType +from ..models.image import ColorField, ImageCategory, ImageField, ImageType from .baseinvocation import ( BaseInvocation, InvocationContext, @@ -125,36 +125,39 @@ class InfillColorInvocation(BaseInvocation): """Infills transparent areas of an image with a solid color""" type: Literal["infill_rgba"] = "infill_rgba" - image: Optional[ImageField] = Field(default=None, description="The image to infill") - color: Optional[ColorField] = Field( + image: Union[ImageField, None] = Field( + default=None, description="The image to infill" + ) + color: ColorField = Field( default=ColorField(r=127, g=127, b=127, a=255), description="The color to use to infill", ) def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get( + image = context.services.images.get_pil_image( self.image.image_type, self.image.image_name ) solid_bg = Image.new("RGBA", image.size, self.color.tuple()) - infilled = Image.alpha_composite(solid_bg, image) + infilled = Image.alpha_composite(solid_bg, image.convert("RGBA")) infilled.paste(image, (0, 0), image.split()[-1]) - image_type = ImageType.RESULT - image_name = context.services.images.create_name( - context.graph_execution_state_id, self.id + image_dto = context.services.images.create( + image=infilled, + image_type=ImageType.RESULT, + image_category=ImageCategory.GENERAL, + node_id=self.id, + session_id=context.graph_execution_state_id, ) - metadata = context.services.metadata.build_metadata( - session_id=context.graph_execution_state_id, node=self - ) - - context.services.images.save(image_type, image_name, infilled, metadata) - return build_image_output( - image_type=image_type, - image_name=image_name, - image=image, + return ImageOutput( + image=ImageField( + image_name=image_dto.image_name, + image_type=image_dto.image_type, + ), + width=image_dto.width, + height=image_dto.height, ) @@ -163,7 +166,9 @@ class InfillTileInvocation(BaseInvocation): type: Literal["infill_tile"] = "infill_tile" - image: Optional[ImageField] = Field(default=None, description="The image to infill") + image: Union[ImageField, None] = Field( + default=None, description="The image to infill" + ) tile_size: int = Field(default=32, ge=1, description="The tile size (px)") seed: int = Field( ge=0, @@ -173,7 +178,7 @@ class InfillTileInvocation(BaseInvocation): ) def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get( + image = context.services.images.get_pil_image( self.image.image_type, self.image.image_name ) @@ -182,20 +187,21 @@ class InfillTileInvocation(BaseInvocation): ) infilled.paste(image, (0, 0), image.split()[-1]) - image_type = ImageType.RESULT - image_name = context.services.images.create_name( - context.graph_execution_state_id, self.id + image_dto = context.services.images.create( + image=infilled, + image_type=ImageType.RESULT, + image_category=ImageCategory.GENERAL, + node_id=self.id, + session_id=context.graph_execution_state_id, ) - metadata = context.services.metadata.build_metadata( - session_id=context.graph_execution_state_id, node=self - ) - - context.services.images.save(image_type, image_name, infilled, metadata) - return build_image_output( - image_type=image_type, - image_name=image_name, - image=image, + return ImageOutput( + image=ImageField( + image_name=image_dto.image_name, + image_type=image_dto.image_type, + ), + width=image_dto.width, + height=image_dto.height, ) @@ -204,10 +210,12 @@ class InfillPatchMatchInvocation(BaseInvocation): type: Literal["infill_patchmatch"] = "infill_patchmatch" - image: Optional[ImageField] = Field(default=None, description="The image to infill") + image: Union[ImageField, None] = Field( + default=None, description="The image to infill" + ) def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get( + image = context.services.images.get_pil_image( self.image.image_type, self.image.image_name ) @@ -216,18 +224,19 @@ class InfillPatchMatchInvocation(BaseInvocation): else: raise ValueError("PatchMatch is not available on this system") - image_type = ImageType.RESULT - image_name = context.services.images.create_name( - context.graph_execution_state_id, self.id + image_dto = context.services.images.create( + image=infilled, + image_type=ImageType.RESULT, + image_category=ImageCategory.GENERAL, + node_id=self.id, + session_id=context.graph_execution_state_id, ) - metadata = context.services.metadata.build_metadata( - session_id=context.graph_execution_state_id, node=self - ) - - context.services.images.save(image_type, image_name, infilled, metadata) - return build_image_output( - image_type=image_type, - image_name=image_name, - image=image, + return ImageOutput( + image=ImageField( + image_name=image_dto.image_name, + image_type=image_dto.image_type, + ), + width=image_dto.width, + height=image_dto.height, ) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 40ba67861a..1fcd434852 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -3,7 +3,7 @@ import random from typing import Literal, Optional, Union import einops -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, validator import torch from invokeai.app.invocations.util.choose_model import choose_model @@ -23,7 +23,7 @@ from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationCont import numpy as np from ..services.image_file_storage import ImageType from .baseinvocation import BaseInvocation, InvocationContext -from .image import ImageField, ImageOutput, build_image_output +from .image import ImageField, ImageOutput from .compel import ConditioningField from ...backend.stable_diffusion import PipelineIntermediateState from diffusers.schedulers import SchedulerMixin as Scheduler @@ -362,19 +362,9 @@ class LatentsToImageInvocation(BaseInvocation): np_image = model.decode_latents(latents) image = model.numpy_to_pil(np_image)[0] - # image_type = ImageType.RESULT - # image_name = context.services.images.create_name( - # context.graph_execution_state_id, self.id - # ) + torch.cuda.empty_cache() - # metadata = context.services.metadata.build_metadata( - # session_id=context.graph_execution_state_id, node=self - # ) - - # torch.cuda.empty_cache() - - # context.services.images.save(image_type, image_name, image, metadata) - image_dto = context.services.images_new.create( + image_dto = context.services.images.create( image=image, image_type=ImageType.RESULT, image_category=ImageCategory.GENERAL, @@ -382,10 +372,13 @@ class LatentsToImageInvocation(BaseInvocation): node_id=self.id, ) - return build_image_output( - image_type=image_dto.image_type, - image_name=image_dto.image_name, - image=image, + return ImageOutput( + image=ImageField( + image_name=image_dto.image_name, + image_type=image_dto.image_type, + ), + width=image_dto.width, + height=image_dto.height, ) @@ -474,7 +467,7 @@ class ImageToLatentsInvocation(BaseInvocation): @torch.no_grad() def invoke(self, context: InvocationContext) -> LatentsOutput: - image = context.services.images.get( + image = context.services.images.get_pil_image( self.image.image_type, self.image.image_name ) @@ -496,3 +489,4 @@ class ImageToLatentsInvocation(BaseInvocation): name = f"{context.graph_execution_state_id}__{self.id}" context.services.latents.save(name, latents) return build_latents_output(latents_name=name, latents=latents) + diff --git a/invokeai/app/invocations/reconstruct.py b/invokeai/app/invocations/reconstruct.py index 94a7277acd..024134cd46 100644 --- a/invokeai/app/invocations/reconstruct.py +++ b/invokeai/app/invocations/reconstruct.py @@ -2,21 +2,23 @@ from typing import Literal, Union from pydantic import Field -from invokeai.app.models.image import ImageField, ImageType +from invokeai.app.models.image import ImageCategory, ImageField, ImageType from .baseinvocation import BaseInvocation, InvocationContext, InvocationConfig -from .image import ImageOutput, build_image_output +from .image import ImageOutput + class RestoreFaceInvocation(BaseInvocation): """Restores faces in an image.""" - #fmt: off + + # fmt: off type: Literal["restore_face"] = "restore_face" # Inputs image: Union[ImageField, None] = Field(description="The input image") strength: float = Field(default=0.75, gt=0, le=1, description="The strength of the restoration" ) - #fmt: on - + # fmt: on + # Schema customisation class Config(InvocationConfig): schema_extra = { @@ -26,7 +28,7 @@ class RestoreFaceInvocation(BaseInvocation): } def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get( + image = context.services.images.get_pil_image( self.image.image_type, self.image.image_name ) results = context.services.restoration.upscale_and_reconstruct( @@ -39,18 +41,19 @@ class RestoreFaceInvocation(BaseInvocation): # Results are image and seed, unwrap for now # TODO: can this return multiple results? - image_type = ImageType.RESULT - image_name = context.services.images.create_name( - context.graph_execution_state_id, self.id + image_dto = context.services.images.create( + image=results[0][0], + image_type=ImageType.INTERMEDIATE, + image_category=ImageCategory.GENERAL, + node_id=self.id, + session_id=context.graph_execution_state_id, ) - metadata = context.services.metadata.build_metadata( - session_id=context.graph_execution_state_id, node=self + return ImageOutput( + image=ImageField( + image_name=image_dto.image_name, + image_type=image_dto.image_type, + ), + width=image_dto.width, + height=image_dto.height, ) - - context.services.images.save(image_type, image_name, results[0][0], metadata) - return build_image_output( - image_type=image_type, - image_name=image_name, - image=results[0][0] - ) \ No newline at end of file diff --git a/invokeai/app/invocations/upscale.py b/invokeai/app/invocations/upscale.py index c4938dfd19..75aeec784f 100644 --- a/invokeai/app/invocations/upscale.py +++ b/invokeai/app/invocations/upscale.py @@ -4,22 +4,22 @@ from typing import Literal, Union from pydantic import Field -from invokeai.app.models.image import ImageField, ImageType +from invokeai.app.models.image import ImageCategory, ImageField, ImageType from .baseinvocation import BaseInvocation, InvocationContext, InvocationConfig -from .image import ImageOutput, build_image_output +from .image import ImageOutput class UpscaleInvocation(BaseInvocation): """Upscales an image.""" - #fmt: off + + # fmt: off type: Literal["upscale"] = "upscale" # Inputs image: Union[ImageField, None] = Field(description="The input image", default=None) strength: float = Field(default=0.75, gt=0, le=1, description="The strength") level: Literal[2, 4] = Field(default=2, description="The upscale level") - #fmt: on - + # fmt: on # Schema customisation class Config(InvocationConfig): @@ -30,7 +30,7 @@ class UpscaleInvocation(BaseInvocation): } def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get( + image = context.services.images.get_pil_image( self.image.image_type, self.image.image_name ) results = context.services.restoration.upscale_and_reconstruct( @@ -43,18 +43,19 @@ class UpscaleInvocation(BaseInvocation): # Results are image and seed, unwrap for now # TODO: can this return multiple results? - image_type = ImageType.RESULT - image_name = context.services.images.create_name( - context.graph_execution_state_id, self.id + image_dto = context.services.images.create( + image=results[0][0], + image_type=ImageType.RESULT, + image_category=ImageCategory.GENERAL, + node_id=self.id, + session_id=context.graph_execution_state_id, ) - metadata = context.services.metadata.build_metadata( - session_id=context.graph_execution_state_id, node=self + return ImageOutput( + image=ImageField( + image_name=image_dto.image_name, + image_type=image_dto.image_type, + ), + width=image_dto.width, + height=image_dto.height, ) - - context.services.images.save(image_type, image_name, results[0][0], metadata) - return build_image_output( - image_type=image_type, - image_name=image_name, - image=results[0][0] - ) \ No newline at end of file diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index a85089554c..16b603e89f 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -6,7 +6,6 @@ from invokeai.app.services.images import ImageService from invokeai.backend import ModelManager from .events import EventServiceBase from .latent_storage import LatentsStorageBase -from .image_file_storage import ImageFileStorageBase from .restoration_services import RestorationServices from .invocation_queue import InvocationQueueABC from .item_storage import ItemStorageABC @@ -23,12 +22,11 @@ class InvocationServices: events: EventServiceBase latents: LatentsStorageBase - images: ImageFileStorageBase queue: InvocationQueueABC model_manager: ModelManager restoration: RestorationServices configuration: InvokeAISettings - images_new: ImageService + images: ImageService # NOTE: we must forward-declare any types that include invocations, since invocations can use services graph_library: ItemStorageABC["LibraryGraph"] @@ -41,9 +39,8 @@ class InvocationServices: events: EventServiceBase, logger: Logger, latents: LatentsStorageBase, - images: ImageFileStorageBase, + images: ImageService, queue: InvocationQueueABC, - images_new: ImageService, graph_library: ItemStorageABC["LibraryGraph"], graph_execution_manager: ItemStorageABC["GraphExecutionState"], processor: "InvocationProcessorABC", @@ -56,7 +53,6 @@ class InvocationServices: self.latents = latents self.images = images self.queue = queue - self.images_new = images_new self.graph_library = graph_library self.graph_execution_manager = graph_execution_manager self.processor = processor From c7c08367215251e32d6c76c416ebdfca4b781702 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 24 May 2023 17:19:13 +1000 Subject: [PATCH 47/72] feat(ui): migrate linear workflows to latents --- .../src/app/selectors/readinessSelector.ts | 2 +- .../ImageMetadataViewer.tsx | 4 +- .../web/src/features/nodes/types/types.ts | 5 +- .../graphBuilders/buildImageToImageGraph.ts | 143 ++++++++++-- .../graphBuilders/buildTextToImageGraph.ts | 112 ++++++++-- .../nodes/util/nodeBuilders/addNoiseNodes.ts | 208 ++++++++++++++++++ .../util/nodeBuilders/buildCompelNode.ts | 26 +++ .../nodeBuilders/buildImageToImageNode.ts | 4 +- .../util/nodeBuilders/buildInpaintNode.ts | 4 +- .../util/nodeBuilders/buildTextToImageNode.ts | 4 +- .../Core/ParamPositiveConditioning.tsx | 6 +- .../ImageToImage/InitialImagePreview.tsx | 8 +- .../features/parameters/hooks/usePrompt.ts | 4 +- .../parameters/store/generationSlice.ts | 15 +- .../store/setAllParametersReducer.ts | 2 +- .../frontend/web/src/services/api/index.ts | 1 + .../web/src/services/api/models/Graph.ts | 3 +- .../web/src/services/api/models/ImageDTO.ts | 2 +- .../web/src/services/api/models/MaskOutput.ts | 8 + .../services/api/models/RangeInvocation.ts | 2 +- .../api/models/RangeOfSizeInvocation.ts | 27 +++ .../services/api/services/ImagesService.ts | 66 +++--- .../services/api/services/SessionsService.ts | 5 +- 23 files changed, 550 insertions(+), 111 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/util/nodeBuilders/addNoiseNodes.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildCompelNode.ts create mode 100644 invokeai/frontend/web/src/services/api/models/RangeOfSizeInvocation.ts diff --git a/invokeai/frontend/web/src/app/selectors/readinessSelector.ts b/invokeai/frontend/web/src/app/selectors/readinessSelector.ts index 6fd212494f..2b77fe9f47 100644 --- a/invokeai/frontend/web/src/app/selectors/readinessSelector.ts +++ b/invokeai/frontend/web/src/app/selectors/readinessSelector.ts @@ -10,7 +10,7 @@ export const readinessSelector = createSelector( [generationSelector, systemSelector, activeTabNameSelector], (generation, system, activeTabName) => { const { - prompt, + positivePrompt: prompt, shouldGenerateVariations, seedWeights, initialImage, 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 b87fdcb90e..b4bf9a6d25 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx @@ -20,7 +20,7 @@ import { setImg2imgStrength, setNegativePrompt, setPerlin, - setPrompt, + setPositivePrompt, setScheduler, setSeamless, setSeed, @@ -199,7 +199,7 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { ? metadata.positive_conditioning : promptToString(metadata.positive_conditioning) } - onClick={() => setPrompt(metadata.positive_conditioning!)} + onClick={() => setPositivePrompt(metadata.positive_conditioning!)} /> )} {metadata.negative_conditioning && ( diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts index d2c7c7e704..efb4a5518d 100644 --- a/invokeai/frontend/web/src/features/nodes/types/types.ts +++ b/invokeai/frontend/web/src/features/nodes/types/types.ts @@ -1,7 +1,10 @@ import { OpenAPIV3 } from 'openapi-types'; import { RgbaColor } from 'react-colorful'; -import { ImageDTO } from 'services/api'; +import { Graph, ImageDTO } from 'services/api'; import { AnyInvocationType } from 'services/events/types'; +import { O } from 'ts-toolbelt'; + +export type NonNullableGraph = O.Required; export type InvocationValue = { id: string; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts index d7a0fc66d3..003069ea89 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts @@ -1,35 +1,132 @@ +import { v4 as uuidv4 } from 'uuid'; import { RootState } from 'app/store/store'; -import { Graph } from 'services/api'; -import { buildImg2ImgNode } from '../nodeBuilders/buildImageToImageNode'; -import { buildRangeNode } from '../nodeBuilders/buildRangeNode'; -import { buildIterateNode } from '../nodeBuilders/buildIterateNode'; -import { buildEdges } from '../edgeBuilders/buildEdges'; +import { + CompelInvocation, + Graph, + ImageToLatentsInvocation, + LatentsToImageInvocation, + LatentsToLatentsInvocation, +} from 'services/api'; +import { NonNullableGraph } from 'features/nodes/types/types'; +import { addNoiseNodes } from '../nodeBuilders/addNoiseNodes'; +import { log } from 'app/logging/useLogger'; + +const moduleLog = log.child({ namespace: 'buildImageToImageGraph' }); + +const POSITIVE_CONDITIONING = 'positive_conditioning'; +const NEGATIVE_CONDITIONING = 'negative_conditioning'; +const IMAGE_TO_LATENTS = 'image_to_latents'; +const LATENTS_TO_LATENTS = 'latents_to_latents'; +const LATENTS_TO_IMAGE = 'latents_to_image'; /** - * Builds the Linear workflow graph. + * Builds the Image to Image tab graph. */ export const buildImageToImageGraph = (state: RootState): Graph => { - const baseNode = buildImg2ImgNode(state); + const { + positivePrompt, + negativePrompt, + model, + cfgScale: cfg_scale, + scheduler, + steps, + initialImage, + img2imgStrength: strength, + } = state.generation; - // We always range and iterate nodes, no matter the iteration count - // This is required to provide the correct seeds to the backend engine - const rangeNode = buildRangeNode(state); - const iterateNode = buildIterateNode(); + if (!initialImage) { + moduleLog.error('No initial image found in state'); + throw new Error('No initial image found in state'); + } - // Build the edges for the nodes selected. - const edges = buildEdges(baseNode, rangeNode, iterateNode); - - // Assemble! - const graph = { - nodes: { - [rangeNode.id]: rangeNode, - [iterateNode.id]: iterateNode, - [baseNode.id]: baseNode, - }, - edges, + let graph: NonNullableGraph = { + nodes: {}, + edges: [], }; - // TODO: hires fix requires latent space upscaling; we don't have nodes for this yet + // Create the conditioning, t2l and l2i nodes + const positiveConditioningNode: CompelInvocation = { + id: POSITIVE_CONDITIONING, + type: 'compel', + prompt: positivePrompt, + model, + }; + + const negativeConditioningNode: CompelInvocation = { + id: NEGATIVE_CONDITIONING, + type: 'compel', + prompt: negativePrompt, + model, + }; + + const imageToLatentsNode: ImageToLatentsInvocation = { + id: IMAGE_TO_LATENTS, + type: 'i2l', + model, + image: { + image_name: initialImage?.image_name, + image_type: initialImage?.image_type, + }, + }; + + const latentsToLatentsNode: LatentsToLatentsInvocation = { + id: LATENTS_TO_LATENTS, + type: 'l2l', + cfg_scale, + model, + scheduler, + steps, + strength, + }; + + const latentsToImageNode: LatentsToImageInvocation = { + id: LATENTS_TO_IMAGE, + type: 'l2i', + model, + }; + + // Add to the graph + graph.nodes[POSITIVE_CONDITIONING] = positiveConditioningNode; + graph.nodes[NEGATIVE_CONDITIONING] = negativeConditioningNode; + graph.nodes[IMAGE_TO_LATENTS] = imageToLatentsNode; + graph.nodes[LATENTS_TO_LATENTS] = latentsToLatentsNode; + graph.nodes[LATENTS_TO_IMAGE] = latentsToImageNode; + + // Connect them + graph.edges.push({ + source: { node_id: POSITIVE_CONDITIONING, field: 'conditioning' }, + destination: { + node_id: LATENTS_TO_LATENTS, + field: 'positive_conditioning', + }, + }); + + graph.edges.push({ + source: { node_id: NEGATIVE_CONDITIONING, field: 'conditioning' }, + destination: { + node_id: LATENTS_TO_LATENTS, + field: 'negative_conditioning', + }, + }); + + graph.edges.push({ + source: { node_id: IMAGE_TO_LATENTS, field: 'latents' }, + destination: { + node_id: LATENTS_TO_LATENTS, + field: 'latents', + }, + }); + + graph.edges.push({ + source: { node_id: LATENTS_TO_LATENTS, field: 'latents' }, + destination: { + node_id: LATENTS_TO_IMAGE, + field: 'latents', + }, + }); + + // Create and add the noise nodes + graph = addNoiseNodes(graph, latentsToLatentsNode.id, state); return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts index 8b1d8edcc9..2fc3ea3975 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts @@ -1,35 +1,99 @@ import { RootState } from 'app/store/store'; -import { Graph } from 'services/api'; -import { buildTxt2ImgNode } from '../nodeBuilders/buildTextToImageNode'; -import { buildRangeNode } from '../nodeBuilders/buildRangeNode'; -import { buildIterateNode } from '../nodeBuilders/buildIterateNode'; -import { buildEdges } from '../edgeBuilders/buildEdges'; +import { + CompelInvocation, + Graph, + LatentsToImageInvocation, + TextToLatentsInvocation, +} from 'services/api'; +import { NonNullableGraph } from 'features/nodes/types/types'; +import { addNoiseNodes } from '../nodeBuilders/addNoiseNodes'; + +const POSITIVE_CONDITIONING = 'positive_conditioning'; +const NEGATIVE_CONDITIONING = 'negative_conditioning'; +const TEXT_TO_LATENTS = 'text_to_latents'; +const LATENTS_TO_IMAGE = 'latnets_to_image'; /** - * Builds the Linear workflow graph. + * Builds the Text to Image tab graph. */ export const buildTextToImageGraph = (state: RootState): Graph => { - const baseNode = buildTxt2ImgNode(state); + const { + positivePrompt, + negativePrompt, + model, + cfgScale: cfg_scale, + scheduler, + steps, + } = state.generation; - // We always range and iterate nodes, no matter the iteration count - // This is required to provide the correct seeds to the backend engine - const rangeNode = buildRangeNode(state); - const iterateNode = buildIterateNode(); - - // Build the edges for the nodes selected. - const edges = buildEdges(baseNode, rangeNode, iterateNode); - - // Assemble! - const graph = { - nodes: { - [rangeNode.id]: rangeNode, - [iterateNode.id]: iterateNode, - [baseNode.id]: baseNode, - }, - edges, + let graph: NonNullableGraph = { + nodes: {}, + edges: [], }; - // TODO: hires fix requires latent space upscaling; we don't have nodes for this yet + // Create the conditioning, t2l and l2i nodes + const positiveConditioningNode: CompelInvocation = { + id: POSITIVE_CONDITIONING, + type: 'compel', + prompt: positivePrompt, + model, + }; + const negativeConditioningNode: CompelInvocation = { + id: NEGATIVE_CONDITIONING, + type: 'compel', + prompt: negativePrompt, + model, + }; + + const textToLatentsNode: TextToLatentsInvocation = { + id: TEXT_TO_LATENTS, + type: 't2l', + cfg_scale, + model, + scheduler, + steps, + }; + + const latentsToImageNode: LatentsToImageInvocation = { + id: LATENTS_TO_IMAGE, + type: 'l2i', + model, + }; + + // Add to the graph + graph.nodes[POSITIVE_CONDITIONING] = positiveConditioningNode; + graph.nodes[NEGATIVE_CONDITIONING] = negativeConditioningNode; + graph.nodes[TEXT_TO_LATENTS] = textToLatentsNode; + graph.nodes[LATENTS_TO_IMAGE] = latentsToImageNode; + + // Connect them + graph.edges.push({ + source: { node_id: POSITIVE_CONDITIONING, field: 'conditioning' }, + destination: { + node_id: TEXT_TO_LATENTS, + field: 'positive_conditioning', + }, + }); + + graph.edges.push({ + source: { node_id: NEGATIVE_CONDITIONING, field: 'conditioning' }, + destination: { + node_id: TEXT_TO_LATENTS, + field: 'negative_conditioning', + }, + }); + + graph.edges.push({ + source: { node_id: TEXT_TO_LATENTS, field: 'latents' }, + destination: { + node_id: LATENTS_TO_IMAGE, + field: 'latents', + }, + }); + + // Create and add the noise nodes + graph = addNoiseNodes(graph, TEXT_TO_LATENTS, state); + console.log(graph); return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/addNoiseNodes.ts b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/addNoiseNodes.ts new file mode 100644 index 0000000000..ba3d4d8168 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/addNoiseNodes.ts @@ -0,0 +1,208 @@ +import { RootState } from 'app/store/store'; +import { + IterateInvocation, + NoiseInvocation, + RandomIntInvocation, + RangeOfSizeInvocation, +} from 'services/api'; +import { NonNullableGraph } from 'features/nodes/types/types'; +import { cloneDeep } from 'lodash-es'; + +const NOISE = 'noise'; +const RANDOM_INT = 'rand_int'; +const RANGE_OF_SIZE = 'range_of_size'; +const ITERATE = 'iterate'; +/** + * Adds the appropriate noise nodes to a linear UI t2l or l2l graph. + * + * @param graph The graph to add the noise nodes to. + * @param baseNodeId The id of the base node to connect the noise nodes to. + * @param state The app state.. + */ +export const addNoiseNodes = ( + graph: NonNullableGraph, + baseNodeId: string, + state: RootState +): NonNullableGraph => { + const graphClone = cloneDeep(graph); + + // Create and add the noise nodes + const { width, height, seed, iterations, shouldRandomizeSeed } = + state.generation; + + // Single iteration, explicit seed + if (!shouldRandomizeSeed && iterations === 1) { + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + seed: seed, + width, + height, + }; + + graphClone.nodes[NOISE] = noiseNode; + + // Connect them + graphClone.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: baseNodeId, + field: 'noise', + }, + }); + } + + // Single iteration, random seed + if (shouldRandomizeSeed && iterations === 1) { + // TODO: This assumes the `high` value is the max seed value + const randomIntNode: RandomIntInvocation = { + id: RANDOM_INT, + type: 'rand_int', + }; + + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + width, + height, + }; + + graphClone.nodes[RANDOM_INT] = randomIntNode; + graphClone.nodes[NOISE] = noiseNode; + + graphClone.edges.push({ + source: { node_id: RANDOM_INT, field: 'a' }, + destination: { + node_id: NOISE, + field: 'seed', + }, + }); + + graphClone.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: baseNodeId, + field: 'noise', + }, + }); + } + + // Multiple iterations, explicit seed + if (!shouldRandomizeSeed && iterations > 1) { + const rangeOfSizeNode: RangeOfSizeInvocation = { + id: RANGE_OF_SIZE, + type: 'range_of_size', + start: seed, + size: iterations, + }; + + const iterateNode: IterateInvocation = { + id: ITERATE, + type: 'iterate', + }; + + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + width, + height, + }; + + graphClone.nodes[RANGE_OF_SIZE] = rangeOfSizeNode; + graphClone.nodes[ITERATE] = iterateNode; + graphClone.nodes[NOISE] = noiseNode; + + graphClone.edges.push({ + source: { node_id: RANGE_OF_SIZE, field: 'collection' }, + destination: { + node_id: ITERATE, + field: 'collection', + }, + }); + + graphClone.edges.push({ + source: { + node_id: ITERATE, + field: 'item', + }, + destination: { + node_id: NOISE, + field: 'seed', + }, + }); + + graphClone.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: baseNodeId, + field: 'noise', + }, + }); + } + + // Multiple iterations, random seed + if (shouldRandomizeSeed && iterations > 1) { + // TODO: This assumes the `high` value is the max seed value + const randomIntNode: RandomIntInvocation = { + id: RANDOM_INT, + type: 'rand_int', + }; + + const rangeOfSizeNode: RangeOfSizeInvocation = { + id: RANGE_OF_SIZE, + type: 'range_of_size', + size: iterations, + }; + + const iterateNode: IterateInvocation = { + id: ITERATE, + type: 'iterate', + }; + + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + width, + height, + }; + + graphClone.nodes[RANDOM_INT] = randomIntNode; + graphClone.nodes[RANGE_OF_SIZE] = rangeOfSizeNode; + graphClone.nodes[ITERATE] = iterateNode; + graphClone.nodes[NOISE] = noiseNode; + + graphClone.edges.push({ + source: { node_id: RANDOM_INT, field: 'a' }, + destination: { node_id: RANGE_OF_SIZE, field: 'start' }, + }); + + graphClone.edges.push({ + source: { node_id: RANGE_OF_SIZE, field: 'collection' }, + destination: { + node_id: ITERATE, + field: 'collection', + }, + }); + + graphClone.edges.push({ + source: { + node_id: ITERATE, + field: 'item', + }, + destination: { + node_id: NOISE, + field: 'seed', + }, + }); + + graphClone.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: baseNodeId, + field: 'noise', + }, + }); + } + + return graphClone; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildCompelNode.ts b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildCompelNode.ts new file mode 100644 index 0000000000..02ac148181 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildCompelNode.ts @@ -0,0 +1,26 @@ +import { v4 as uuidv4 } from 'uuid'; +import { RootState } from 'app/store/store'; +import { CompelInvocation } from 'services/api'; +import { O } from 'ts-toolbelt'; + +export const buildCompelNode = ( + prompt: string, + state: RootState, + overrides: O.Partial = {} +): CompelInvocation => { + const nodeId = uuidv4(); + const { generation } = state; + + const { model } = generation; + + const compelNode: CompelInvocation = { + id: nodeId, + type: 'compel', + prompt, + model, + }; + + Object.assign(compelNode, overrides); + + return compelNode; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildImageToImageNode.ts b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildImageToImageNode.ts index 02480289d4..5f00d12a23 100644 --- a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildImageToImageNode.ts +++ b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildImageToImageNode.ts @@ -18,8 +18,8 @@ export const buildImg2ImgNode = ( const activeTabName = activeTabNameSelector(state); const { - prompt, - negativePrompt, + positivePrompt: prompt, + negativePrompt: negativePrompt, seed, steps, width, diff --git a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildInpaintNode.ts b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildInpaintNode.ts index 36658ef58f..b3f6cca933 100644 --- a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildInpaintNode.ts +++ b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildInpaintNode.ts @@ -13,8 +13,8 @@ export const buildInpaintNode = ( const activeTabName = activeTabNameSelector(state); const { - prompt, - negativePrompt, + positivePrompt: prompt, + negativePrompt: negativePrompt, seed, steps, width, diff --git a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildTextToImageNode.ts b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildTextToImageNode.ts index 761c909776..64e7aaa831 100644 --- a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildTextToImageNode.ts +++ b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildTextToImageNode.ts @@ -11,8 +11,8 @@ export const buildTxt2ImgNode = ( const { generation } = state; const { - prompt, - negativePrompt, + positivePrompt: prompt, + negativePrompt: negativePrompt, seed, steps, width, diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx index b4a5c1f09a..365bade0aa 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx @@ -8,7 +8,7 @@ import { readinessSelector } from 'app/selectors/readinessSelector'; import { GenerationState, clampSymmetrySteps, - setPrompt, + setPositivePrompt, } from 'features/parameters/store/generationSlice'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; @@ -22,7 +22,7 @@ const promptInputSelector = createSelector( [(state: RootState) => state.generation, activeTabNameSelector], (parameters: GenerationState, activeTabName) => { return { - prompt: parameters.prompt, + prompt: parameters.positivePrompt, activeTabName, }; }, @@ -46,7 +46,7 @@ const ParamPositiveConditioning = () => { const { t } = useTranslation(); const handleChangePrompt = (e: ChangeEvent) => { - dispatch(setPrompt(e.target.value)); + dispatch(setPositivePrompt(e.target.value)); }; useHotkeys( diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx index 9ae1ff55e2..be40f548e6 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx @@ -57,7 +57,7 @@ const InitialImagePreview = () => { const name = e.dataTransfer.getData('invokeai/imageName'); const type = e.dataTransfer.getData('invokeai/imageType') as ImageType; - dispatch(initialImageSelected({ name, type })); + dispatch(initialImageSelected({ image_name: name, image_type: type })); }, [dispatch] ); @@ -73,10 +73,10 @@ const InitialImagePreview = () => { }} onDrop={handleDrop} > - {initialImage?.url && ( + {initialImage?.image_url && ( <> } onError={handleError} @@ -92,7 +92,7 @@ const InitialImagePreview = () => { )} - {!initialImage?.url && ( + {!initialImage?.image_url && ( { const [prompt, negativePrompt] = getPromptAndNegative(promptString); - dispatch(setPrompt(prompt)); + dispatch(setPositivePrompt(prompt)); dispatch(setNegativePrompt(negativePrompt)); }, [dispatch] diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index b471ffc783..f5054f1969 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -16,7 +16,7 @@ export interface GenerationState { initialImage?: ImageDTO; iterations: number; perlin: number; - prompt: string; + positivePrompt: string; negativePrompt: string; scheduler: Scheduler; seamBlur: number; @@ -50,7 +50,7 @@ export const initialGenerationState: GenerationState = { infillMethod: 'patchmatch', iterations: 1, perlin: 0, - prompt: '', + positivePrompt: '', negativePrompt: '', scheduler: 'lms', seamBlur: 16, @@ -83,12 +83,15 @@ export const generationSlice = createSlice({ name: 'generation', initialState, reducers: { - setPrompt: (state, action: PayloadAction) => { + setPositivePrompt: ( + state, + action: PayloadAction + ) => { const newPrompt = action.payload; if (typeof newPrompt === 'string') { - state.prompt = newPrompt; + state.positivePrompt = newPrompt; } else { - state.prompt = promptToString(newPrompt); + state.positivePrompt = promptToString(newPrompt); } }, setNegativePrompt: ( @@ -244,7 +247,7 @@ export const { setInfillMethod, setIterations, setPerlin, - setPrompt, + setPositivePrompt, setNegativePrompt, setScheduler, setSeamBlur, diff --git a/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts b/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts index dc147090b4..d6d1af0f8e 100644 --- a/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts +++ b/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts @@ -31,7 +31,7 @@ export const setAllParametersReducer = ( state.model = String(model); } if (prompt !== undefined) { - state.prompt = String(prompt); + state.positivePrompt = String(prompt); } if (scheduler !== undefined) { const schedulerString = String(scheduler); diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index 0b97a97fb7..ecf8621ed6 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -66,6 +66,7 @@ export type { PromptOutput } from './models/PromptOutput'; export type { RandomIntInvocation } from './models/RandomIntInvocation'; export type { RandomRangeInvocation } from './models/RandomRangeInvocation'; export type { RangeInvocation } from './models/RangeInvocation'; +export type { RangeOfSizeInvocation } from './models/RangeOfSizeInvocation'; export type { ResizeLatentsInvocation } from './models/ResizeLatentsInvocation'; export type { RestoreFaceInvocation } from './models/RestoreFaceInvocation'; export type { ScaleLatentsInvocation } from './models/ScaleLatentsInvocation'; diff --git a/invokeai/frontend/web/src/services/api/models/Graph.ts b/invokeai/frontend/web/src/services/api/models/Graph.ts index 4399725680..039923e585 100644 --- a/invokeai/frontend/web/src/services/api/models/Graph.ts +++ b/invokeai/frontend/web/src/services/api/models/Graph.ts @@ -31,6 +31,7 @@ import type { PasteImageInvocation } from './PasteImageInvocation'; import type { RandomIntInvocation } from './RandomIntInvocation'; import type { RandomRangeInvocation } from './RandomRangeInvocation'; import type { RangeInvocation } from './RangeInvocation'; +import type { RangeOfSizeInvocation } from './RangeOfSizeInvocation'; import type { ResizeLatentsInvocation } from './ResizeLatentsInvocation'; import type { RestoreFaceInvocation } from './RestoreFaceInvocation'; import type { ScaleLatentsInvocation } from './ScaleLatentsInvocation'; @@ -48,7 +49,7 @@ export type Graph = { /** * The nodes in this graph */ - nodes?: Record; + nodes?: Record; /** * The connections between nodes and their fields in this graph */ diff --git a/invokeai/frontend/web/src/services/api/models/ImageDTO.ts b/invokeai/frontend/web/src/services/api/models/ImageDTO.ts index c5fad70d8a..c5377b4c76 100644 --- a/invokeai/frontend/web/src/services/api/models/ImageDTO.ts +++ b/invokeai/frontend/web/src/services/api/models/ImageDTO.ts @@ -59,7 +59,7 @@ export type ImageDTO = { */ node_id?: string; /** - * A limited subset of the image's metadata. Retrieve the image's session for full metadata. + * A limited subset of the image's generation metadata. Retrieve the image's session for full metadata. */ metadata?: ImageMetadata; }; diff --git a/invokeai/frontend/web/src/services/api/models/MaskOutput.ts b/invokeai/frontend/web/src/services/api/models/MaskOutput.ts index 645fb8d1cb..d4594fe6e9 100644 --- a/invokeai/frontend/web/src/services/api/models/MaskOutput.ts +++ b/invokeai/frontend/web/src/services/api/models/MaskOutput.ts @@ -13,5 +13,13 @@ export type MaskOutput = { * The output mask */ mask: ImageField; + /** + * The width of the mask in pixels + */ + width?: number; + /** + * The height of the mask in pixels + */ + height?: number; }; diff --git a/invokeai/frontend/web/src/services/api/models/RangeInvocation.ts b/invokeai/frontend/web/src/services/api/models/RangeInvocation.ts index 72bc4806da..1c37ca7fe3 100644 --- a/invokeai/frontend/web/src/services/api/models/RangeInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/RangeInvocation.ts @@ -3,7 +3,7 @@ /* eslint-disable */ /** - * Creates a range + * Creates a range of numbers from start to stop with step */ export type RangeInvocation = { /** diff --git a/invokeai/frontend/web/src/services/api/models/RangeOfSizeInvocation.ts b/invokeai/frontend/web/src/services/api/models/RangeOfSizeInvocation.ts new file mode 100644 index 0000000000..b918f17130 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/RangeOfSizeInvocation.ts @@ -0,0 +1,27 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Creates a range from start to start + size with step + */ +export type RangeOfSizeInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'range_of_size'; + /** + * The start of the range + */ + start?: number; + /** + * The number of values + */ + size?: number; + /** + * The step of the range + */ + step?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/services/ImagesService.ts b/invokeai/frontend/web/src/services/api/services/ImagesService.ts index a172c022f4..13b2ef836a 100644 --- a/invokeai/frontend/web/src/services/api/services/ImagesService.ts +++ b/invokeai/frontend/web/src/services/api/services/ImagesService.ts @@ -89,6 +89,39 @@ export class ImagesService { }); } + /** + * Get Image Full + * Gets a full-resolution image file + * @returns any Return the full-resolution image + * @throws ApiError + */ + public static getImageFull({ + imageType, + imageName, + }: { + /** + * The type of full-resolution image file to get + */ + imageType: ImageType, + /** + * The name of full-resolution image file to get + */ + imageName: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/images/{image_type}/{image_name}', + path: { + 'image_type': imageType, + 'image_name': imageName, + }, + errors: { + 404: `Image not found`, + 422: `Validation Error`, + }, + }); + } + /** * Delete Image * Deletes an image @@ -150,39 +183,6 @@ export class ImagesService { }); } - /** - * Get Image Full - * Gets a full-resolution image file - * @returns any Return the full-resolution image - * @throws ApiError - */ - public static getImageFull({ - imageType, - imageName, - }: { - /** - * The type of full-resolution image file to get - */ - imageType: ImageType, - /** - * The name of full-resolution image file to get - */ - imageName: string, - }): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/images/{image_type}/{image_name}/full', - path: { - 'image_type': imageType, - 'image_name': imageName, - }, - errors: { - 404: `Image not found`, - 422: `Validation Error`, - }, - }); - } - /** * Get Image Thumbnail * Gets a thumbnail image file diff --git a/invokeai/frontend/web/src/services/api/services/SessionsService.ts b/invokeai/frontend/web/src/services/api/services/SessionsService.ts index 1925b0800f..23597c9e9e 100644 --- a/invokeai/frontend/web/src/services/api/services/SessionsService.ts +++ b/invokeai/frontend/web/src/services/api/services/SessionsService.ts @@ -33,6 +33,7 @@ import type { PasteImageInvocation } from '../models/PasteImageInvocation'; import type { RandomIntInvocation } from '../models/RandomIntInvocation'; import type { RandomRangeInvocation } from '../models/RandomRangeInvocation'; import type { RangeInvocation } from '../models/RangeInvocation'; +import type { RangeOfSizeInvocation } from '../models/RangeOfSizeInvocation'; import type { ResizeLatentsInvocation } from '../models/ResizeLatentsInvocation'; import type { RestoreFaceInvocation } from '../models/RestoreFaceInvocation'; import type { ScaleLatentsInvocation } from '../models/ScaleLatentsInvocation'; @@ -150,7 +151,7 @@ export class SessionsService { * The id of the session */ sessionId: string, - requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), + requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), }): CancelablePromise { return __request(OpenAPI, { method: 'POST', @@ -187,7 +188,7 @@ export class SessionsService { * The path to the node in the graph */ nodePath: string, - requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), + requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), }): CancelablePromise { return __request(OpenAPI, { method: 'PUT', From 66ad04fcfc18fc1dd6e92593b84be77f359915dd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 24 May 2023 21:34:53 +1000 Subject: [PATCH 48/72] feat(nodes): add mask image category --- invokeai/app/models/image.py | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/app/models/image.py b/invokeai/app/models/image.py index 1d493e65c2..544951ea34 100644 --- a/invokeai/app/models/image.py +++ b/invokeai/app/models/image.py @@ -28,6 +28,7 @@ class ImageCategory(str, Enum, metaclass=MetaEnum): GENERAL = "general" CONTROL = "control" + MASK = "mask" OTHER = "other" From 460d555a3dd89885bdaabb9a54515f6e412a3fb2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 24 May 2023 21:35:46 +1000 Subject: [PATCH 49/72] feat(nodes): add image mul, channel, convert nodes also make img node names consistent --- invokeai/app/invocations/image.py | 137 +++++++++++++++++++++++++++--- 1 file changed, 125 insertions(+), 12 deletions(-) diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 8f789853ac..b25d3735c2 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -4,7 +4,7 @@ import io from typing import Literal, Optional, Union import numpy -from PIL import Image, ImageFilter, ImageOps +from PIL import Image, ImageFilter, ImageOps, ImageChops from pydantic import BaseModel, Field from ..models.image import ImageCategory, ImageField, ImageType @@ -112,11 +112,11 @@ class ShowImageInvocation(BaseInvocation): ) -class CropImageInvocation(BaseInvocation, PILInvocationConfig): +class ImageCropInvocation(BaseInvocation, PILInvocationConfig): """Crops an image to a specified box. The box can be outside of the image.""" # fmt: off - type: Literal["crop"] = "crop" + type: Literal["img_crop"] = "img_crop" # Inputs image: Union[ImageField, None] = Field(default=None, description="The image to crop") @@ -154,11 +154,11 @@ class CropImageInvocation(BaseInvocation, PILInvocationConfig): ) -class PasteImageInvocation(BaseInvocation, PILInvocationConfig): +class ImagePasteInvocation(BaseInvocation, PILInvocationConfig): """Pastes an image into another image.""" # fmt: off - type: Literal["paste"] = "paste" + type: Literal["img_paste"] = "img_paste" # Inputs base_image: Union[ImageField, None] = Field(default=None, description="The base image") @@ -238,7 +238,7 @@ class MaskFromAlphaInvocation(BaseInvocation, PILInvocationConfig): image_dto = context.services.images.create( image=image_mask, image_type=ImageType.INTERMEDIATE, - image_category=ImageCategory.GENERAL, + image_category=ImageCategory.MASK, node_id=self.id, session_id=context.graph_execution_state_id, ) @@ -252,11 +252,124 @@ class MaskFromAlphaInvocation(BaseInvocation, PILInvocationConfig): ) -class BlurInvocation(BaseInvocation, PILInvocationConfig): +class ImageMultiplyInvocation(BaseInvocation, PILInvocationConfig): + """Multiplies two images together using `PIL.ImageChops.multiply()`.""" + + # fmt: off + type: Literal["img_mul"] = "img_mul" + + # Inputs + image1: Union[ImageField, None] = Field(default=None, description="The first image to multiply") + image2: Union[ImageField, None] = Field(default=None, description="The second image to multiply") + # fmt: on + + def invoke(self, context: InvocationContext) -> ImageOutput: + image1 = context.services.images.get_pil_image( + self.image1.image_type, self.image1.image_name + ) + image2 = context.services.images.get_pil_image( + self.image2.image_type, self.image2.image_name + ) + + multiply_image = ImageChops.multiply(image1, image2) + + image_dto = context.services.images.create( + image=multiply_image, + image_type=ImageType.INTERMEDIATE, + image_category=ImageCategory.GENERAL, + node_id=self.id, + session_id=context.graph_execution_state_id, + ) + + return ImageOutput( + image=ImageField( + image_type=image_dto.image_type, image_name=image_dto.image_name + ), + width=image_dto.width, + height=image_dto.height, + ) + + +IMAGE_CHANNELS = Literal["A", "R", "G", "B"] + + +class ImageChannelInvocation(BaseInvocation, PILInvocationConfig): + """Gets a channel from an image.""" + + # fmt: off + type: Literal["img_chan"] = "img_chan" + + # Inputs + image: Union[ImageField, None] = Field(default=None, description="The image to get the channel from") + channel: IMAGE_CHANNELS = Field(default="A", description="The channel to get") + # fmt: on + + def invoke(self, context: InvocationContext) -> ImageOutput: + image = context.services.images.get_pil_image( + self.image.image_type, self.image.image_name + ) + + channel_image = image.getchannel(self.channel) + + image_dto = context.services.images.create( + image=channel_image, + image_type=ImageType.INTERMEDIATE, + image_category=ImageCategory.GENERAL, + node_id=self.id, + session_id=context.graph_execution_state_id, + ) + + return ImageOutput( + image=ImageField( + image_type=image_dto.image_type, image_name=image_dto.image_name + ), + width=image_dto.width, + height=image_dto.height, + ) + + +IMAGE_MODES = Literal['L', 'RGB', 'RGBA', 'CMYK', 'YCbCr', 'LAB', 'HSV', 'I', 'F'] + +class ImageConvertInvocation(BaseInvocation, PILInvocationConfig): + """Converts an image to a different mode.""" + + # fmt: off + type: Literal["img_conv"] = "img_conv" + + # Inputs + image: Union[ImageField, None] = Field(default=None, description="The image to convert") + mode: IMAGE_MODES = Field(default="L", description="The mode to convert to") + # fmt: on + + def invoke(self, context: InvocationContext) -> ImageOutput: + image = context.services.images.get_pil_image( + self.image.image_type, self.image.image_name + ) + + converted_image = image.convert(self.mode) + + image_dto = context.services.images.create( + image=converted_image, + image_type=ImageType.INTERMEDIATE, + image_category=ImageCategory.GENERAL, + node_id=self.id, + session_id=context.graph_execution_state_id, + ) + + return ImageOutput( + image=ImageField( + image_type=image_dto.image_type, image_name=image_dto.image_name + ), + width=image_dto.width, + height=image_dto.height, + ) + + +class ImageBlurInvocation(BaseInvocation, PILInvocationConfig): """Blurs an image""" # fmt: off - type: Literal["blur"] = "blur" + type: Literal["img_blur"] = "img_blur" # Inputs image: Union[ImageField, None] = Field(default=None, description="The image to blur") @@ -294,11 +407,11 @@ class BlurInvocation(BaseInvocation, PILInvocationConfig): ) -class LerpInvocation(BaseInvocation, PILInvocationConfig): +class ImageLerpInvocation(BaseInvocation, PILInvocationConfig): """Linear interpolation of all pixels of an image""" # fmt: off - type: Literal["lerp"] = "lerp" + type: Literal["img_lerp"] = "img_lerp" # Inputs image: Union[ImageField, None] = Field(default=None, description="The image to lerp") @@ -334,11 +447,11 @@ class LerpInvocation(BaseInvocation, PILInvocationConfig): ) -class InverseLerpInvocation(BaseInvocation, PILInvocationConfig): +class ImageInverseLerpInvocation(BaseInvocation, PILInvocationConfig): """Inverse linear interpolation of all pixels of an image""" # fmt: off - type: Literal["ilerp"] = "ilerp" + type: Literal["img_ilerp"] = "img_ilerp" # Inputs image: Union[ImageField, None] = Field(default=None, description="The image to lerp") From 1e0ae8404c0a3f7dd279177e7056dd13667c07c4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 24 May 2023 21:41:46 +1000 Subject: [PATCH 50/72] feat(nodes): comment out seamless this will be a model config feature when model manager is ready --- invokeai/app/invocations/latent.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 1fcd434852..34da76d39a 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -169,8 +169,8 @@ class TextToLatentsInvocation(BaseInvocation): cfg_scale: float = Field(default=7.5, gt=0, description="The Classifier-Free Guidance, higher values may result in a result closer to the prompt", ) scheduler: SAMPLER_NAME_VALUES = Field(default="lms", description="The scheduler to use" ) model: str = Field(default="", description="The model to use (currently ignored)") - seamless: bool = Field(default=False, description="Whether or not to generate an image that can tile without seams", ) - seamless_axes: str = Field(default="", description="The axes to tile the image on, 'x' and/or 'y'") + # seamless: bool = Field(default=False, description="Whether or not to generate an image that can tile without seams", ) + # seamless_axes: str = Field(default="", description="The axes to tile the image on, 'x' and/or 'y'") # fmt: on # Schema customisation @@ -205,17 +205,17 @@ class TextToLatentsInvocation(BaseInvocation): scheduler_name=self.scheduler ) - if isinstance(model, DiffusionPipeline): - for component in [model.unet, model.vae]: - configure_model_padding(component, - self.seamless, - self.seamless_axes - ) - else: - configure_model_padding(model, - self.seamless, - self.seamless_axes - ) + # if isinstance(model, DiffusionPipeline): + # for component in [model.unet, model.vae]: + # configure_model_padding(component, + # self.seamless, + # self.seamless_axes + # ) + # else: + # configure_model_padding(model, + # self.seamless, + # self.seamless_axes + # ) return model From ad39680febe0b9fe936d3fca2a0ddfee746d540a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 25 May 2023 00:05:34 +1000 Subject: [PATCH 51/72] feat(nodes): wip inpainting nodes prep --- invokeai/app/invocations/image.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index b25d3735c2..21dfb4c1cd 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -67,16 +67,17 @@ class LoadImageInvocation(BaseInvocation): type: Literal["load_image"] = "load_image" # Inputs - image_type: ImageType = Field(description="The type of the image") - image_name: str = Field(description="The name of the image") + image: Union[ImageField, None] = Field( + default=None, description="The image to load" + ) # fmt: on def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image_type, self.image_name) + image = context.services.images.get_pil_image(self.image.image_type, self.image.image_name) return ImageOutput( image=ImageField( - image_name=self.image_name, - image_type=self.image_type, + image_name=self.image.image_name, + image_type=self.image.image_type, ), width=image.width, height=image.height, @@ -138,7 +139,7 @@ class ImageCropInvocation(BaseInvocation, PILInvocationConfig): image_dto = context.services.images.create( image=image_crop, - image_type=ImageType.INTERMEDIATE, + image_type=ImageType.RESULT, image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, @@ -237,7 +238,7 @@ class MaskFromAlphaInvocation(BaseInvocation, PILInvocationConfig): image_dto = context.services.images.create( image=image_mask, - image_type=ImageType.INTERMEDIATE, + image_type=ImageType.RESULT, image_category=ImageCategory.MASK, node_id=self.id, session_id=context.graph_execution_state_id, @@ -275,7 +276,7 @@ class ImageMultiplyInvocation(BaseInvocation, PILInvocationConfig): image_dto = context.services.images.create( image=multiply_image, - image_type=ImageType.INTERMEDIATE, + image_type=ImageType.RESULT, image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, @@ -313,7 +314,7 @@ class ImageChannelInvocation(BaseInvocation, PILInvocationConfig): image_dto = context.services.images.create( image=channel_image, - image_type=ImageType.INTERMEDIATE, + image_type=ImageType.RESULT, image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, @@ -328,7 +329,8 @@ class ImageChannelInvocation(BaseInvocation, PILInvocationConfig): ) -IMAGE_MODES = Literal['L', 'RGB', 'RGBA', 'CMYK', 'YCbCr', 'LAB', 'HSV', 'I', 'F'] +IMAGE_MODES = Literal["L", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F"] + class ImageConvertInvocation(BaseInvocation, PILInvocationConfig): """Converts an image to a different mode.""" @@ -350,7 +352,7 @@ class ImageConvertInvocation(BaseInvocation, PILInvocationConfig): image_dto = context.services.images.create( image=converted_image, - image_type=ImageType.INTERMEDIATE, + image_type=ImageType.RESULT, image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, @@ -391,7 +393,7 @@ class ImageBlurInvocation(BaseInvocation, PILInvocationConfig): image_dto = context.services.images.create( image=blur_image, - image_type=ImageType.INTERMEDIATE, + image_type=ImageType.RESULT, image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, @@ -431,7 +433,7 @@ class ImageLerpInvocation(BaseInvocation, PILInvocationConfig): image_dto = context.services.images.create( image=lerp_image, - image_type=ImageType.INTERMEDIATE, + image_type=ImageType.RESULT, image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, @@ -476,7 +478,7 @@ class ImageInverseLerpInvocation(BaseInvocation, PILInvocationConfig): image_dto = context.services.images.create( image=ilerp_image, - image_type=ImageType.INTERMEDIATE, + image_type=ImageType.RESULT, image_category=ImageCategory.GENERAL, node_id=self.id, session_id=context.graph_execution_state_id, From 068bbe3a391e78ca92cc32db2984aefbacd95aa8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 25 May 2023 00:07:02 +1000 Subject: [PATCH 52/72] fix(ui): fix uploads tab in gallery --- .../frontend/web/src/features/gallery/store/uploadsSlice.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts index 2864db2660..5e458503ec 100644 --- a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts @@ -14,7 +14,7 @@ export type UploadsImageDTO = Omit & { }; export const uploadsAdapter = createEntityAdapter({ - selectId: (image) => image.image_category, + selectId: (image) => image.image_name, sortComparer: (a, b) => dateComparator(b.created_at, a.created_at), }); From 010f63a50d3a57f31179fb8b20855540b7c3a978 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 25 May 2023 00:07:17 +1000 Subject: [PATCH 53/72] feat(ui): misc tidy --- .../web/src/features/gallery/hooks/useGetImageByName.ts | 2 -- .../features/nodes/util/graphBuilders/buildImageToImageGraph.ts | 1 - .../features/nodes/util/graphBuilders/buildTextToImageGraph.ts | 2 +- invokeai/frontend/web/src/features/nodes/util/parseSchema.ts | 2 +- 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts index d15c3fb51f..ad0870e7a4 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts @@ -13,11 +13,9 @@ const useGetImageByNameSelector = createSelector( const useGetImageByNameAndType = () => { const { allResults, allUploads } = useAppSelector(useGetImageByNameSelector); - return (name: string, type: ImageType) => { if (type === 'results') { const resultImagesResult = allResults[name]; - if (resultImagesResult) { return resultImagesResult; } diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts index 003069ea89..d9eb80d654 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts @@ -1,4 +1,3 @@ -import { v4 as uuidv4 } from 'uuid'; import { RootState } from 'app/store/store'; import { CompelInvocation, diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts index 2fc3ea3975..cbe16abe28 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts @@ -94,6 +94,6 @@ export const buildTextToImageGraph = (state: RootState): Graph => { // Create and add the noise nodes graph = addNoiseNodes(graph, TEXT_TO_LATENTS, state); - console.log(graph); + return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts b/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts index 85b187dca4..ddd19b8749 100644 --- a/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts +++ b/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts @@ -13,7 +13,7 @@ import { buildOutputFieldTemplates, } from './fieldTemplateBuilders'; -const invocationDenylist = ['Graph', 'LoadImage']; +const invocationDenylist = ['Graph']; export const parseSchema = (openAPI: OpenAPIV3.Document) => { // filter out non-invocation schemas, plus some tricky invocations for now From 29c952dcf6d627d2222fa9e9dc9bfba15f48cb9b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 25 May 2023 00:26:22 +1000 Subject: [PATCH 54/72] feat(ui): restore canvas functionality --- .../listeners/invocationComplete.ts | 74 +-- .../frontend/web/src/app/types/invokeai.ts | 1 - .../components/IAICanvasObjectRenderer.tsx | 2 +- .../components/IAICanvasStagingArea.tsx | 2 +- .../IAICanvasStagingAreaToolbar.tsx | 8 +- .../src/features/canvas/store/canvasSlice.ts | 2 +- .../components/CurrentImageButtons.tsx | 8 +- .../OLD_ImageMetadataViewer.tsx | 470 ------------------ .../src/features/system/store/configSlice.ts | 1 - .../web/src/services/events/actions.ts | 1 - .../services/events/util/setEventListeners.ts | 2 - 11 files changed, 28 insertions(+), 543 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/OLD_ImageMetadataViewer.tsx diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts index 3755b38d41..0222eea93c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts @@ -5,6 +5,7 @@ import { imageUrlsReceived, } from 'services/thunks/image'; import { startAppListening } from '..'; +import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; const nodeDenylist = ['dataURL_image']; @@ -24,7 +25,7 @@ export const addImageResultReceivedListener = () => { return; } - const { data, shouldFetchImages } = action.payload; + const { data } = action.payload; const { result, node, graph_execution_state_id } = data; if (isImageOutput(result) && !nodeDenylist.includes(node.type)) { @@ -41,63 +42,20 @@ export const addImageResultReceivedListener = () => { }) ); - // const [x] = await take( - // ( - // action - // ): action is ReturnType => - // imageMetadataReceived.fulfilled.match(action) && - // action.payload.image_name === name - // ); - - // console.log(x); - - // const state = getState(); - - // // if we need to refetch, set URLs to placeholder for now - // const { url, thumbnail } = shouldFetchImages - // ? { url: '', thumbnail: '' } - // : buildImageUrls(type, name); - - // const timestamp = extractTimestampFromImageName(name); - - // const image: Image = { - // name, - // type, - // url, - // thumbnail, - // metadata: { - // created: timestamp, - // width: result.width, - // height: result.height, - // invokeai: { - // session_id: graph_execution_state_id, - // ...(node ? { node } : {}), - // }, - // }, - // }; - - // dispatch(resultAdded(image)); - - // if (state.gallery.shouldAutoSwitchToNewImages) { - // dispatch(imageSelected(image)); - // } - - // if (state.config.shouldFetchImages) { - // dispatch(imageReceived({ imageName: name, imageType: type })); - // dispatch( - // thumbnailReceived({ - // thumbnailName: name, - // thumbnailType: type, - // }) - // ); - // } - - // if ( - // graph_execution_state_id === - // state.canvas.layerState.stagingArea.sessionId - // ) { - // dispatch(addImageToStagingArea(image)); - // } + // Handle canvas image + if ( + graph_execution_state_id === + getState().canvas.layerState.stagingArea.sessionId + ) { + const [{ payload: image }] = await take( + ( + action + ): action is ReturnType => + imageMetadataReceived.fulfilled.match(action) && + action.payload.image_name === image_name + ); + dispatch(addImageToStagingArea(image)); + } } }, }); diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index 9cd2028984..68f7568779 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -346,7 +346,6 @@ export type AppConfig = { /** * Whether or not we need to re-fetch images */ - shouldFetchImages: boolean; disabledTabs: InvokeTabName[]; disabledFeatures: AppFeature[]; disabledSDFeatures: SDFeature[]; diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx index 32d2b36324..c99465cf40 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx @@ -46,7 +46,7 @@ const IAICanvasObjectRenderer = () => { key={i} x={obj.x} y={obj.y} - url={getUrl(obj.image.url)} + url={getUrl(obj.image.image_url)} /> ); } else if (isCanvasBaseLine(obj)) { diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx index f84a5b0e49..e6a8a82ed2 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx @@ -62,7 +62,7 @@ const IAICanvasStagingArea = (props: Props) => { {shouldShowStagingImage && currentStagingAreaImage && ( diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx index eeb51d955b..64c752fce0 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx @@ -157,17 +157,19 @@ const IAICanvasStagingAreaToolbar = () => { } colorScheme="accent" /> - } onClick={() => dispatch( - saveStagingAreaImageToGallery(currentStagingAreaImage.image.url) + saveStagingAreaImageToGallery( + currentStagingAreaImage.image.image_url + ) ) } colorScheme="accent" - /> + /> */} ) => { const image = action.payload; - const { width, height } = image.metadata; + const { width, height } = image; const { stageDimensions } = state; const newBoundingBoxDimensions = { diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx index f7932db0c4..f5265b54db 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx @@ -195,14 +195,14 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { } if (shouldTransformUrls) { - return getUrl(image.url); + return getUrl(image.image_url); } - if (image.url.startsWith('http')) { - return image.url; + if (image.image_url.startsWith('http')) { + return image.image_url; } - return window.location.toString() + image.url; + return window.location.toString() + image.image_url; }; const url = getImageUrl(); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/OLD_ImageMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/OLD_ImageMetadataViewer.tsx deleted file mode 100644 index c76ee7f078..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/OLD_ImageMetadataViewer.tsx +++ /dev/null @@ -1,470 +0,0 @@ -import { ExternalLinkIcon } from '@chakra-ui/icons'; -import { - Box, - Center, - Flex, - Heading, - IconButton, - Link, - Text, - Tooltip, -} from '@chakra-ui/react'; -import * as InvokeAI from 'app/types/invokeai'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { useGetUrl } from 'common/util/getUrl'; -import promptToString from 'common/util/promptToString'; -import { seedWeightsToString } from 'common/util/seedWeightPairs'; -import useSetBothPrompts from 'features/parameters/hooks/usePrompt'; -import { - setCfgScale, - setHeight, - setImg2imgStrength, - // setInitialImage, - setMaskPath, - setPerlin, - setSampler, - setSeamless, - setSeed, - setSeedWeights, - setShouldFitToWidthHeight, - setSteps, - setThreshold, - setWidth, -} from 'features/parameters/store/generationSlice'; -import { - setCodeformerFidelity, - setFacetoolStrength, - setFacetoolType, - setHiresFix, - setUpscalingDenoising, - setUpscalingLevel, - setUpscalingStrength, -} from 'features/parameters/store/postprocessingSlice'; -import { setShouldShowImageDetails } from 'features/ui/store/uiSlice'; -import { memo } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { FaCopy } from 'react-icons/fa'; -import { IoArrowUndoCircleOutline } from 'react-icons/io5'; -import * as png from '@stevebel/png'; - -type MetadataItemProps = { - isLink?: boolean; - label: string; - onClick?: () => void; - value: number | string | boolean; - labelPosition?: string; - withCopy?: boolean; -}; - -/** - * Component to display an individual metadata item or parameter. - */ -const MetadataItem = ({ - label, - value, - onClick, - isLink, - labelPosition, - withCopy = false, -}: MetadataItemProps) => { - const { t } = useTranslation(); - - return ( - - {onClick && ( - - } - size="xs" - variant="ghost" - fontSize={20} - onClick={onClick} - /> - - )} - {withCopy && ( - - } - size="xs" - variant="ghost" - fontSize={14} - onClick={() => navigator.clipboard.writeText(value.toString())} - /> - - )} - - - {label}: - - {isLink ? ( - - {value.toString()} - - ) : ( - - {value.toString()} - - )} - - - ); -}; - -type ImageMetadataViewerProps = { - image: InvokeAI.Image; -}; - -// TODO: I don't know if this is needed. -const memoEqualityCheck = ( - prev: ImageMetadataViewerProps, - next: ImageMetadataViewerProps -) => prev.image.name === next.image.name; - -// TODO: Show more interesting information in this component. - -/** - * Image metadata viewer overlays currently selected image and provides - * access to any of its metadata for use in processing. - */ -const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { - const dispatch = useAppDispatch(); - - const setBothPrompts = useSetBothPrompts(); - - useHotkeys('esc', () => { - dispatch(setShouldShowImageDetails(false)); - }); - - const metadata = image?.metadata.sd_metadata || {}; - const dreamPrompt = image?.metadata.sd_metadata?.dreamPrompt; - - const { - cfg_scale, - fit, - height, - hires_fix, - init_image_path, - mask_image_path, - orig_path, - perlin, - postprocessing, - prompt, - sampler, - seamless, - seed, - steps, - strength, - threshold, - type, - variations, - width, - model_weights, - } = metadata; - - const { t } = useTranslation(); - const { getUrl } = useGetUrl(); - - const metadataJSON = JSON.stringify(image, null, 2); - - // fetch(getUrl(image.url)) - // .then((r) => r.arrayBuffer()) - // .then((buffer) => { - // const { text } = png.decode(buffer); - // const metadata = text?.['sd-metadata'] - // ? JSON.parse(text['sd-metadata'] ?? {}) - // : {}; - // console.log(metadata); - // }); - - return ( - - - File: - - {image.url.length > 64 - ? image.url.substring(0, 64).concat('...') - : image.url} - - - - - - - } - size="xs" - variant="ghost" - fontSize={14} - onClick={() => navigator.clipboard.writeText(metadataJSON)} - /> - - Metadata JSON: - - -
{metadataJSON}
-
-
- {Object.keys(metadata).length > 0 ? ( - <> - {type && } - {model_weights && ( - - )} - {['esrgan', 'gfpgan'].includes(type) && ( - - )} - {prompt && ( - setBothPrompts(prompt)} - /> - )} - {seed !== undefined && ( - dispatch(setSeed(seed))} - /> - )} - {threshold !== undefined && ( - dispatch(setThreshold(threshold))} - /> - )} - {perlin !== undefined && ( - dispatch(setPerlin(perlin))} - /> - )} - {sampler && ( - dispatch(setSampler(sampler))} - /> - )} - {steps && ( - dispatch(setSteps(steps))} - /> - )} - {cfg_scale !== undefined && ( - dispatch(setCfgScale(cfg_scale))} - /> - )} - {variations && variations.length > 0 && ( - - dispatch(setSeedWeights(seedWeightsToString(variations))) - } - /> - )} - {seamless && ( - dispatch(setSeamless(seamless))} - /> - )} - {hires_fix && ( - dispatch(setHiresFix(hires_fix))} - /> - )} - {width && ( - dispatch(setWidth(width))} - /> - )} - {height && ( - dispatch(setHeight(height))} - /> - )} - {/* {init_image_path && ( - dispatch(setInitialImage(init_image_path))} - /> - )} */} - {mask_image_path && ( - dispatch(setMaskPath(mask_image_path))} - /> - )} - {type === 'img2img' && strength && ( - dispatch(setImg2imgStrength(strength))} - /> - )} - {fit && ( - dispatch(setShouldFitToWidthHeight(fit))} - /> - )} - {postprocessing && postprocessing.length > 0 && ( - <> - Postprocessing - {postprocessing.map( - ( - postprocess: InvokeAI.PostProcessedImageMetadata, - i: number - ) => { - if (postprocess.type === 'esrgan') { - const { scale, strength, denoise_str } = postprocess; - return ( - - {`${i + 1}: Upscale (ESRGAN)`} - dispatch(setUpscalingLevel(scale))} - /> - - dispatch(setUpscalingStrength(strength)) - } - /> - {denoise_str !== undefined && ( - - dispatch(setUpscalingDenoising(denoise_str)) - } - /> - )} - - ); - } else if (postprocess.type === 'gfpgan') { - const { strength } = postprocess; - return ( - - {`${ - i + 1 - }: Face restoration (GFPGAN)`} - - { - dispatch(setFacetoolStrength(strength)); - dispatch(setFacetoolType('gfpgan')); - }} - /> - - ); - } else if (postprocess.type === 'codeformer') { - const { strength, fidelity } = postprocess; - return ( - - {`${ - i + 1 - }: Face restoration (Codeformer)`} - - { - dispatch(setFacetoolStrength(strength)); - dispatch(setFacetoolType('codeformer')); - }} - /> - {fidelity && ( - { - dispatch(setCodeformerFidelity(fidelity)); - dispatch(setFacetoolType('codeformer')); - }} - /> - )} - - ); - } - } - )} - - )} - {dreamPrompt && ( - - )} - - ) : ( -
- - No metadata available - -
- )} -
- ); -}, memoEqualityCheck); - -ImageMetadataViewer.displayName = 'ImageMetadataViewer'; - -export default ImageMetadataViewer; diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts index 7b3a1b1eea..f8cb3a483c 100644 --- a/invokeai/frontend/web/src/features/system/store/configSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts @@ -5,7 +5,6 @@ import { merge } from 'lodash-es'; export const initialConfigState: AppConfig = { shouldTransformUrls: false, - shouldFetchImages: false, disabledTabs: [], disabledFeatures: [], disabledSDFeatures: [], diff --git a/invokeai/frontend/web/src/services/events/actions.ts b/invokeai/frontend/web/src/services/events/actions.ts index 84268773a9..76bffeaa49 100644 --- a/invokeai/frontend/web/src/services/events/actions.ts +++ b/invokeai/frontend/web/src/services/events/actions.ts @@ -38,7 +38,6 @@ export const invocationStarted = createAction< export const invocationComplete = createAction< BaseSocketPayload & { data: InvocationCompleteEvent; - shouldFetchImages: boolean; } >('socket/invocationComplete'); diff --git a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts index e9356dd271..4431a9fd8b 100644 --- a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts +++ b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts @@ -165,7 +165,6 @@ export const setEventListeners = (arg: SetEventListenersArg) => { const sessionId = data.graph_execution_state_id; const { cancelType, isCancelScheduled } = getState().system; - const { shouldFetchImages } = getState().config; // Handle scheduled cancelation if (cancelType === 'scheduled' && isCancelScheduled) { @@ -176,7 +175,6 @@ export const setEventListeners = (arg: SetEventListenersArg) => { invocationComplete({ data, timestamp: getTimestamp(), - shouldFetchImages, }) ); }); From 1fb307abf41a43363ea37ffb624f8b62b0bf61aa Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 25 May 2023 00:28:04 +1000 Subject: [PATCH 55/72] feat(nodes): restore canvas functionality (non-latents) --- invokeai/app/invocations/generate.py | 146 +++++++++++++-------------- 1 file changed, 71 insertions(+), 75 deletions(-) diff --git a/invokeai/app/invocations/generate.py b/invokeai/app/invocations/generate.py index 3b3e5512c7..aa16243093 100644 --- a/invokeai/app/invocations/generate.py +++ b/invokeai/app/invocations/generate.py @@ -14,14 +14,17 @@ from invokeai.app.models.image import ImageCategory, ImageType from invokeai.app.util.misc import SEED_MAX, get_random_seed from invokeai.backend.generator.inpaint import infill_methods from .baseinvocation import BaseInvocation, InvocationContext, InvocationConfig -from .image import ImageOutput, build_image_output +from .image import ImageOutput from ...backend.generator import Txt2Img, Img2Img, Inpaint, InvokeAIGenerator from ...backend.stable_diffusion import PipelineIntermediateState from ..util.step_callback import stable_diffusion_step_callback SAMPLER_NAME_VALUES = Literal[tuple(InvokeAIGenerator.schedulers())] INFILL_METHODS = Literal[tuple(infill_methods())] -DEFAULT_INFILL_METHOD = 'patchmatch' if 'patchmatch' in get_args(INFILL_METHODS) else 'tile' +DEFAULT_INFILL_METHOD = ( + "patchmatch" if "patchmatch" in get_args(INFILL_METHODS) else "tile" +) + class SDImageInvocation(BaseModel): """Helper class to provide all Stable Diffusion raster image invocations with additional config""" @@ -92,7 +95,7 @@ class TextToImageInvocation(BaseInvocation, SDImageInvocation): # each time it is called. We only need the first one. generate_output = next(outputs) - image_dto = context.services.images_new.create( + image_dto = context.services.images.create( image=generate_output.image, image_type=ImageType.RESULT, image_category=ImageCategory.GENERAL, @@ -100,35 +103,13 @@ class TextToImageInvocation(BaseInvocation, SDImageInvocation): node_id=self.id, ) - # Results are image and seed, unwrap for now and ignore the seed - # TODO: pre-seed? - # TODO: can this return multiple results? Should it? - # image_type = ImageType.RESULT - # image_name = context.services.images.create_name( - # context.graph_execution_state_id, self.id - # ) - - # metadata = context.services.metadata.build_metadata( - # session_id=context.graph_execution_state_id, node=self - # ) - - # context.services.images.save( - # image_type, image_name, generate_output.image, metadata - # ) - - # context.services.images_db.set( - # id=image_name, - # image_type=ImageType.RESULT, - # image_category=ImageCategory.GENERAL, - # session_id=context.graph_execution_state_id, - # node_id=self.id, - # metadata=GeneratedImageOrLatentsMetadata(), - # ) - - return build_image_output( - image_type=image_dto.image_type, - image_name=image_dto.image_name, - image=generate_output.image, + return ImageOutput( + image=ImageField( + image_name=image_dto.image_name, + image_type=image_dto.image_type, + ), + width=image_dto.width, + height=image_dto.height, ) @@ -164,7 +145,7 @@ class ImageToImageInvocation(TextToImageInvocation): image = ( None if self.image is None - else context.services.images.get( + else context.services.images.get_pil_image( self.image.image_type, self.image.image_name ) ) @@ -194,26 +175,23 @@ class ImageToImageInvocation(TextToImageInvocation): # each time it is called. We only need the first one. generator_output = next(outputs) - result_image = generator_output.image - - # Results are image and seed, unwrap for now and ignore the seed - # TODO: pre-seed? - # TODO: can this return multiple results? Should it? - image_type = ImageType.RESULT - image_name = context.services.images.create_name( - context.graph_execution_state_id, self.id + image_dto = context.services.images.create( + image=generator_output.image, + image_type=ImageType.RESULT, + image_category=ImageCategory.GENERAL, + session_id=context.graph_execution_state_id, + node_id=self.id, ) - metadata = context.services.metadata.build_metadata( - session_id=context.graph_execution_state_id, node=self + return ImageOutput( + image=ImageField( + image_name=image_dto.image_name, + image_type=image_dto.image_type, + ), + width=image_dto.width, + height=image_dto.height, ) - context.services.images.save(image_type, image_name, result_image, metadata) - return build_image_output( - image_type=image_type, - image_name=image_name, - image=result_image, - ) class InpaintInvocation(ImageToImageInvocation): """Generates an image using inpaint.""" @@ -223,16 +201,38 @@ class InpaintInvocation(ImageToImageInvocation): # Inputs mask: Union[ImageField, None] = Field(description="The mask") seam_size: int = Field(default=96, ge=1, description="The seam inpaint size (px)") - seam_blur: int = Field(default=16, ge=0, description="The seam inpaint blur radius (px)") + seam_blur: int = Field( + default=16, ge=0, description="The seam inpaint blur radius (px)" + ) seam_strength: float = Field( default=0.75, gt=0, le=1, description="The seam inpaint strength" ) - seam_steps: int = Field(default=30, ge=1, description="The number of steps to use for seam inpaint") - tile_size: int = Field(default=32, ge=1, description="The tile infill method size (px)") - infill_method: INFILL_METHODS = Field(default=DEFAULT_INFILL_METHOD, description="The method used to infill empty regions (px)") - inpaint_width: Optional[int] = Field(default=None, multiple_of=8, gt=0, description="The width of the inpaint region (px)") - inpaint_height: Optional[int] = Field(default=None, multiple_of=8, gt=0, description="The height of the inpaint region (px)") - inpaint_fill: Optional[ColorField] = Field(default=ColorField(r=127, g=127, b=127, a=255), description="The solid infill method color") + seam_steps: int = Field( + default=30, ge=1, description="The number of steps to use for seam inpaint" + ) + tile_size: int = Field( + default=32, ge=1, description="The tile infill method size (px)" + ) + infill_method: INFILL_METHODS = Field( + default=DEFAULT_INFILL_METHOD, + description="The method used to infill empty regions (px)", + ) + inpaint_width: Optional[int] = Field( + default=None, + multiple_of=8, + gt=0, + description="The width of the inpaint region (px)", + ) + inpaint_height: Optional[int] = Field( + default=None, + multiple_of=8, + gt=0, + description="The height of the inpaint region (px)", + ) + inpaint_fill: Optional[ColorField] = Field( + default=ColorField(r=127, g=127, b=127, a=255), + description="The solid infill method color", + ) inpaint_replace: float = Field( default=0.0, ge=0.0, @@ -257,14 +257,14 @@ class InpaintInvocation(ImageToImageInvocation): image = ( None if self.image is None - else context.services.images.get( + else context.services.images.get_pil_image( self.image.image_type, self.image.image_name ) ) mask = ( None if self.mask is None - else context.services.images.get(self.mask.image_type, self.mask.image_name) + else context.services.images.get_pil_image(self.mask.image_type, self.mask.image_name) ) # Handle invalid model parameter @@ -290,23 +290,19 @@ class InpaintInvocation(ImageToImageInvocation): # each time it is called. We only need the first one. generator_output = next(outputs) - result_image = generator_output.image - - # Results are image and seed, unwrap for now and ignore the seed - # TODO: pre-seed? - # TODO: can this return multiple results? Should it? - image_type = ImageType.RESULT - image_name = context.services.images.create_name( - context.graph_execution_state_id, self.id + image_dto = context.services.images.create( + image=generator_output.image, + image_type=ImageType.RESULT, + image_category=ImageCategory.GENERAL, + session_id=context.graph_execution_state_id, + node_id=self.id, ) - metadata = context.services.metadata.build_metadata( - session_id=context.graph_execution_state_id, node=self - ) - - context.services.images.save(image_type, image_name, result_image, metadata) - return build_image_output( - image_type=image_type, - image_name=image_name, - image=result_image, + return ImageOutput( + image=ImageField( + image_name=image_dto.image_name, + image_type=image_dto.image_type, + ), + width=image_dto.width, + height=image_dto.height, ) From ff6b345d45445bea88eaad336841331f09ad4a8b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 25 May 2023 01:00:07 +1000 Subject: [PATCH 56/72] fix(nodes): rebase fixes --- invokeai/app/api/dependencies.py | 5 +++-- invokeai/app/services/invocation_services.py | 4 +--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index 99e0f7238f..dfef5d2176 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -46,7 +46,8 @@ class ApiDependencies: invoker: Invoker = None - def initialize(config, event_handler_id: int, logger: types.ModuleType=logger): + @staticmethod + def initialize(config, event_handler_id: int, logger: Logger = logger): logger.info(f"Internet connectivity is {config.internet_available}") events = FastAPIEventService(event_handler_id) @@ -91,7 +92,7 @@ class ApiDependencies: ), graph_execution_manager=graph_execution_manager, processor=DefaultInvocationProcessor(), - restoration=RestorationServices(config,logger), + restoration=RestorationServices(config, logger), configuration=config, logger=logger, ) diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index 16b603e89f..bcbe95a41f 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -45,7 +45,7 @@ class InvocationServices: graph_execution_manager: ItemStorageABC["GraphExecutionState"], processor: "InvocationProcessorABC", restoration: RestorationServices, - configuration: InvokeAISettings=None, + configuration: InvokeAISettings = None, ): self.model_manager = model_manager self.events = events @@ -58,5 +58,3 @@ class InvocationServices: self.processor = processor self.restoration = restoration self.configuration = configuration - - From 295b98a13c8b38bc2df9f9f12dc7131deba0def8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 25 May 2023 01:17:14 +1000 Subject: [PATCH 57/72] build(nodes): remove outdated metadata test I will add tests for the new service soon --- tests/nodes/test_png_metadata_service.py | 53 ------------------------ 1 file changed, 53 deletions(-) delete mode 100644 tests/nodes/test_png_metadata_service.py diff --git a/tests/nodes/test_png_metadata_service.py b/tests/nodes/test_png_metadata_service.py deleted file mode 100644 index 975e716fa9..0000000000 --- a/tests/nodes/test_png_metadata_service.py +++ /dev/null @@ -1,53 +0,0 @@ -import json -import os - -from PIL import Image, PngImagePlugin - -from invokeai.app.invocations.generate import TextToImageInvocation -from invokeai.app.services.metadata import PngMetadataService - -valid_metadata = { - "session_id": "1", - "node": { - "id": "1", - "type": "txt2img", - "prompt": "dog", - "seed": 178785523, - "steps": 30, - "width": 512, - "height": 512, - "cfg_scale": 7.5, - "scheduler": "lms", - "model": "stable-diffusion-1.5", - }, -} - -metadata_service = PngMetadataService() - - -def test_can_load_and_parse_invokeai_metadata(tmp_path): - raw_metadata = {"session_id": "123", "node": {"id": "456", "type": "test_type"}} - - temp_image = Image.new("RGB", (512, 512)) - temp_image_path = os.path.join(tmp_path, "test.png") - - pnginfo = PngImagePlugin.PngInfo() - pnginfo.add_text("invokeai", json.dumps(raw_metadata)) - - temp_image.save(temp_image_path, pnginfo=pnginfo) - - image = Image.open(temp_image_path) - - loaded_metadata = metadata_service.get_metadata(image) - - assert loaded_metadata is not None - assert raw_metadata == loaded_metadata - - -def test_can_build_invokeai_metadata(): - session_id = valid_metadata["session_id"] - node = TextToImageInvocation(**valid_metadata["node"]) - - metadata = metadata_service.build_metadata(session_id=session_id, node=node) - - assert valid_metadata == metadata From 0bfbda512d8f150b26bb0a00dceeae0c16931c00 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 25 May 2023 01:21:15 +1000 Subject: [PATCH 58/72] build(nodes): remove references to metadata service in tests --- tests/nodes/test_graph_execution_state.py | 1 - tests/nodes/test_invoker.py | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/nodes/test_graph_execution_state.py b/tests/nodes/test_graph_execution_state.py index 3c262cf88e..d4631ec735 100644 --- a/tests/nodes/test_graph_execution_state.py +++ b/tests/nodes/test_graph_execution_state.py @@ -28,7 +28,6 @@ def mock_services(): logger = None, # type: ignore images = None, # type: ignore latents = None, # type: ignore - metadata = None, # type: ignore queue = MemoryInvocationQueue(), graph_library=SqliteItemStorage[LibraryGraph]( filename=sqlite_memory, table_name="graphs" diff --git a/tests/nodes/test_invoker.py b/tests/nodes/test_invoker.py index 66c6b94d6f..80ed427485 100644 --- a/tests/nodes/test_invoker.py +++ b/tests/nodes/test_invoker.py @@ -26,7 +26,6 @@ def mock_services() -> InvocationServices: logger = None, # type: ignore images = None, # type: ignore latents = None, # type: ignore - metadata = None, # type: ignore queue = MemoryInvocationQueue(), graph_library=SqliteItemStorage[LibraryGraph]( filename=sqlite_memory, table_name="graphs" From 6f3c6ddf3f93b510fc3ef10f8891d0a2354c2599 Mon Sep 17 00:00:00 2001 From: Rohan Barar <57999059+KernelGhost@users.noreply.github.com> Date: Sat, 20 May 2023 18:33:32 +1000 Subject: [PATCH 59/72] Update 020_INSTALL_MANUAL.md Corrected a markdown formatting error (missing backtick). --- docs/installation/020_INSTALL_MANUAL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/020_INSTALL_MANUAL.md b/docs/installation/020_INSTALL_MANUAL.md index 657e3f055d..670b62e1ed 100644 --- a/docs/installation/020_INSTALL_MANUAL.md +++ b/docs/installation/020_INSTALL_MANUAL.md @@ -216,7 +216,7 @@ manager, please follow these steps: 9. Run the command-line- or the web- interface: From within INVOKEAI_ROOT, activate the environment - (with `source .venv/bin/activate` or `.venv\scripts\activate), and then run + (with `source .venv/bin/activate` or `.venv\scripts\activate`), and then run the script `invokeai`. If the virtual environment you selected is NOT inside INVOKEAI_ROOT, then you must specify the path to the root directory by adding `--root_dir \path\to\invokeai` to the commands below: From 37cdd91f5de624acec9fbf7caa8d33c37ad920a1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 25 May 2023 10:21:46 +1000 Subject: [PATCH 60/72] fix(nodes): use forward declarations for InvocationServices Also use `TYPE_CHECKING` to get IDE hints. --- invokeai/app/services/invocation_services.py | 60 ++++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index bcbe95a41f..1f910253e5 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -1,18 +1,17 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI Team +from __future__ import annotations from typing import TYPE_CHECKING -from logging import Logger - -from invokeai.app.services.images import ImageService -from invokeai.backend import ModelManager -from .events import EventServiceBase -from .latent_storage import LatentsStorageBase -from .restoration_services import RestorationServices -from .invocation_queue import InvocationQueueABC -from .item_storage import ItemStorageABC -from .config import InvokeAISettings - if TYPE_CHECKING: + from logging import Logger + from invokeai.app.services.images import ImageService + from invokeai.backend import ModelManager + from invokeai.app.services.events import EventServiceBase + from invokeai.app.services.latent_storage import LatentsStorageBase + from invokeai.app.services.restoration_services import RestorationServices + from invokeai.app.services.invocation_queue import InvocationQueueABC + from invokeai.app.services.item_storage import ItemStorageABC + from invokeai.app.services.config import InvokeAISettings from invokeai.app.services.graph import GraphExecutionState, LibraryGraph from invokeai.app.services.invoker import InvocationProcessorABC @@ -20,32 +19,33 @@ if TYPE_CHECKING: class InvocationServices: """Services that can be used by invocations""" - events: EventServiceBase - latents: LatentsStorageBase - queue: InvocationQueueABC - model_manager: ModelManager - restoration: RestorationServices - configuration: InvokeAISettings - images: ImageService + # TODO: Just forward-declared everything due to circular dependencies. Fix structure. + events: "EventServiceBase" + latents: "LatentsStorageBase" + queue: "InvocationQueueABC" + model_manager: "ModelManager" + restoration: "RestorationServices" + configuration: "InvokeAISettings" + images: "ImageService" # NOTE: we must forward-declare any types that include invocations, since invocations can use services - graph_library: ItemStorageABC["LibraryGraph"] - graph_execution_manager: ItemStorageABC["GraphExecutionState"] + graph_library: "ItemStorageABC"["LibraryGraph"] + graph_execution_manager: "ItemStorageABC"["GraphExecutionState"] processor: "InvocationProcessorABC" def __init__( self, - model_manager: ModelManager, - events: EventServiceBase, - logger: Logger, - latents: LatentsStorageBase, - images: ImageService, - queue: InvocationQueueABC, - graph_library: ItemStorageABC["LibraryGraph"], - graph_execution_manager: ItemStorageABC["GraphExecutionState"], + model_manager: "ModelManager", + events: "EventServiceBase", + logger: "Logger", + latents: "LatentsStorageBase", + images: "ImageService", + queue: "InvocationQueueABC", + graph_library: "ItemStorageABC"["LibraryGraph"], + graph_execution_manager: "ItemStorageABC"["GraphExecutionState"], processor: "InvocationProcessorABC", - restoration: RestorationServices, - configuration: InvokeAISettings = None, + restoration: "RestorationServices", + configuration: "InvokeAISettings", ): self.model_manager = model_manager self.events = events From 30004361219a54e4a94b2f364e903f81f904d292 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 25 May 2023 10:22:41 +1000 Subject: [PATCH 61/72] chore(nodes): remove unused imports --- invokeai/app/services/processor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/invokeai/app/services/processor.py b/invokeai/app/services/processor.py index cdd9db85de..9e3b5a0a30 100644 --- a/invokeai/app/services/processor.py +++ b/invokeai/app/services/processor.py @@ -1,10 +1,7 @@ import time import traceback from threading import Event, Thread, BoundedSemaphore -from typing import Any, TypeGuard -from invokeai.app.invocations.image import ImageOutput -from invokeai.app.models.image import ImageType from ..invocations.baseinvocation import InvocationContext from .invocation_queue import InvocationQueueItem from .invoker import InvocationProcessorABC, Invoker From 96adb566336bbdd5c0bef0ba05348debec35f8c4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 25 May 2023 10:23:25 +1000 Subject: [PATCH 62/72] fix(tests): fix missing services in tests; fix ImageField instantiation --- tests/nodes/test_graph_execution_state.py | 1 + tests/nodes/test_invoker.py | 1 + tests/nodes/test_nodes.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/nodes/test_graph_execution_state.py b/tests/nodes/test_graph_execution_state.py index d4631ec735..9f433aa330 100644 --- a/tests/nodes/test_graph_execution_state.py +++ b/tests/nodes/test_graph_execution_state.py @@ -35,6 +35,7 @@ def mock_services(): graph_execution_manager = SqliteItemStorage[GraphExecutionState](filename = sqlite_memory, table_name = 'graph_executions'), processor = DefaultInvocationProcessor(), restoration = None, # type: ignore + configuration = None, # type: ignore ) def invoke_next(g: GraphExecutionState, services: InvocationServices) -> tuple[BaseInvocation, BaseInvocationOutput]: diff --git a/tests/nodes/test_invoker.py b/tests/nodes/test_invoker.py index 80ed427485..6e1dde716c 100644 --- a/tests/nodes/test_invoker.py +++ b/tests/nodes/test_invoker.py @@ -33,6 +33,7 @@ def mock_services() -> InvocationServices: graph_execution_manager = SqliteItemStorage[GraphExecutionState](filename = sqlite_memory, table_name = 'graph_executions'), processor = DefaultInvocationProcessor(), restoration = None, # type: ignore + configuration = None, # type: ignore ) @pytest.fixture() diff --git a/tests/nodes/test_nodes.py b/tests/nodes/test_nodes.py index e334953d7e..d16d67d815 100644 --- a/tests/nodes/test_nodes.py +++ b/tests/nodes/test_nodes.py @@ -49,7 +49,7 @@ class ImageTestInvocation(BaseInvocation): prompt: str = Field(default = "") def invoke(self, context: InvocationContext) -> ImageTestInvocationOutput: - return ImageTestInvocationOutput(image=ImageField(image_name=self.id, width=512, height=512, mode="", info={})) + return ImageTestInvocationOutput(image=ImageField(image_name=self.id)) class PromptCollectionTestInvocationOutput(BaseInvocationOutput): type: Literal['test_prompt_collection_output'] = 'test_prompt_collection_output' From ee0c6ad86e9ea82a576a94d64f9b1db8d309c95e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 25 May 2023 10:47:45 +1000 Subject: [PATCH 63/72] fix(cli): fix invocation services for cli --- invokeai/app/cli_app.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/invokeai/app/cli_app.py b/invokeai/app/cli_app.py index 073a8f569b..c2fbdfda40 100644 --- a/invokeai/app/cli_app.py +++ b/invokeai/app/cli_app.py @@ -13,10 +13,13 @@ from typing import ( from pydantic import BaseModel, ValidationError from pydantic.fields import Field +from invokeai.app.services.image_record_storage import SqliteImageRecordStorage +from invokeai.app.services.images import ImageService +from invokeai.app.services.metadata import CoreMetadataService +from invokeai.app.services.urls import LocalUrlService import invokeai.backend.util.logging as logger -from invokeai.app.services.metadata import PngMetadataService from .services.default_graphs import create_system_graphs from .services.latent_storage import DiskLatentsStorage, ForwardCacheLatentsStorage @@ -188,6 +191,9 @@ def invoke_all(context: CliContext): raise SessionError() +logger = logger.InvokeAILogger.getLogger() + + def invoke_cli(): # this gets the basic configuration config = get_invokeai_config() @@ -206,24 +212,38 @@ def invoke_cli(): events = EventServiceBase() output_folder = config.output_path - metadata = PngMetadataService() # TODO: build a file/path manager? db_location = os.path.join(output_folder, "invokeai.db") + graph_execution_manager = SqliteItemStorage[GraphExecutionState]( + filename=db_location, table_name="graph_executions" + ) + + urls = LocalUrlService() + metadata = CoreMetadataService() + image_record_storage = SqliteImageRecordStorage(db_location) + image_file_storage = DiskImageFileStorage(f"{output_folder}/images") + + images = ImageService( + image_record_storage=image_record_storage, + image_file_storage=image_file_storage, + metadata=metadata, + url=urls, + logger=logger, + graph_execution_manager=graph_execution_manager, + ) + services = InvocationServices( model_manager=model_manager, events=events, latents = ForwardCacheLatentsStorage(DiskLatentsStorage(f'{output_folder}/latents')), - images=DiskImageFileStorage(f'{output_folder}/images', metadata_service=metadata), - metadata=metadata, + images=images, queue=MemoryInvocationQueue(), graph_library=SqliteItemStorage[LibraryGraph]( filename=db_location, table_name="graphs" ), - graph_execution_manager=SqliteItemStorage[GraphExecutionState]( - filename=db_location, table_name="graph_executions" - ), + graph_execution_manager=graph_execution_manager, processor=DefaultInvocationProcessor(), restoration=RestorationServices(config,logger=logger), logger=logger, From d22ebe08beb4fc1e4bf287f675fa91494203e677 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 25 May 2023 11:03:14 +1000 Subject: [PATCH 64/72] fix(tests): log db_location --- invokeai/app/cli_app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/invokeai/app/cli_app.py b/invokeai/app/cli_app.py index c2fbdfda40..c6c7320d79 100644 --- a/invokeai/app/cli_app.py +++ b/invokeai/app/cli_app.py @@ -225,6 +225,8 @@ def invoke_cli(): image_record_storage = SqliteImageRecordStorage(db_location) image_file_storage = DiskImageFileStorage(f"{output_folder}/images") + logger.info(f'InvokeAI database location is "{db_location}"') + images = ImageService( image_record_storage=image_record_storage, image_file_storage=image_file_storage, From ad619ae880b61b5979f6e9617ae2969a2e7797ac Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 25 May 2023 11:12:22 +1000 Subject: [PATCH 65/72] fix(tests): log db_location --- invokeai/app/cli_app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/app/cli_app.py b/invokeai/app/cli_app.py index c6c7320d79..967a79bfd6 100644 --- a/invokeai/app/cli_app.py +++ b/invokeai/app/cli_app.py @@ -216,6 +216,8 @@ def invoke_cli(): # TODO: build a file/path manager? db_location = os.path.join(output_folder, "invokeai.db") + logger.info(f'InvokeAI database location is "{db_location}"') + graph_execution_manager = SqliteItemStorage[GraphExecutionState]( filename=db_location, table_name="graph_executions" ) @@ -225,8 +227,6 @@ def invoke_cli(): image_record_storage = SqliteImageRecordStorage(db_location) image_file_storage = DiskImageFileStorage(f"{output_folder}/images") - logger.info(f'InvokeAI database location is "{db_location}"') - images = ImageService( image_record_storage=image_record_storage, image_file_storage=image_file_storage, From 3829ffbe6693da2f55631f8fa52569a563650130 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 25 May 2023 11:53:02 +1000 Subject: [PATCH 66/72] fix(tests): add `--use_memory_db` flag; use it in tests --- .github/workflows/test-invoke-pip.yml | 1 + invokeai/app/cli_app.py | 5 ++++- invokeai/app/services/config.py | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-invoke-pip.yml b/.github/workflows/test-invoke-pip.yml index 17673de937..071232e06e 100644 --- a/.github/workflows/test-invoke-pip.yml +++ b/.github/workflows/test-invoke-pip.yml @@ -125,6 +125,7 @@ jobs: --no-nsfw_checker --precision=float32 --always_use_cpu + --use_memory_db --outdir ${{ env.INVOKEAI_OUTDIR }}/${{ matrix.python-version }}/${{ matrix.pytorch }} --from_file ${{ env.TEST_PROMPTS }} diff --git a/invokeai/app/cli_app.py b/invokeai/app/cli_app.py index 967a79bfd6..de543d2d85 100644 --- a/invokeai/app/cli_app.py +++ b/invokeai/app/cli_app.py @@ -214,7 +214,10 @@ def invoke_cli(): output_folder = config.output_path # TODO: build a file/path manager? - db_location = os.path.join(output_folder, "invokeai.db") + if config.use_memory_db: + db_location = ":memory:" + else: + db_location = os.path.join(output_folder, "invokeai.db") logger.info(f'InvokeAI database location is "{db_location}"') diff --git a/invokeai/app/services/config.py b/invokeai/app/services/config.py index 2d87125744..49e0b6bed4 100644 --- a/invokeai/app/services/config.py +++ b/invokeai/app/services/config.py @@ -353,6 +353,7 @@ setting environment variables INVOKEAI_. sequential_guidance : bool = Field(default=False, description="Whether to calculate guidance in serial instead of in parallel, lowering memory requirements", category='Memory/Performance') xformers_enabled : bool = Field(default=True, description="Enable/disable memory-efficient attention", category='Memory/Performance') + root : Path = Field(default=_find_root(), description='InvokeAI runtime root directory', category='Paths') autoconvert_dir : Path = Field(default=None, description='Path to a directory of ckpt files to be converted into diffusers and imported on startup.', category='Paths') conf_path : Path = Field(default='configs/models.yaml', description='Path to models definition file', category='Paths') @@ -362,6 +363,7 @@ setting environment variables INVOKEAI_. lora_dir : Path = Field(default='loras', description='Path to InvokeAI LoRA model directory', category='Paths') outdir : Path = Field(default='outputs', description='Default folder for output images', category='Paths') from_file : Path = Field(default=None, description='Take command input from the indicated file (command-line client only)', category='Paths') + use_memory_db : bool = Field(default=False, description='Use in-memory database for storing image metadata', category='Paths') model : str = Field(default='stable-diffusion-1.5', description='Initial model name', category='Models') embeddings : bool = Field(default=True, description='Load contents of embeddings directory', category='Models') @@ -511,7 +513,7 @@ class PagingArgumentParser(argparse.ArgumentParser): text = self.format_help() pydoc.pager(text) -def get_invokeai_config(cls:Type[InvokeAISettings]=InvokeAIAppConfig,**kwargs)->InvokeAISettings: +def get_invokeai_config(cls:Type[InvokeAISettings]=InvokeAIAppConfig,**kwargs)->InvokeAIAppConfig: ''' This returns a singleton InvokeAIAppConfig configuration object. ''' From 1e94d7739a59f295e958bf1bf541914fcf13cc81 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Thu, 25 May 2023 14:47:16 -0400 Subject: [PATCH 67/72] fix metadata references, add support for negative_conditioning syntax --- .../components/CurrentImageButtons.tsx | 23 +++++++++++-------- .../gallery/components/HoverableImage.tsx | 5 +++- .../ImageMetadataViewer.tsx | 12 ---------- .../graphBuilders/buildTextToImageGraph.ts | 2 +- .../parameters/hooks/useParameters.ts | 13 ++++++----- .../features/parameters/hooks/usePrompt.ts | 15 ++++-------- 6 files changed, 30 insertions(+), 40 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx index f5265b54db..c19a404a37 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx @@ -109,8 +109,9 @@ const currentImageButtonsSelector = createSelector( isLightboxOpen, shouldHidePreview, image: selectedImage, - seed: selectedImage?.metadata?.invokeai?.node?.seed, - prompt: selectedImage?.metadata?.invokeai?.node?.prompt, + seed: selectedImage?.metadata?.seed, + prompt: selectedImage?.metadata?.positive_conditioning, + negativePrompt: selectedImage?.metadata?.negative_conditioning, }; }, { @@ -245,13 +246,16 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { ); const handleUseSeed = useCallback(() => { - recallSeed(image?.metadata?.invokeai?.node?.seed); + recallSeed(image?.metadata?.seed); }, [image, recallSeed]); useHotkeys('s', handleUseSeed, [image]); const handleUsePrompt = useCallback(() => { - recallPrompt(image?.metadata?.invokeai?.node?.prompt); + recallPrompt( + image?.metadata?.positive_conditioning, + image?.metadata?.negative_conditioning + ); }, [image, recallPrompt]); useHotkeys('p', handleUsePrompt, [image]); @@ -454,7 +458,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { {t('parameters.copyImageToLink')} - + } size="sm" w="100%"> {t('parameters.downloadImage')} @@ -500,7 +504,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { icon={} tooltip={`${t('parameters.usePrompt')} (P)`} aria-label={`${t('parameters.usePrompt')} (P)`} - isDisabled={!image?.metadata?.invokeai?.node?.prompt} + isDisabled={!image?.metadata?.positive_conditioning} onClick={handleUsePrompt} /> @@ -508,7 +512,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { icon={} tooltip={`${t('parameters.useSeed')} (S)`} aria-label={`${t('parameters.useSeed')} (S)`} - isDisabled={!image?.metadata?.invokeai?.node?.seed} + isDisabled={!image?.metadata?.seed} onClick={handleUseSeed} /> @@ -517,9 +521,8 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { tooltip={`${t('parameters.useAll')} (A)`} aria-label={`${t('parameters.useAll')} (A)`} isDisabled={ - !['txt2img', 'img2img', 'inpaint'].includes( - String(image?.metadata?.invokeai?.node?.type) - ) + // not sure what this list should be + !['t2l', 'l2l', 'inpaint'].includes(String(image?.metadata?.type)) } onClick={handleClickUseAllParameters} /> diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx index 04fecac463..fccfba0a70 100644 --- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx @@ -155,7 +155,10 @@ const HoverableImage = memo((props: HoverableImageProps) => { // Recall parameters handlers const handleRecallPrompt = useCallback(() => { - recallPrompt(image.metadata?.positive_conditioning); + recallPrompt( + image.metadata?.positive_conditioning, + image.metadata?.negative_conditioning + ); }, [image, recallPrompt]); const handleRecallSeed = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx index b4bf9a6d25..b01191105e 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx @@ -8,29 +8,20 @@ import { Text, Tooltip, } from '@chakra-ui/react'; -import * as InvokeAI from 'app/types/invokeai'; import { useAppDispatch } from 'app/store/storeHooks'; import { useGetUrl } from 'common/util/getUrl'; import promptToString from 'common/util/promptToString'; -import { seedWeightsToString } from 'common/util/seedWeightPairs'; -import useSetBothPrompts from 'features/parameters/hooks/usePrompt'; import { setCfgScale, setHeight, setImg2imgStrength, setNegativePrompt, - setPerlin, setPositivePrompt, setScheduler, - setSeamless, setSeed, - setSeedWeights, - setShouldFitToWidthHeight, setSteps, - setThreshold, setWidth, } from 'features/parameters/store/generationSlice'; -import { setHiresFix } from 'features/parameters/store/postprocessingSlice'; import { setShouldShowImageDetails } from 'features/ui/store/uiSlice'; import { memo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -39,7 +30,6 @@ import { FaCopy } from 'react-icons/fa'; import { IoArrowUndoCircleOutline } from 'react-icons/io5'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import { ImageDTO } from 'services/api'; -import { filter } from 'lodash-es'; import { Scheduler } from 'app/constants'; type MetadataItemProps = { @@ -126,8 +116,6 @@ const memoEqualityCheck = ( const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { const dispatch = useAppDispatch(); - const setBothPrompts = useSetBothPrompts(); - useHotkeys('esc', () => { dispatch(setShouldShowImageDetails(false)); }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts index cbe16abe28..51f89e8f74 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts @@ -11,7 +11,7 @@ import { addNoiseNodes } from '../nodeBuilders/addNoiseNodes'; const POSITIVE_CONDITIONING = 'positive_conditioning'; const NEGATIVE_CONDITIONING = 'negative_conditioning'; const TEXT_TO_LATENTS = 'text_to_latents'; -const LATENTS_TO_IMAGE = 'latnets_to_image'; +const LATENTS_TO_IMAGE = 'latents_to_image'; /** * Builds the Text to Image tab graph. diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts b/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts index ad9985b5de..27ae63e5dd 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts @@ -21,8 +21,8 @@ export const useParameters = () => { * Sets prompt with toast */ const recallPrompt = useCallback( - (prompt: unknown) => { - if (!isString(prompt)) { + (prompt: unknown, negativePrompt?: unknown) => { + if (!isString(prompt) || !isString(negativePrompt)) { toaster({ title: t('toast.promptNotSet'), description: t('toast.promptNotSetDesc'), @@ -33,7 +33,7 @@ export const useParameters = () => { return; } - setBothPrompts(prompt); + setBothPrompts(prompt, negativePrompt); toaster({ title: t('toast.promptSet'), status: 'info', @@ -112,12 +112,13 @@ export const useParameters = () => { const recallAllParameters = useCallback( (image: ImageDTO | undefined) => { const type = image?.metadata?.type; - if (['txt2img', 'img2img', 'inpaint'].includes(String(type))) { + // not sure what this list should be + if (['t2l', 'l2l', 'inpaint'].includes(String(type))) { dispatch(allParametersSet(image)); - if (image?.metadata?.type === 'img2img') { + if (image?.metadata?.type === 'l2l') { dispatch(setActiveTab('img2img')); - } else if (image?.metadata?.type === 'txt2img') { + } else if (image?.metadata?.type === 't2l') { dispatch(setActiveTab('txt2img')); } diff --git a/invokeai/frontend/web/src/features/parameters/hooks/usePrompt.ts b/invokeai/frontend/web/src/features/parameters/hooks/usePrompt.ts index 2a6a832720..cb39be6e1e 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/usePrompt.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/usePrompt.ts @@ -12,16 +12,11 @@ const useSetBothPrompts = () => { const dispatch = useAppDispatch(); return useCallback( - (inputPrompt: InvokeAI.Prompt) => { - const promptString = - typeof inputPrompt === 'string' - ? inputPrompt - : promptToString(inputPrompt); - - const [prompt, negativePrompt] = getPromptAndNegative(promptString); - - dispatch(setPositivePrompt(prompt)); - dispatch(setNegativePrompt(negativePrompt)); + (inputPrompt: InvokeAI.Prompt, negativePrompt?: InvokeAI.Prompt) => { + dispatch(setPositivePrompt(inputPrompt)); + if (negativePrompt) { + dispatch(setNegativePrompt(negativePrompt)); + } }, [dispatch] ); From a4c44edf8d0393bad37e378636e6bd9f6c672dc4 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Thu, 25 May 2023 15:17:02 -0400 Subject: [PATCH 68/72] more use parameter fixes --- .../gallery/components/HoverableImage.tsx | 3 +- .../features/parameters/hooks/usePrompt.ts | 6 ++-- .../store/setAllParametersReducer.ts | 35 +++++++++++++------ 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx index fccfba0a70..ed427f4984 100644 --- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx @@ -251,7 +251,8 @@ const HoverableImage = memo((props: HoverableImageProps) => { icon={} onClickCapture={handleUseAllParameters} isDisabled={ - !['txt2img', 'img2img', 'inpaint'].includes( + // what should these be + !['t2l', 'l2l', 'inpaint'].includes( String(image?.metadata?.type) ) } diff --git a/invokeai/frontend/web/src/features/parameters/hooks/usePrompt.ts b/invokeai/frontend/web/src/features/parameters/hooks/usePrompt.ts index cb39be6e1e..3fee0bcdd8 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/usePrompt.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/usePrompt.ts @@ -12,11 +12,9 @@ const useSetBothPrompts = () => { const dispatch = useAppDispatch(); return useCallback( - (inputPrompt: InvokeAI.Prompt, negativePrompt?: InvokeAI.Prompt) => { + (inputPrompt: InvokeAI.Prompt, negativePrompt: InvokeAI.Prompt) => { dispatch(setPositivePrompt(inputPrompt)); - if (negativePrompt) { - dispatch(setNegativePrompt(negativePrompt)); - } + dispatch(setNegativePrompt(negativePrompt)); }, [dispatch] ); diff --git a/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts b/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts index d6d1af0f8e..8f06c7d0ef 100644 --- a/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts +++ b/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts @@ -7,19 +7,29 @@ export const setAllParametersReducer = ( state: Draft, action: PayloadAction ) => { - const node = action.payload?.metadata.invokeai?.node; + const metadata = action.payload?.metadata; - if (!node) { + if (!metadata) { return; } + // not sure what this list should be if ( - node.type === 'txt2img' || - node.type === 'img2img' || - node.type === 'inpaint' + metadata.type === 't2l' || + metadata.type === 'l2l' || + metadata.type === 'inpaint' ) { - const { cfg_scale, height, model, prompt, scheduler, seed, steps, width } = - node; + const { + cfg_scale, + height, + model, + positive_conditioning, + negative_conditioning, + scheduler, + seed, + steps, + width, + } = metadata; if (cfg_scale !== undefined) { state.cfgScale = Number(cfg_scale); @@ -30,8 +40,11 @@ export const setAllParametersReducer = ( if (model !== undefined) { state.model = String(model); } - if (prompt !== undefined) { - state.positivePrompt = String(prompt); + if (positive_conditioning !== undefined) { + state.positivePrompt = String(positive_conditioning); + } + if (negative_conditioning !== undefined) { + state.negativePrompt = String(negative_conditioning); } if (scheduler !== undefined) { const schedulerString = String(scheduler); @@ -51,8 +64,8 @@ export const setAllParametersReducer = ( } } - if (node.type === 'img2img') { - const { fit, image } = node as ImageToImageInvocation; + if (metadata.type === 'l2l') { + const { fit, image } = metadata as ImageToImageInvocation; if (fit !== undefined) { state.shouldFitToWidthHeight = Boolean(fit); From 93bb27f2c74a908d59d04ce3f4b771c09bc75e16 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Thu, 25 May 2023 16:56:39 -0400 Subject: [PATCH 69/72] fix gallery navigation --- .../src/features/gallery/components/NextPrevImageButtons.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx index d0d25f8bc6..fcf8359187 100644 --- a/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx @@ -33,7 +33,7 @@ export const nextPrevImageButtonsSelector = createSelector( } const currentImageIndex = state[currentCategory].ids.findIndex( - (i) => i === selectedImage.name + (i) => i === selectedImage.image_name ); const nextImageIndex = clamp( From d98868e524651917e21565db061f6253ff9a834d Mon Sep 17 00:00:00 2001 From: Kent Keirsey <31807370+hipsterusername@users.noreply.github.com> Date: Wed, 24 May 2023 16:42:49 -0400 Subject: [PATCH 70/72] Update generationSlice.ts to change Default Scheduler --- .../web/src/features/parameters/store/generationSlice.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index f5054f1969..849f848ff3 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -52,7 +52,7 @@ export const initialGenerationState: GenerationState = { perlin: 0, positivePrompt: '', negativePrompt: '', - scheduler: 'lms', + scheduler: 'euler', seamBlur: 16, seamSize: 96, seamSteps: 30, From d4acd49ee346a642b0111687a3250ecbd4326e17 Mon Sep 17 00:00:00 2001 From: Kent Keirsey <31807370+hipsterusername@users.noreply.github.com> Date: Wed, 24 May 2023 16:45:21 -0400 Subject: [PATCH 71/72] Update generate.py --- invokeai/app/invocations/generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/invocations/generate.py b/invokeai/app/invocations/generate.py index aa16243093..6af959a5bd 100644 --- a/invokeai/app/invocations/generate.py +++ b/invokeai/app/invocations/generate.py @@ -56,7 +56,7 @@ class TextToImageInvocation(BaseInvocation, SDImageInvocation): width: int = Field(default=512, multiple_of=8, gt=0, description="The width of the resulting image", ) height: int = Field(default=512, multiple_of=8, gt=0, description="The height of the resulting image", ) cfg_scale: float = Field(default=7.5, ge=1, description="The Classifier-Free Guidance, higher values may result in a result closer to the prompt", ) - scheduler: SAMPLER_NAME_VALUES = Field(default="lms", description="The scheduler to use" ) + scheduler: SAMPLER_NAME_VALUES = Field(default="euler", description="The scheduler to use" ) model: str = Field(default="", description="The model to use (currently ignored)") # fmt: on From 05fb0ac2b29026ca39664e17a683027cb99d8ea2 Mon Sep 17 00:00:00 2001 From: Kent Keirsey <31807370+hipsterusername@users.noreply.github.com> Date: Wed, 24 May 2023 16:47:47 -0400 Subject: [PATCH 72/72] Update latent.py --- invokeai/app/invocations/latent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 34da76d39a..12cebdf41d 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -167,7 +167,7 @@ class TextToLatentsInvocation(BaseInvocation): noise: Optional[LatentsField] = Field(description="The noise to use") steps: int = Field(default=10, gt=0, description="The number of steps to use to generate the image") cfg_scale: float = Field(default=7.5, gt=0, description="The Classifier-Free Guidance, higher values may result in a result closer to the prompt", ) - scheduler: SAMPLER_NAME_VALUES = Field(default="lms", description="The scheduler to use" ) + scheduler: SAMPLER_NAME_VALUES = Field(default="euler", description="The scheduler to use" ) model: str = Field(default="", description="The model to use (currently ignored)") # seamless: bool = Field(default=False, description="Whether or not to generate an image that can tile without seams", ) # seamless_axes: str = Field(default="", description="The axes to tile the image on, 'x' and/or 'y'")