From 2f6964bfa5efaf59d029f41a73ffb4ed63abeef5 Mon Sep 17 00:00:00 2001 From: Jennifer Player Date: Tue, 5 Mar 2024 22:57:05 -0500 Subject: [PATCH] fetching model image, still not working --- invokeai/app/api/dependencies.py | 5 + invokeai/app/api/routers/model_manager.py | 98 ++++++++++- invokeai/app/services/invocation_services.py | 3 + .../model_images/model_images_base.py | 22 +-- .../model_images/model_images_common.py | 6 +- ...images_disk.py => model_images_default.py} | 45 +++--- invokeai/app/services/urls/urls_base.py | 5 + invokeai/app/services/urls/urls_default.py | 3 + invokeai/backend/model_manager/config.py | 5 + .../ModelPanel/Fields/ModelImage.tsx | 73 +++++++++ .../ModelPanel/Fields/ModelImageUpload.tsx | 27 ++-- .../subpanels/ModelPanel/ModelEdit.tsx | 42 ++++- .../web/src/services/api/endpoints/models.ts | 28 +++- .../frontend/web/src/services/api/schema.ts | 152 +++++++++++++++++- 14 files changed, 461 insertions(+), 53 deletions(-) rename invokeai/app/services/model_images/{model_images_disk.py => model_images_default.py} (60%) create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImage.tsx diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index c26122cc77..149b267956 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -25,6 +25,7 @@ from ..services.invocation_cache.invocation_cache_memory import MemoryInvocation from ..services.invocation_services import InvocationServices from ..services.invocation_stats.invocation_stats_default import InvocationStatsService from ..services.invoker import Invoker +from ..services.model_images.model_images_default import ModelImagesService from ..services.model_manager.model_manager_default import ModelManagerService from ..services.model_records import ModelRecordServiceSQL from ..services.names.names_default import SimpleNameService @@ -71,6 +72,8 @@ class ApiDependencies: image_files = DiskImageFileStorage(f"{output_folder}/images") + model_images_folder = config.models_path + db = init_db(config=config, logger=logger, image_files=image_files) configuration = config @@ -92,6 +95,7 @@ class ApiDependencies: ObjectSerializerDisk[ConditioningFieldData](output_folder / "conditioning", ephemeral=True) ) download_queue_service = DownloadQueueService(event_bus=events) + model_images_service = ModelImagesService(model_images_folder / "model_images") model_manager = ModelManagerService.build_model_manager( app_config=configuration, model_record_service=ModelRecordServiceSQL(db=db), @@ -118,6 +122,7 @@ class ApiDependencies: images=images, invocation_cache=invocation_cache, logger=logger, + model_images=model_images_service, model_manager=model_manager, download_queue=download_queue_service, names=names, diff --git a/invokeai/app/api/routers/model_manager.py b/invokeai/app/api/routers/model_manager.py index 0ab1ac241a..41ee9767a8 100644 --- a/invokeai/app/api/routers/model_manager.py +++ b/invokeai/app/api/routers/model_manager.py @@ -1,12 +1,16 @@ # Copyright (c) 2023 Lincoln D. Stein """FastAPI route for model configuration records.""" +import io import pathlib import shutil +import traceback from typing import Any, Dict, List, Optional -from fastapi import Body, Path, Query, Response +from fastapi import Body, Path, Query, Response, UploadFile +from fastapi.responses import FileResponse from fastapi.routing import APIRouter +from PIL import Image from pydantic import BaseModel, ConfigDict, Field from starlette.exceptions import HTTPException from typing_extensions import Annotated @@ -31,6 +35,9 @@ from ..dependencies import ApiDependencies model_manager_router = APIRouter(prefix="/v2/models", tags=["model_manager"]) +# images are immutable; set a high max-age +IMAGE_MAX_AGE = 31536000 + class ModelsList(BaseModel): """Return list of configs.""" @@ -72,7 +79,7 @@ example_model_input = { "description": "Model description", "vae": None, "variant": "normal", - "image": "blob" + "image": "blob", } ############################################################################## @@ -267,6 +274,93 @@ async def update_model_record( return model_response +@model_manager_router.get( + "/i/{key}/image", + operation_id="get_model_image", + responses={ + 200: { + "description": "The model image was fetched successfully", + }, + 400: {"description": "Bad request"}, + 404: {"description": "The model could not be found"}, + }, + status_code=200, +) +async def get_model_image( + key: str = Path(description="The name of model image file to get"), +) -> FileResponse: + """Gets a full-resolution image file""" + + try: + path = ApiDependencies.invoker.services.model_images.get_path(key + ".png") + + if not path: + raise HTTPException(status_code=404) + + response = FileResponse( + path, + media_type="image/png", + filename=key + ".png", + content_disposition_type="inline", + ) + response.headers["Cache-Control"] = f"max-age={IMAGE_MAX_AGE}" + return response + except Exception: + raise HTTPException(status_code=404) + + +# async def get_model_image( +# key: Annotated[str, Path(description="Unique key of model")], +# ) -> Optional[str]: +# model_images = ApiDependencies.invoker.services.model_images +# try: +# url = model_images.get_url(key) + +# if not url: +# return None + +# return url +# except Exception: +# raise HTTPException(status_code=404) + + +@model_manager_router.patch( + "/i/{key}/image", + operation_id="update_model_image", + responses={ + 200: { + "description": "The model image was updated successfully", + }, + 400: {"description": "Bad request"}, + }, + status_code=200, +) +async def update_model_image( + key: Annotated[str, Path(description="Unique key of model")], + image: UploadFile, +) -> None: + if not image.content_type or not image.content_type.startswith("image"): + raise HTTPException(status_code=415, detail="Not an image") + + contents = await image.read() + try: + pil_image = Image.open(io.BytesIO(contents)) + + except Exception: + ApiDependencies.invoker.services.logger.error(traceback.format_exc()) + raise HTTPException(status_code=415, detail="Failed to read image") + + logger = ApiDependencies.invoker.services.logger + model_images = ApiDependencies.invoker.services.model_images + try: + model_images.save(pil_image, key) + logger.info(f"Updated image for model: {key}") + except ValueError as e: + logger.error(str(e)) + raise HTTPException(status_code=409, detail=str(e)) + return + + @model_manager_router.delete( "/i/{key}", operation_id="delete_model", diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index a560696692..fc79b02b9c 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -25,6 +25,7 @@ if TYPE_CHECKING: from .images.images_base import ImageServiceABC from .invocation_cache.invocation_cache_base import InvocationCacheBase from .invocation_stats.invocation_stats_base import InvocationStatsServiceBase + from .model_images.model_images_base import ModelImagesBase from .model_manager.model_manager_base import ModelManagerServiceBase from .names.names_base import NameServiceBase from .session_processor.session_processor_base import SessionProcessorBase @@ -49,6 +50,7 @@ class InvocationServices: image_files: "ImageFileStorageBase", image_records: "ImageRecordStorageBase", logger: "Logger", + model_images: "ModelImagesBase", model_manager: "ModelManagerServiceBase", download_queue: "DownloadQueueServiceBase", performance_statistics: "InvocationStatsServiceBase", @@ -72,6 +74,7 @@ class InvocationServices: self.image_files = image_files self.image_records = image_records self.logger = logger + self.model_images = model_images self.model_manager = model_manager self.download_queue = download_queue self.performance_statistics = performance_statistics diff --git a/invokeai/app/services/model_images/model_images_base.py b/invokeai/app/services/model_images/model_images_base.py index 624cdd8216..890ff2b864 100644 --- a/invokeai/app/services/model_images/model_images_base.py +++ b/invokeai/app/services/model_images/model_images_base.py @@ -3,30 +3,34 @@ from pathlib import Path from PIL.Image import Image as PILImageType - class ModelImagesBase(ABC): """Low-level service responsible for storing and retrieving image files.""" @abstractmethod - def get(self, image_name: str) -> PILImageType: - """Retrieves an image as PIL Image.""" + def get(self, model_key: str) -> PILImageType: + """Retrieves a model image as PIL Image.""" pass @abstractmethod - def get_path(self, image_name: str) -> Path: - """Gets the internal path to an image.""" + def get_path(self, model_key: str) -> Path: + """Gets the internal path to a model image.""" + pass + + @abstractmethod + def get_url(self, model_key: str) -> str: + """Gets the url for a model image.""" pass @abstractmethod def save( self, image: PILImageType, - image_name: str, + model_key: str, ) -> None: - """Saves an image. Returns a tuple of the image name and created timestamp.""" + """Saves a model image. Returns a tuple of the image name and created timestamp.""" pass @abstractmethod - def delete(self, image_name: str) -> None: - """Deletes an image.""" + def delete(self, model_key: str) -> None: + """Deletes a model image.""" pass diff --git a/invokeai/app/services/model_images/model_images_common.py b/invokeai/app/services/model_images/model_images_common.py index e63b780261..0d856f2dfe 100644 --- a/invokeai/app/services/model_images/model_images_common.py +++ b/invokeai/app/services/model_images/model_images_common.py @@ -2,19 +2,19 @@ class ModelImageFileNotFoundException(Exception): """Raised when an image file is not found in storage.""" - def __init__(self, message="Image file not found"): + def __init__(self, message="Model image file not found"): super().__init__(message) class ModelImageFileSaveException(Exception): """Raised when an image cannot be saved.""" - def __init__(self, message="Image file not saved"): + def __init__(self, message="Model image file not saved"): super().__init__(message) class ModelImageFileDeleteException(Exception): """Raised when an image cannot be deleted.""" - def __init__(self, message="Image file not deleted"): + def __init__(self, message="Model image file not deleted"): super().__init__(message) diff --git a/invokeai/app/services/model_images/model_images_disk.py b/invokeai/app/services/model_images/model_images_default.py similarity index 60% rename from invokeai/app/services/model_images/model_images_disk.py rename to invokeai/app/services/model_images/model_images_default.py index 340a3d6b6f..6411d32d9c 100644 --- a/invokeai/app/services/model_images/model_images_disk.py +++ b/invokeai/app/services/model_images/model_images_default.py @@ -10,25 +10,24 @@ from invokeai.app.services.invoker import Invoker from .model_images_base import ModelImagesBase from .model_images_common import ModelImageFileDeleteException, ModelImageFileNotFoundException, ModelImageFileSaveException - -class DiskImageFileStorage(ModelImagesBase): +class ModelImagesService(ModelImagesBase): """Stores images on disk""" - __output_folder: Path + __model_images_folder: Path __invoker: Invoker - def __init__(self, output_folder: Union[str, Path]): + def __init__(self, model_images_folder: Union[str, Path]): - self.__output_folder: Path = output_folder if isinstance(output_folder, Path) else Path(output_folder) - # Validate required output folders at launch + self.__model_images_folder: Path = model_images_folder if isinstance(model_images_folder, Path) else Path(model_images_folder) + # Validate required folders at launch self.__validate_storage_folders() def start(self, invoker: Invoker) -> None: self.__invoker = invoker - def get(self, image_name: str) -> PILImageType: + def get(self, model_key: str) -> PILImageType: try: - image_path = self.get_path(image_name) + image_path = self.get_path(model_key + '.png') image = Image.open(image_path) return image @@ -38,17 +37,13 @@ class DiskImageFileStorage(ModelImagesBase): def save( self, image: PILImageType, - image_name: str, + model_key: str, ) -> None: try: self.__validate_storage_folders() - image_path = self.get_path(image_name) - + image_path = self.get_path(model_key + '.png') pnginfo = PngImagePlugin.PngInfo() - info_dict = {} - # When saving the image, the image object's info field is not populated. We need to set it - image.info = info_dict image.save( image_path, "PNG", @@ -59,9 +54,17 @@ class DiskImageFileStorage(ModelImagesBase): except Exception as e: raise ModelImageFileSaveException from e - def delete(self, image_name: str) -> None: + def get_path(self, model_key: str) -> Path: + path = self.__model_images_folder / model_key + + return path + + def get_url(self, model_key: str) -> str: + return self.__invoker.services.urls.get_model_image_url(model_key) + + def delete(self, model_key: str) -> None: try: - image_path = self.get_path(image_name) + image_path = self.get_path(model_key + '.png') if image_path.exists(): send2trash(image_path) @@ -69,14 +72,8 @@ class DiskImageFileStorage(ModelImagesBase): except Exception as e: raise ModelImageFileDeleteException from e - # TODO: make this a bit more flexible for e.g. cloud storage - def get_path(self, image_name: str) -> Path: - path = self.__output_folder / image_name - - return path - def __validate_storage_folders(self) -> None: - """Checks if the required output folders exist and create them if they don't""" - folders: list[Path] = [self.__output_folder] + """Checks if the required folders exist and create them if they don't""" + folders: list[Path] = [self.__model_images_folder] for folder in folders: folder.mkdir(parents=True, exist_ok=True) diff --git a/invokeai/app/services/urls/urls_base.py b/invokeai/app/services/urls/urls_base.py index c39ba055f3..477ef04624 100644 --- a/invokeai/app/services/urls/urls_base.py +++ b/invokeai/app/services/urls/urls_base.py @@ -8,3 +8,8 @@ class UrlServiceBase(ABC): def get_image_url(self, image_name: str, thumbnail: bool = False) -> str: """Gets the URL for an image or thumbnail.""" pass + + @abstractmethod + def get_model_image_url(self, model_key: str) -> str: + """Gets the URL for a model image""" + pass diff --git a/invokeai/app/services/urls/urls_default.py b/invokeai/app/services/urls/urls_default.py index 801aeac560..021f120b6d 100644 --- a/invokeai/app/services/urls/urls_default.py +++ b/invokeai/app/services/urls/urls_default.py @@ -15,3 +15,6 @@ class LocalUrlService(UrlServiceBase): return f"{self._base_url}/images/i/{image_basename}/thumbnail" return f"{self._base_url}/images/i/{image_basename}/full" + + def get_model_image_url(self, model_key: str) -> str: + return f"{self._base_url}/model_images/{model_key}.png" \ No newline at end of file diff --git a/invokeai/backend/model_manager/config.py b/invokeai/backend/model_manager/config.py index 250e8c5f83..896ddd361d 100644 --- a/invokeai/backend/model_manager/config.py +++ b/invokeai/backend/model_manager/config.py @@ -161,6 +161,7 @@ class ModelConfigBase(BaseModel): default_settings: Optional[ModelDefaultSettings] = Field( description="Default settings for this model", default=None ) + image: Optional[str] = Field(description="Image to preview model", default=None) @staticmethod def json_schema_extra(schema: dict[str, Any], model_class: Type[BaseModel]) -> None: @@ -374,6 +375,10 @@ AnyModelConfig = Annotated[ AnyModelConfigValidator = TypeAdapter(AnyModelConfig) +class ModelImage(str, Enum): + path: str + + class ModelConfigFactory(object): """Class for parsing config dicts into StableDiffusion Config obects.""" diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImage.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImage.tsx new file mode 100644 index 0000000000..952fd8f44e --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImage.tsx @@ -0,0 +1,73 @@ +import { Box, IconButton, Image } from '@invoke-ai/ui-library'; +import { typedMemo } from 'common/util/typedMemo'; +import { useCallback } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; +import type { AnyModelConfig } from 'services/api/types'; + +import { Button } from '@invoke-ai/ui-library'; +import { useDropzone } from 'react-dropzone'; +import { PiArrowCounterClockwiseBold, PiUploadSimpleBold } from 'react-icons/pi'; + +const ModelImageUpload = (props: UseControllerProps) => { + const { field } = useController(props); + + const onDropAccepted = useCallback( + (files: File[]) => { + const file = files[0]; + + if (!file) { + return; + } + + field.onChange(file); + }, + [field] + ); + + const handleResetControlImage = useCallback(() => { + field.onChange(undefined); + }, [field]); + + console.log('field', field); + + const { getInputProps, getRootProps } = useDropzone({ + accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] }, + onDropAccepted, + noDrag: true, + multiple: false, + }); + + if (field.value) { + return ( + + + } + size="sm" + variant="link" + /> + + ); + } + + return ( + <> + + + + ); +}; + +export default typedMemo(ModelImageUpload); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImageUpload.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImageUpload.tsx index 58d15d79d9..a31da3c893 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImageUpload.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImageUpload.tsx @@ -1,18 +1,22 @@ import { Box, IconButton, Image } from '@invoke-ai/ui-library'; import { typedMemo } from 'common/util/typedMemo'; -import { useCallback } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import type { UseControllerProps } from 'react-hook-form'; -import { useController } from 'react-hook-form'; +import { useController, useWatch } from 'react-hook-form'; import type { AnyModelConfig } from 'services/api/types'; import { Button } from '@invoke-ai/ui-library'; import { useDropzone } from 'react-dropzone'; import { PiArrowCounterClockwiseBold, PiUploadSimpleBold } from 'react-icons/pi'; -import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; +import { useGetModelImageQuery } from 'services/api/endpoints/models'; const ModelImageUpload = (props: UseControllerProps) => { const { field } = useController(props); + const key = useWatch({ control: props.control, name: 'key' }); + + const { data } = useGetModelImageQuery(key); + const onDropAccepted = useCallback( (files: File[]) => { const file = files[0]; @@ -30,8 +34,6 @@ const ModelImageUpload = (props: UseControllerProps) => { field.onChange(undefined); }, [field]); - console.log('field', field); - const { getInputProps, getRootProps } = useDropzone({ accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] }, onDropAccepted, @@ -39,11 +41,20 @@ const ModelImageUpload = (props: UseControllerProps) => { multiple: false, }); - if (field.value) { + const image = useMemo(() => { + console.log(field.value, 'asdf' ); + if (field.value) { + return URL.createObjectURL(field.value); + } + + return data; + }, [field.value, data]); + + if (image) { return ( ) => { icon={} size="sm" variant="link" - // sx={sx} /> ); @@ -73,4 +83,3 @@ const ModelImageUpload = (props: UseControllerProps) => { }; export default typedMemo(ModelImageUpload); - diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx index 6be73ffe28..5273e98757 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx @@ -20,7 +20,11 @@ import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import type { UpdateModelArg } from 'services/api/endpoints/models'; -import { useGetModelConfigQuery, useUpdateModelMutation } from 'services/api/endpoints/models'; +import { + useGetModelConfigQuery, + useUpdateModelImageMutation, + useUpdateModelsMutation, +} from 'services/api/endpoints/models'; import BaseModelSelect from './Fields/BaseModelSelect'; import ModelImageUpload from './Fields/ModelImageUpload'; @@ -32,7 +36,8 @@ export const ModelEdit = () => { const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); const { data, isLoading } = useGetModelConfigQuery(selectedModelKey ?? skipToken); - const [updateModel, { isLoading: isSubmitting }] = useUpdateModelMutation(); + const [updateModel, { isLoading: isSubmitting }] = useUpdateModelsMutation(); + const [updateModelImage] = useUpdateModelImageMutation(); const { t } = useTranslation(); @@ -55,11 +60,15 @@ export const ModelEdit = () => { return; } + // remove image from body + const image = values.image; + if (values.image) { + delete values.image; + } const responseBody: UpdateModelArg = { key: data.key, body: values, }; - console.log(responseBody, 'responseBody') updateModel(responseBody) .unwrap() @@ -86,6 +95,33 @@ export const ModelEdit = () => { ) ); }); + if (image) { + updateModelImage({ key: data.key, image: image }) + .unwrap() + .then((payload) => { + reset(payload, { keepDefaultValues: true }); + dispatch(setSelectedModelMode('view')); + dispatch( + addToast( + makeToast({ + title: t('modelManager.modelUpdated'), + status: 'success', + }) + ) + ); + }) + .catch((_) => { + reset(); + dispatch( + addToast( + makeToast({ + title: t('modelManager.modelUpdateFailed'), + status: 'error', + }) + ) + ); + }); + } }, [dispatch, data?.key, reset, t, updateModel] ); diff --git a/invokeai/frontend/web/src/services/api/endpoints/models.ts b/invokeai/frontend/web/src/services/api/endpoints/models.ts index fca2178dae..bb2b6bbd1d 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/models.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/models.ts @@ -23,7 +23,16 @@ export type UpdateModelArg = { body: paths['/api/v2/models/i/{key}']['patch']['requestBody']['content']['application/json']; }; +export type UpdateModelImageArg = { + key: paths['/api/v2/models/i/{key}/image']['patch']['parameters']['path']['key']; + image: paths['/api/v2/models/i/{key}/image']['patch']['formData']['content']['multipart/form-data']; +}; + type UpdateModelResponse = paths['/api/v2/models/i/{key}']['patch']['responses']['200']['content']['application/json']; +type UpdateModelImageResponse = + paths['/api/v2/models/i/{key}/image']['patch']['responses']['200']['content']['application/json']; +type GetModelImageResponse = + paths['/api/v2/models/i/{key}/image']['get']['responses']['200']['content']['application/json']; type GetModelConfigResponse = paths['/api/v2/models/i/{key}']['get']['responses']['200']['content']['application/json']; @@ -144,6 +153,21 @@ export const modelsApi = api.injectEndpoints({ }, invalidatesTags: ['Model'], }), + getModelImage: build.query({ + query: (key) => buildModelsUrl(`i/${key}/image`), + }), + updateModelImage: build.mutation({ + query: ({ key, image }) => { + const formData = new FormData(); + formData.append('image', image); + return { + url: buildModelsUrl(`i/${key}/image`), + method: 'PATCH', + body: formData, + }; + }, + invalidatesTags: ['Model'], + }), installModel: build.mutation({ query: ({ source }) => { return { @@ -330,7 +354,9 @@ export const { useGetTextualInversionModelsQuery, useGetVaeModelsQuery, useDeleteModelsMutation, - useUpdateModelMutation, + useUpdateModelsMutation, + useGetModelImageQuery, + useUpdateModelImageMutation, useInstallModelMutation, useConvertModelMutation, useSyncModelsMutation, diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index fcbc697924..0791c54b18 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -51,6 +51,15 @@ export type paths = { /** Scan For Models */ get: operations["scan_for_models"]; }; + "/api/v2/models/i/{key}/image": { + /** + * Get Model Image + * @description Gets a full-resolution image file + */ + get: operations["get_model_image"]; + /** Update Model Image */ + patch: operations["update_model_image"]; + }; "/api/v2/models/install": { /** * List Model Installs @@ -1104,6 +1113,14 @@ export type components = { */ image_names: string[]; }; + /** Body_update_model_image */ + Body_update_model_image: { + /** + * Image + * Format: binary + */ + image: Blob; + }; /** Body_update_workflow */ Body_update_workflow: { /** @description The updated workflow */ @@ -1364,6 +1381,11 @@ export type components = { trigger_phrases?: string[] | null; /** @description Default settings for this model */ default_settings?: components["schemas"]["ModelDefaultSettings"] | null; + /** + * Image + * @description Image to preview model + */ + image?: string | null; /** * Type * @default clip_vision @@ -2360,6 +2382,11 @@ export type components = { trigger_phrases?: string[] | null; /** @description Default settings for this model */ default_settings?: components["schemas"]["ModelDefaultSettings"] | null; + /** + * Image + * @description Image to preview model + */ + image?: string | null; /** * Format * @default checkpoint @@ -2434,6 +2461,11 @@ export type components = { trigger_phrases?: string[] | null; /** @description Default settings for this model */ default_settings?: components["schemas"]["ModelDefaultSettings"] | null; + /** + * Image + * @description Image to preview model + */ + image?: string | null; /** * Format * @default diffusers @@ -4130,7 +4162,7 @@ export type components = { * @description The nodes in this graph */ nodes: { - [key: string]: components["schemas"]["FloatInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SeamlessModeInvocation"]; + [key: string]: components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"]; }; /** * Edges @@ -4167,7 +4199,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["ControlOutput"] | components["schemas"]["String2Output"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["CollectInvocationOutput"]; + [key: string]: components["schemas"]["VAEOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["String2Output"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["FaceMaskOutput"]; }; /** * Errors @@ -4354,6 +4386,11 @@ export type components = { trigger_phrases?: string[] | null; /** @description Default settings for this model */ default_settings?: components["schemas"]["ModelDefaultSettings"] | null; + /** + * Image + * @description Image to preview model + */ + image?: string | null; /** * Type * @default ip_adapter @@ -6407,6 +6444,11 @@ export type components = { trigger_phrases?: string[] | null; /** @description Default settings for this model */ default_settings?: components["schemas"]["ModelDefaultSettings"] | null; + /** + * Image + * @description Image to preview model + */ + image?: string | null; /** * Type * @default lora @@ -6555,6 +6597,11 @@ export type components = { trigger_phrases?: string[] | null; /** @description Default settings for this model */ default_settings?: components["schemas"]["ModelDefaultSettings"] | null; + /** + * Image + * @description Image to preview model + */ + image?: string | null; /** * Type * @default lora @@ -6656,6 +6703,11 @@ export type components = { trigger_phrases?: string[] | null; /** @description Default settings for this model */ default_settings?: components["schemas"]["ModelDefaultSettings"] | null; + /** + * Image + * @description Image to preview model + */ + image?: string | null; /** * Format * @default checkpoint @@ -6739,6 +6791,11 @@ export type components = { trigger_phrases?: string[] | null; /** @description Default settings for this model */ default_settings?: components["schemas"]["ModelDefaultSettings"] | null; + /** + * Image + * @description Image to preview model + */ + image?: string | null; /** * Format * @default diffusers @@ -9705,6 +9762,11 @@ export type components = { trigger_phrases?: string[] | null; /** @description Default settings for this model */ default_settings?: components["schemas"]["ModelDefaultSettings"] | null; + /** + * Image + * @description Image to preview model + */ + image?: string | null; /** * Type * @default t2i_adapter @@ -9918,6 +9980,11 @@ export type components = { trigger_phrases?: string[] | null; /** @description Default settings for this model */ default_settings?: components["schemas"]["ModelDefaultSettings"] | null; + /** + * Image + * @description Image to preview model + */ + image?: string | null; /** * Type * @default embedding @@ -9982,6 +10049,11 @@ export type components = { trigger_phrases?: string[] | null; /** @description Default settings for this model */ default_settings?: components["schemas"]["ModelDefaultSettings"] | null; + /** + * Image + * @description Image to preview model + */ + image?: string | null; /** * Type * @default embedding @@ -10307,6 +10379,11 @@ export type components = { trigger_phrases?: string[] | null; /** @description Default settings for this model */ default_settings?: components["schemas"]["ModelDefaultSettings"] | null; + /** + * Image + * @description Image to preview model + */ + image?: string | null; /** * Format * @default checkpoint @@ -10381,6 +10458,11 @@ export type components = { trigger_phrases?: string[] | null; /** @description Default settings for this model */ default_settings?: components["schemas"]["ModelDefaultSettings"] | null; + /** + * Image + * @description Image to preview model + */ + image?: string | null; /** * Type * @default vae @@ -11127,6 +11209,72 @@ export type operations = { }; }; }; + /** + * Get Model Image + * @description Gets a full-resolution image file + */ + get_model_image: { + parameters: { + path: { + /** @description The name of model image file to get */ + key: string; + }; + }; + responses: { + /** @description The model image was fetched successfully */ + 200: { + content: { + "application/json": unknown; + }; + }; + /** @description Bad request */ + 400: { + content: never; + }; + /** @description The model could not be found */ + 404: { + content: never; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + /** Update Model Image */ + update_model_image: { + parameters: { + path: { + /** @description Unique key of model */ + key: string; + }; + }; + requestBody: { + content: { + "multipart/form-data": components["schemas"]["Body_update_model_image"]; + }; + }; + responses: { + /** @description The model image was updated successfully */ + 200: { + content: { + "application/json": unknown; + }; + }; + /** @description Bad request */ + 400: { + content: never; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; /** * List Model Installs * @description Return the list of model install jobs.