From cc96dcf0ed1716afc3e408866a16164f64aa30c7 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Wed, 7 Aug 2024 09:58:27 -0400 Subject: [PATCH] style preset images --- invokeai/app/api/dependencies.py | 7 + invokeai/app/api/routers/style_presets.py | 137 ++++++- .../app/services/config/config_default.py | 1 + invokeai/app/services/invocation_services.py | 3 + .../style_preset_images_base.py | 33 ++ .../style_preset_images_common.py | 19 + .../style_preset_images_default.py | 84 ++++ .../style_preset_records_common.py | 4 + invokeai/app/services/urls/urls_base.py | 5 + invokeai/app/services/urls/urls_default.py | 3 + .../SingleSelectionMenuItems.tsx | 15 + .../components/ActiveStylePreset.tsx | 6 +- .../components/StylePresetForm.tsx | 49 ++- .../components/StylePresetImage.tsx | 36 ++ .../components/StylePresetImageField.tsx | 79 ++++ .../components/StylePresetList.tsx | 4 +- .../components/StylePresetListItem.tsx | 34 +- .../components/StylePresetMenu.tsx | 4 +- .../components/StylePresetPromptField.tsx | 4 +- .../hooks/useStylePresetFields.ts | 44 +- .../store/stylePresetModalSlice.ts | 13 +- .../stylePresets/store/stylePresetSlice.ts | 4 +- .../src/features/stylePresets/store/types.ts | 14 +- .../services/api/endpoints/stylePresets.ts | 51 ++- .../frontend/web/src/services/api/schema.ts | 388 ++++++++++-------- .../4b356226-204a-4bc4-b803-01a62af8b5e9.webp | Bin 0 -> 12594 bytes .../8f6890b8-e1b1-41f2-8d78-e13d09341b3a.webp | Bin 0 -> 4414 bytes .../ab1a7540-be87-44fc-8844-00739d67cabc.webp | Bin 0 -> 7778 bytes 28 files changed, 786 insertions(+), 255 deletions(-) create mode 100644 invokeai/app/services/style_preset_images/style_preset_images_base.py create mode 100644 invokeai/app/services/style_preset_images/style_preset_images_common.py create mode 100644 invokeai/app/services/style_preset_images/style_preset_images_default.py create mode 100644 invokeai/frontend/web/src/features/stylePresets/components/StylePresetImage.tsx create mode 100644 invokeai/frontend/web/src/features/stylePresets/components/StylePresetImageField.tsx create mode 100644 style_preset_images/style_preset_images/4b356226-204a-4bc4-b803-01a62af8b5e9.webp create mode 100644 style_preset_images/style_preset_images/8f6890b8-e1b1-41f2-8d78-e13d09341b3a.webp create mode 100644 style_preset_images/style_preset_images/ab1a7540-be87-44fc-8844-00739d67cabc.webp diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index d2eeab6219..5c2a355f83 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -31,6 +31,8 @@ from invokeai.app.services.session_processor.session_processor_default import ( ) from invokeai.app.services.session_queue.session_queue_sqlite import SqliteSessionQueue from invokeai.app.services.shared.sqlite.sqlite_util import init_db +from invokeai.app.services.style_preset_images.style_preset_images_base import StylePresetImageFileStorageBase +from invokeai.app.services.style_preset_images.style_preset_images_default import StylePresetImageFileStorageDisk from invokeai.app.services.style_preset_records.style_preset_records_sqlite import SqliteStylePresetRecordsStorage from invokeai.app.services.urls.urls_default import LocalUrlService from invokeai.app.services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage @@ -75,6 +77,7 @@ class ApiDependencies: image_files = DiskImageFileStorage(f"{output_folder}/images") model_images_folder = config.models_path + style_preset_images_folder = config.style_preset_images_path db = init_db(config=config, logger=logger, image_files=image_files) @@ -111,6 +114,9 @@ class ApiDependencies: urls = LocalUrlService() workflow_records = SqliteWorkflowRecordsStorage(db=db) style_preset_records = SqliteStylePresetRecordsStorage(db=db) + style_preset_images_service = StylePresetImageFileStorageDisk( + style_preset_images_folder / "style_preset_images" + ) services = InvocationServices( board_image_records=board_image_records, @@ -137,6 +143,7 @@ class ApiDependencies: tensors=tensors, conditioning=conditioning, style_preset_records=style_preset_records, + style_preset_images_service=style_preset_images_service, ) ApiDependencies.invoker = Invoker(services) diff --git a/invokeai/app/api/routers/style_presets.py b/invokeai/app/api/routers/style_presets.py index 07f07fddb9..6e31e226c5 100644 --- a/invokeai/app/api/routers/style_presets.py +++ b/invokeai/app/api/routers/style_presets.py @@ -1,10 +1,18 @@ -from fastapi import APIRouter, Body, HTTPException, Path +import io +import traceback +from typing import Optional + +from fastapi import APIRouter, Body, File, Form, HTTPException, Path, UploadFile +from fastapi.responses import FileResponse +from PIL import Image from invokeai.app.api.dependencies import ApiDependencies +from invokeai.app.api.routers.model_manager import IMAGE_MAX_AGE from invokeai.app.services.style_preset_records.style_preset_records_common import ( + PresetData, StylePresetChanges, StylePresetNotFoundError, - StylePresetRecordDTO, + StylePresetRecordWithImage, StylePresetWithoutId, ) @@ -15,15 +23,17 @@ style_presets_router = APIRouter(prefix="/v1/style_presets", tags=["style_preset "/i/{style_preset_id}", operation_id="get_style_preset", responses={ - 200: {"model": StylePresetRecordDTO}, + 200: {"model": StylePresetRecordWithImage}, }, ) async def get_style_preset( style_preset_id: str = Path(description="The style preset to get"), -) -> StylePresetRecordDTO: +) -> StylePresetRecordWithImage: """Gets a style preset""" try: - return ApiDependencies.invoker.services.style_preset_records.get(style_preset_id) + image = ApiDependencies.invoker.services.style_preset_images_service.get_url(style_preset_id) + style_preset = ApiDependencies.invoker.services.style_preset_records.get(style_preset_id) + return StylePresetRecordWithImage(image=image, **style_preset.model_dump()) except StylePresetNotFoundError: raise HTTPException(status_code=404, detail="Style preset not found") @@ -32,15 +42,45 @@ async def get_style_preset( "/i/{style_preset_id}", operation_id="update_style_preset", responses={ - 200: {"model": StylePresetRecordDTO}, + 200: {"model": StylePresetRecordWithImage}, }, ) async def update_style_preset( style_preset_id: str = Path(description="The id of the style preset to update"), - changes: StylePresetChanges = Body(description="The updated style preset", embed=True), -) -> StylePresetRecordDTO: + name: str = Form(description="The name of the style preset to create"), + positive_prompt: str = Form(description="The positive prompt of the style preset"), + negative_prompt: str = Form(description="The negative prompt of the style preset"), + image: Optional[UploadFile] = File(description="The image file to upload", default=None), +) -> StylePresetRecordWithImage: """Updates a style preset""" - return ApiDependencies.invoker.services.style_preset_records.update(id=style_preset_id, changes=changes) + if image is not 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") + + try: + ApiDependencies.invoker.services.style_preset_images_service.save(pil_image, style_preset_id) + except ValueError as e: + raise HTTPException(status_code=409, detail=str(e)) + else: + try: + ApiDependencies.invoker.services.style_preset_images_service.delete(style_preset_id) + except ValueError as e: + raise HTTPException(status_code=409, detail=str(e)) + + preset_data = PresetData(positive_prompt=positive_prompt, negative_prompt=negative_prompt) + changes = StylePresetChanges(name=name, preset_data=preset_data) + + style_preset_image = ApiDependencies.invoker.services.style_preset_images_service.get_url(style_preset_id) + style_preset = ApiDependencies.invoker.services.style_preset_records.update(id=style_preset_id, changes=changes) + return StylePresetRecordWithImage(image=style_preset_image, **style_preset.model_dump()) @style_presets_router.delete( @@ -51,6 +91,7 @@ async def delete_style_preset( style_preset_id: str = Path(description="The style preset to delete"), ) -> None: """Deletes a style preset""" + ApiDependencies.invoker.services.style_preset_images_service.delete(style_preset_id) ApiDependencies.invoker.services.style_preset_records.delete(style_preset_id) @@ -58,23 +99,87 @@ async def delete_style_preset( "/", operation_id="create_style_preset", responses={ - 200: {"model": StylePresetRecordDTO}, + 200: {"model": StylePresetRecordWithImage}, }, ) async def create_style_preset( - style_preset: StylePresetWithoutId = Body(description="The style preset to create", embed=True), -) -> StylePresetRecordDTO: + name: str = Form(description="The name of the style preset to create"), + positive_prompt: str = Form(description="The positive prompt of the style preset"), + negative_prompt: str = Form(description="The negative prompt of the style preset"), + image: Optional[UploadFile] = File(description="The image file to upload", default=None), +) -> StylePresetRecordWithImage: """Creates a style preset""" - return ApiDependencies.invoker.services.style_preset_records.create(style_preset=style_preset) + preset_data = PresetData(positive_prompt=positive_prompt, negative_prompt=negative_prompt) + style_preset = StylePresetWithoutId(name=name, preset_data=preset_data) + new_style_preset = ApiDependencies.invoker.services.style_preset_records.create(style_preset=style_preset) + + if image is not 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") + + try: + ApiDependencies.invoker.services.style_preset_images_service.save(pil_image, new_style_preset.id) + except ValueError as e: + raise HTTPException(status_code=409, detail=str(e)) + + preset_image = ApiDependencies.invoker.services.style_preset_images_service.get_url(new_style_preset.id) + return StylePresetRecordWithImage(image=preset_image, **new_style_preset.model_dump()) @style_presets_router.get( "/", operation_id="list_style_presets", responses={ - 200: {"model": list[StylePresetRecordDTO]}, + 200: {"model": list[StylePresetRecordWithImage]}, }, ) -async def list_style_presets() -> list[StylePresetRecordDTO]: +async def list_style_presets() -> list[StylePresetRecordWithImage]: """Gets a page of style presets""" - return ApiDependencies.invoker.services.style_preset_records.get_many() + style_presets_with_image: list[StylePresetRecordWithImage] = [] + style_presets = ApiDependencies.invoker.services.style_preset_records.get_many() + for preset in style_presets: + image = ApiDependencies.invoker.services.style_preset_images_service.get_url(preset.id) + style_preset_with_image = StylePresetRecordWithImage(image=image, **preset.model_dump()) + style_presets_with_image.append(style_preset_with_image) + + return style_presets_with_image + + +@style_presets_router.get( + "/i/{style_preset_id}/image", + operation_id="get_style_preset_image", + responses={ + 200: { + "description": "The style preset image was fetched successfully", + }, + 400: {"description": "Bad request"}, + 404: {"description": "The style preset image could not be found"}, + }, + status_code=200, +) +async def get_style_preset_image( + style_preset_id: str = Path(description="The id of the style preset image to get"), +) -> FileResponse: + """Gets an image file that previews the model""" + + try: + path = ApiDependencies.invoker.services.style_preset_images_service.get_path(style_preset_id) + + response = FileResponse( + path, + media_type="image/png", + filename=style_preset_id + ".png", + content_disposition_type="inline", + ) + response.headers["Cache-Control"] = f"max-age={IMAGE_MAX_AGE}" + return response + except Exception: + raise HTTPException(status_code=404) diff --git a/invokeai/app/services/config/config_default.py b/invokeai/app/services/config/config_default.py index 6c39760bdc..61b520ce67 100644 --- a/invokeai/app/services/config/config_default.py +++ b/invokeai/app/services/config/config_default.py @@ -153,6 +153,7 @@ class InvokeAIAppConfig(BaseSettings): db_dir: Path = Field(default=Path("databases"), description="Path to InvokeAI databases directory.") outputs_dir: Path = Field(default=Path("outputs"), description="Path to directory for outputs.") custom_nodes_dir: Path = Field(default=Path("nodes"), description="Path to directory for custom nodes.") + style_preset_images_path: Path = Field(default=Path("style_preset_images"), description="Path to directory for style preset images.") # LOGGING log_handlers: list[str] = Field(default=["console"], description='Log handler. Valid options are "console", "file=", "syslog=path|address:host:port", "http=".') diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index c756eae8e5..3686b1d9e1 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import TYPE_CHECKING from invokeai.app.services.object_serializer.object_serializer_base import ObjectSerializerBase +from invokeai.app.services.style_preset_images.style_preset_images_base import StylePresetImageFileStorageBase from invokeai.app.services.style_preset_records.style_preset_records_base import StylePresetRecordsStorageBase if TYPE_CHECKING: @@ -63,6 +64,7 @@ class InvocationServices: tensors: "ObjectSerializerBase[torch.Tensor]", conditioning: "ObjectSerializerBase[ConditioningFieldData]", style_preset_records: "StylePresetRecordsStorageBase", + style_preset_images_service: "StylePresetImageFileStorageBase", ): self.board_images = board_images self.board_image_records = board_image_records @@ -88,3 +90,4 @@ class InvocationServices: self.tensors = tensors self.conditioning = conditioning self.style_preset_records = style_preset_records + self.style_preset_images_service = style_preset_images_service diff --git a/invokeai/app/services/style_preset_images/style_preset_images_base.py b/invokeai/app/services/style_preset_images/style_preset_images_base.py new file mode 100644 index 0000000000..cc5cf3edae --- /dev/null +++ b/invokeai/app/services/style_preset_images/style_preset_images_base.py @@ -0,0 +1,33 @@ +from abc import ABC, abstractmethod +from pathlib import Path + +from PIL.Image import Image as PILImageType + + +class StylePresetImageFileStorageBase(ABC): + """Low-level service responsible for storing and retrieving image files.""" + + @abstractmethod + def get(self, style_preset_id: str) -> PILImageType: + """Retrieves a style preset image as PIL Image.""" + pass + + @abstractmethod + def get_path(self, style_preset_id: str) -> Path: + """Gets the internal path to a style preset image.""" + pass + + @abstractmethod + def get_url(self, style_preset_id: str) -> str | None: + """Gets the URL to fetch a style preset image.""" + pass + + @abstractmethod + def save(self, image: PILImageType, style_preset_id: str) -> None: + """Saves a style preset image.""" + pass + + @abstractmethod + def delete(self, style_preset_id: str) -> None: + """Deletes a style preset image.""" + pass diff --git a/invokeai/app/services/style_preset_images/style_preset_images_common.py b/invokeai/app/services/style_preset_images/style_preset_images_common.py new file mode 100644 index 0000000000..fe7d204b00 --- /dev/null +++ b/invokeai/app/services/style_preset_images/style_preset_images_common.py @@ -0,0 +1,19 @@ +class StylePresetImageFileNotFoundException(Exception): + """Raised when an image file is not found in storage.""" + + def __init__(self, message="Style preset image file not found"): + super().__init__(message) + + +class StylePresetImageFileSaveException(Exception): + """Raised when an image cannot be saved.""" + + def __init__(self, message="Style preset image file not saved"): + super().__init__(message) + + +class StylePresetImageFileDeleteException(Exception): + """Raised when an image cannot be deleted.""" + + def __init__(self, message="Style preset image file not deleted"): + super().__init__(message) diff --git a/invokeai/app/services/style_preset_images/style_preset_images_default.py b/invokeai/app/services/style_preset_images/style_preset_images_default.py new file mode 100644 index 0000000000..5f907c78ac --- /dev/null +++ b/invokeai/app/services/style_preset_images/style_preset_images_default.py @@ -0,0 +1,84 @@ +from pathlib import Path + +from PIL import Image +from PIL.Image import Image as PILImageType +from send2trash import send2trash + +from invokeai.app.services.invoker import Invoker +from invokeai.app.services.style_preset_images.style_preset_images_base import StylePresetImageFileStorageBase +from invokeai.app.services.style_preset_images.style_preset_images_common import ( + StylePresetImageFileDeleteException, + StylePresetImageFileNotFoundException, + StylePresetImageFileSaveException, +) +from invokeai.app.util.misc import uuid_string +from invokeai.app.util.thumbnails import make_thumbnail + + +class StylePresetImageFileStorageDisk(StylePresetImageFileStorageBase): + """Stores images on disk""" + + def __init__(self, style_preset_images_folder: Path): + self._style_preset_images_folder = style_preset_images_folder + self._validate_storage_folders() + + def start(self, invoker: Invoker) -> None: + self._invoker = invoker + + def get(self, style_preset_id: str) -> PILImageType: + try: + path = self.get_path(style_preset_id) + + if not self._validate_path(path): + raise StylePresetImageFileNotFoundException + + return Image.open(path) + except FileNotFoundError as e: + raise StylePresetImageFileNotFoundException from e + + def save(self, image: PILImageType, style_preset_id: str) -> None: + try: + self._validate_storage_folders() + image_path = self._style_preset_images_folder / (style_preset_id + ".webp") + thumbnail = make_thumbnail(image, 256) + thumbnail.save(image_path, format="webp") + + except Exception as e: + raise StylePresetImageFileSaveException from e + + def get_path(self, style_preset_id: str) -> Path: + path = self._style_preset_images_folder / (style_preset_id + ".webp") + + return path + + def get_url(self, style_preset_id: str) -> str | None: + path = self.get_path(style_preset_id) + if not self._validate_path(path): + return + + url = self._invoker.services.urls.get_style_preset_image_url(style_preset_id) + + # The image URL never changes, so we must add random query string to it to prevent caching + url += f"?{uuid_string()}" + + return url + + def delete(self, style_preset_id: str) -> None: + try: + path = self.get_path(style_preset_id) + + if not self._validate_path(path): + raise StylePresetImageFileNotFoundException + + send2trash(path) + + except Exception as e: + raise StylePresetImageFileDeleteException from e + + def _validate_path(self, path: Path) -> bool: + """Validates the path given for an image.""" + return path.exists() + + def _validate_storage_folders(self) -> None: + """Checks if the required folders exist and create them if they don't""" + self._style_preset_images_folder.mkdir(parents=True, exist_ok=True) diff --git a/invokeai/app/services/style_preset_records/style_preset_records_common.py b/invokeai/app/services/style_preset_records/style_preset_records_common.py index f683eea97b..dfe14caa11 100644 --- a/invokeai/app/services/style_preset_records/style_preset_records_common.py +++ b/invokeai/app/services/style_preset_records/style_preset_records_common.py @@ -39,3 +39,7 @@ class StylePresetRecordDTO(StylePresetWithoutId): StylePresetRecordDTOValidator = TypeAdapter(StylePresetRecordDTO) + + +class StylePresetRecordWithImage(StylePresetRecordDTO): + image: Optional[str] = Field(description="The path for image") diff --git a/invokeai/app/services/urls/urls_base.py b/invokeai/app/services/urls/urls_base.py index 477ef04624..b2e41db3e4 100644 --- a/invokeai/app/services/urls/urls_base.py +++ b/invokeai/app/services/urls/urls_base.py @@ -13,3 +13,8 @@ class UrlServiceBase(ABC): def get_model_image_url(self, model_key: str) -> str: """Gets the URL for a model image""" pass + + @abstractmethod + def get_style_preset_image_url(self, style_preset_id: str) -> str: + """Gets the URL for a style preset image""" + pass diff --git a/invokeai/app/services/urls/urls_default.py b/invokeai/app/services/urls/urls_default.py index d570521fb8..f62bebe901 100644 --- a/invokeai/app/services/urls/urls_default.py +++ b/invokeai/app/services/urls/urls_default.py @@ -19,3 +19,6 @@ class LocalUrlService(UrlServiceBase): def get_model_image_url(self, model_key: str) -> str: return f"{self._base_url_v2}/models/i/{model_key}/image" + + def get_style_preset_image_url(self, style_preset_id: str) -> str: + return f"{self._base_url}/style_presets/i/{style_preset_id}/image" diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index ab12684c11..6da89d1604 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -30,6 +30,7 @@ import { PiFlowArrowBold, PiFoldersBold, PiImagesBold, + PiPaintBrushBold, PiPlantBold, PiQuotesBold, PiShareFatBold, @@ -39,6 +40,8 @@ import { } from 'react-icons/pi'; import { useStarImagesMutation, useUnstarImagesMutation } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; +import { isMenuOpenChanged } from '../../../stylePresets/store/stylePresetSlice'; +import { createPresetFromImageChanged } from '../../../stylePresets/store/stylePresetModalSlice'; type SingleSelectionMenuItemsProps = { imageDTO: ImageDTO; @@ -130,6 +133,11 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { dispatch(setActiveTab('upscaling')); }, [dispatch, imageDTO]); + const handleCreatePreset = useCallback(() => { + dispatch(createPresetFromImageChanged(imageDTO)); + dispatch(isMenuOpenChanged(true)); + }, [dispatch, imageDTO]); + return ( <> }> @@ -182,6 +190,13 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { > {t('parameters.useAll')} + : } + onClickCapture={handleCreatePreset} + isDisabled={isLoadingMetadata || !hasPrompts} + > + Create Preset + } onClickCapture={handleSendToImageToImage} id="send-to-img2img"> {t('parameters.sendToImg2Img')} diff --git a/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx b/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx index ebad9de24a..e6a81d256f 100644 --- a/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx +++ b/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx @@ -1,13 +1,13 @@ import { Flex, IconButton, Text } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { negativePromptChanged, positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; -import ModelImage from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelImage'; import { usePresetModifiedPrompts } from 'features/stylePresets/hooks/usePresetModifiedPrompts'; import { activeStylePresetChanged } from 'features/stylePresets/store/stylePresetSlice'; -import type { MouseEventHandler} from 'react'; +import type { MouseEventHandler } from 'react'; import { useCallback } from 'react'; import { CgPushDown } from 'react-icons/cg'; import { PiXBold } from 'react-icons/pi'; +import StylePresetImage from './StylePresetImage'; export const ActiveStylePreset = () => { const { activeStylePreset } = useAppSelector((s) => s.stylePreset); @@ -40,7 +40,7 @@ export const ActiveStylePreset = () => { <> - + Prompt Style diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetForm.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetForm.tsx index 7d84d4e43f..adbc8e26af 100644 --- a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetForm.tsx +++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetForm.tsx @@ -4,21 +4,23 @@ import { useStylePresetFields } from 'features/stylePresets/hooks/useStylePreset import { isModalOpenChanged, updatingStylePresetChanged } from 'features/stylePresets/store/stylePresetModalSlice'; import { toast } from 'features/toast/toast'; import { useCallback } from 'react'; -import type { SubmitHandler} from 'react-hook-form'; +import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { PiBracketsCurlyBold } from 'react-icons/pi'; -import type { StylePresetRecordDTO } from 'services/api/endpoints/stylePresets'; +import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets'; import { useCreateStylePresetMutation, useUpdateStylePresetMutation } from 'services/api/endpoints/stylePresets'; import { StylePresetPromptField } from './StylePresetPromptField'; +import { StylePresetImageField } from './StylePresetImageField'; export type StylePresetFormData = { name: string; positivePrompt: string; negativePrompt: string; + image: File | null; }; -export const StylePresetForm = ({ updatingPreset }: { updatingPreset: StylePresetRecordDTO | null }) => { +export const StylePresetForm = ({ updatingPreset }: { updatingPreset: StylePresetRecordWithImage | null }) => { const [createStylePreset] = useCreateStylePresetMutation(); const [updateStylePreset] = useUpdateStylePresetMutation(); const dispatch = useAppDispatch(); @@ -31,20 +33,21 @@ export const StylePresetForm = ({ updatingPreset }: { updatingPreset: StylePrese const handleClickSave = useCallback>( async (data) => { + const payload = { + name: data.name, + positive_prompt: data.positivePrompt, + negative_prompt: data.negativePrompt, + image: data.image, + }; + try { if (updatingPreset) { await updateStylePreset({ id: updatingPreset.id, - changes: { - name: data.name, - preset_data: { positive_prompt: data.positivePrompt, negative_prompt: data.negativePrompt }, - }, + ...payload, }).unwrap(); } else { - await createStylePreset({ - name: data.name, - preset_data: { positive_prompt: data.positivePrompt, negative_prompt: data.negativePrompt }, - }).unwrap(); + await createStylePreset(payload).unwrap(); } } catch (error) { toast({ @@ -61,19 +64,21 @@ export const StylePresetForm = ({ updatingPreset }: { updatingPreset: StylePrese return ( - - Name - - - - - Use the button to specify where your manual prompt should be included in the - template. If you do not provide one, the template will be appended to your prompt. - - - + + + + Name + + + + + + Use the button to specify where your manual prompt should be included in the + template. If you do not provide one, the template will be appended to your prompt. + + ); diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetImage.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetImage.tsx new file mode 100644 index 0000000000..057967485d --- /dev/null +++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetImage.tsx @@ -0,0 +1,36 @@ +import { Flex, Icon, Image } from '@invoke-ai/ui-library'; +import { typedMemo } from 'common/util/typedMemo'; +import { PiImage } from 'react-icons/pi'; + +const IMAGE_THUMBNAIL_SIZE = '40px'; +const FALLBACK_ICON_SIZE = '24px'; + +const StylePresetImage = ({ presetImageUrl }: { presetImageUrl: string | null }) => { + return ( + + + + } + objectFit="cover" + objectPosition="50% 50%" + height={IMAGE_THUMBNAIL_SIZE} + width={IMAGE_THUMBNAIL_SIZE} + minHeight={IMAGE_THUMBNAIL_SIZE} + minWidth={IMAGE_THUMBNAIL_SIZE} + borderRadius="base" + /> + ); +}; + +export default typedMemo(StylePresetImage); diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetImageField.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetImageField.tsx new file mode 100644 index 0000000000..1e804f3cc3 --- /dev/null +++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetImageField.tsx @@ -0,0 +1,79 @@ +import { Tooltip, Flex, Button, Icon, Box, Image, IconButton } from '@invoke-ai/ui-library'; +import { t } from 'i18next'; +import { useCallback, useState } from 'react'; +import { useDropzone } from 'react-dropzone'; +import { PiArrowCounterClockwiseBold, PiUploadSimpleBold } from 'react-icons/pi'; +import { useController, UseControllerProps } from 'react-hook-form'; +import { StylePresetFormData } from './StylePresetForm'; + +export const StylePresetImageField = (props: UseControllerProps) => { + const { field } = useController(props); + const onDropAccepted = useCallback( + (files: File[]) => { + const file = files[0]; + if (file) { + field.onChange(file); + } + }, + [field, t] + ); + + const handleResetImage = useCallback(() => { + field.onChange(null); + }, []); + + const { getInputProps, getRootProps } = useDropzone({ + accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] }, + onDropAccepted, + noDrag: true, + multiple: false, + }); + + if (field.value) { + return ( + + + } + size="md" + variant="ghost" + /> + + ); + } + + return ( + <> + + + + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetList.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetList.tsx index ae402d97ca..410cf4e1e6 100644 --- a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetList.tsx +++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetList.tsx @@ -1,10 +1,10 @@ import { Button, Collapse, Flex, Icon, Text, useDisclosure } from '@invoke-ai/ui-library'; import { PiCaretDownBold } from 'react-icons/pi'; -import type { StylePresetRecordDTO } from 'services/api/endpoints/stylePresets'; +import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets'; import { StylePresetListItem } from './StylePresetListItem'; -export const StylePresetList = ({ title, data }: { title: string; data: StylePresetRecordDTO[] }) => { +export const StylePresetList = ({ title, data }: { title: string; data: StylePresetRecordWithImage[] }) => { const { onToggle, isOpen } = useDisclosure({ defaultIsOpen: true }); if (!data.length) { diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetListItem.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetListItem.tsx index b2d2f87938..ba4a479ced 100644 --- a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetListItem.tsx +++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetListItem.tsx @@ -1,33 +1,43 @@ import { Badge, Flex, IconButton, Text } from '@invoke-ai/ui-library'; +import type { MouseEvent } from 'react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import ModelImage from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelImage'; import { isModalOpenChanged, updatingStylePresetChanged } from 'features/stylePresets/store/stylePresetModalSlice'; import { activeStylePresetChanged, isMenuOpenChanged } from 'features/stylePresets/store/stylePresetSlice'; import { useCallback } from 'react'; import { PiPencilBold, PiTrashBold } from 'react-icons/pi'; -import type { StylePresetRecordDTO } from 'services/api/endpoints/stylePresets'; +import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets'; import { useDeleteStylePresetMutation } from 'services/api/endpoints/stylePresets'; +import StylePresetImage from './StylePresetImage'; -export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordDTO }) => { +export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithImage }) => { const dispatch = useAppDispatch(); const [deleteStylePreset] = useDeleteStylePresetMutation(); const activeStylePreset = useAppSelector((s) => s.stylePreset.activeStylePreset); - const handleClickEdit = useCallback(() => { - dispatch(updatingStylePresetChanged(preset)); - dispatch(isModalOpenChanged(true)); - }, [dispatch, preset]); + const handleClickEdit = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); + dispatch(updatingStylePresetChanged(preset)); + dispatch(isModalOpenChanged(true)); + }, + [dispatch, preset] + ); const handleClickApply = useCallback(() => { dispatch(activeStylePresetChanged(preset)); dispatch(isMenuOpenChanged(false)); }, [dispatch, preset]); - const handleDeletePreset = useCallback(async () => { - try { - await deleteStylePreset(preset.id); - } catch (error) {} - }, [preset]); + const handleDeletePreset = useCallback( + async (e: MouseEvent) => { + e.stopPropagation(); + try { + await deleteStylePreset(preset.id); + } catch (error) {} + }, + [preset] + ); return ( - + diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenu.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenu.tsx index 0779f886c3..5e8b2aa6c8 100644 --- a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenu.tsx +++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenu.tsx @@ -4,7 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { isModalOpenChanged, updatingStylePresetChanged } from 'features/stylePresets/store/stylePresetModalSlice'; import { useCallback } from 'react'; import { PiPlusBold } from 'react-icons/pi'; -import type { StylePresetRecordDTO} from 'services/api/endpoints/stylePresets'; +import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets'; import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; import { StylePresetList } from './StylePresetList'; @@ -18,7 +18,7 @@ export const StylePresetMenu = () => { data?.filter((preset) => preset.name.toLowerCase().includes(searchTerm.toLowerCase())) || EMPTY_ARRAY; const groupedData = filteredData.reduce( - (acc: { defaultPresets: StylePresetRecordDTO[]; presets: StylePresetRecordDTO[] }, preset) => { + (acc: { defaultPresets: StylePresetRecordWithImage[]; presets: StylePresetRecordWithImage[] }, preset) => { if (preset.is_default) { acc.defaultPresets.push(preset); } else { diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetPromptField.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetPromptField.tsx index f84ddffac0..70f848bef6 100644 --- a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetPromptField.tsx +++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetPromptField.tsx @@ -24,7 +24,7 @@ export const StylePresetPromptField = (props: Props) => { ); const value = useMemo(() => { - return field.value; + return field.value as string; }, [field.value]); const insertPromptPlaceholder = useCallback(() => { @@ -40,7 +40,7 @@ export const StylePresetPromptField = (props: Props) => { } }, [value, field, textareaRef]); - const isPromptPresent = useMemo(() => value.includes(PRESET_PLACEHOLDER), [value]); + const isPromptPresent = useMemo(() => value?.includes(PRESET_PLACEHOLDER), [value]); return ( diff --git a/invokeai/frontend/web/src/features/stylePresets/hooks/useStylePresetFields.ts b/invokeai/frontend/web/src/features/stylePresets/hooks/useStylePresetFields.ts index fa16c0d5b0..9262893ded 100644 --- a/invokeai/frontend/web/src/features/stylePresets/hooks/useStylePresetFields.ts +++ b/invokeai/frontend/web/src/features/stylePresets/hooks/useStylePresetFields.ts @@ -1,17 +1,45 @@ -import { useMemo } from 'react'; -import type { StylePresetRecordDTO } from 'services/api/endpoints/stylePresets'; +import { useCallback, useMemo } from 'react'; +import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets'; +import { useAppSelector } from '../../../app/store/storeHooks'; +import { useDebouncedMetadata } from '../../../services/api/hooks/useDebouncedMetadata'; +import { handlers } from '../../metadata/util/handlers'; +import { useImageUrlToBlob } from '../../../common/hooks/useImageUrlToBlob'; + + +export const useStylePresetFields = (preset: StylePresetRecordWithImage | null) => { + const createPresetFromImage = useAppSelector(s => s.stylePresetModal.createPresetFromImage) + + const imageUrlToBlob = useImageUrlToBlob(); + + const getStylePresetFieldDefaults = useCallback(async () => { + if (preset) { + let file: File | null = null; + if (preset.image) { + const blob = await imageUrlToBlob(preset.image); + if (blob) { + file = new File([blob], "name"); + } + + } + + return { + name: preset.name, + positivePrompt: preset.preset_data.positive_prompt, + negativePrompt: preset.preset_data.negative_prompt, + image: file + }; + } -export const useStylePresetFields = (preset: StylePresetRecordDTO | null) => { - const stylePresetFieldDefaults = useMemo(() => { return { - name: preset ? preset.name : '', - positivePrompt: preset ? preset.preset_data.positive_prompt : '', - negativePrompt: preset ? preset.preset_data.negative_prompt : '' + name: "", + positivePrompt: "", + negativePrompt: "", + image: null }; }, [ preset ]); - return stylePresetFieldDefaults; + return getStylePresetFieldDefaults; }; diff --git a/invokeai/frontend/web/src/features/stylePresets/store/stylePresetModalSlice.ts b/invokeai/frontend/web/src/features/stylePresets/store/stylePresetModalSlice.ts index b0dc894037..81582197c7 100644 --- a/invokeai/frontend/web/src/features/stylePresets/store/stylePresetModalSlice.ts +++ b/invokeai/frontend/web/src/features/stylePresets/store/stylePresetModalSlice.ts @@ -1,14 +1,16 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; -import type { StylePresetRecordDTO } from 'services/api/endpoints/stylePresets'; +import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets'; -import type { StylePresetModalState } from './types'; +import type { StylePresetModalState, StylePresetPrefillOptions } from './types'; +import { ImageDTO } from '../../../services/api/types'; export const initialState: StylePresetModalState = { isModalOpen: false, updatingStylePreset: null, + createPresetFromImage: null }; @@ -19,12 +21,15 @@ export const stylePresetModalSlice = createSlice({ isModalOpenChanged: (state, action: PayloadAction) => { state.isModalOpen = action.payload; }, - updatingStylePresetChanged: (state, action: PayloadAction) => { + updatingStylePresetChanged: (state, action: PayloadAction) => { state.updatingStylePreset = action.payload; }, + createPresetFromImageChanged: (state, action: PayloadAction) => { + state.createPresetFromImage = action.payload; + }, }, }); -export const { isModalOpenChanged, updatingStylePresetChanged } = stylePresetModalSlice.actions; +export const { isModalOpenChanged, updatingStylePresetChanged, createPresetFromImageChanged } = stylePresetModalSlice.actions; export const selectStylePresetModalSlice = (state: RootState) => state.stylePresetModal; diff --git a/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts b/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts index c8b493d3e7..a14d3d75ab 100644 --- a/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts +++ b/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts @@ -1,7 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; -import type { StylePresetRecordDTO } from 'services/api/endpoints/stylePresets'; +import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets'; import type { StylePresetState } from './types'; @@ -20,7 +20,7 @@ export const stylePresetSlice = createSlice({ isMenuOpenChanged: (state, action: PayloadAction) => { state.isMenuOpen = action.payload; }, - activeStylePresetChanged: (state, action: PayloadAction) => { + activeStylePresetChanged: (state, action: PayloadAction) => { state.activeStylePreset = action.payload; }, searchTermChanged: (state, action: PayloadAction) => { diff --git a/invokeai/frontend/web/src/features/stylePresets/store/types.ts b/invokeai/frontend/web/src/features/stylePresets/store/types.ts index 619e1bbd57..56358ac363 100644 --- a/invokeai/frontend/web/src/features/stylePresets/store/types.ts +++ b/invokeai/frontend/web/src/features/stylePresets/store/types.ts @@ -1,13 +1,21 @@ -import type { StylePresetRecordDTO } from "services/api/endpoints/stylePresets"; +import type { StylePresetRecordWithImage } from "services/api/endpoints/stylePresets"; +import { ImageDTO } from "../../../services/api/types"; export type StylePresetModalState = { isModalOpen: boolean; - updatingStylePreset: StylePresetRecordDTO | null; + updatingStylePreset: StylePresetRecordWithImage | null; + createPresetFromImage: ImageDTO | null }; +export type StylePresetPrefillOptions = { + positivePrompt: string; + negativePrompt: string; + image: File; +} + export type StylePresetState = { isMenuOpen: boolean; - activeStylePreset: StylePresetRecordDTO | null; + activeStylePreset: StylePresetRecordWithImage | null; searchTerm: string } diff --git a/invokeai/frontend/web/src/services/api/endpoints/stylePresets.ts b/invokeai/frontend/web/src/services/api/endpoints/stylePresets.ts index 7e6850fb2e..96662f77f9 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/stylePresets.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/stylePresets.ts @@ -2,7 +2,7 @@ import type { paths } from 'services/api/schema'; import { api, buildV1Url, LIST_TAG } from '..'; -export type StylePresetRecordDTO = paths['/api/v1/style_presets/i/{style_preset_id}']['get']['responses']['200']['content']['application/json'] +export type StylePresetRecordWithImage = paths['/api/v1/style_presets/i/{style_preset_id}']['get']['responses']['200']['content']['application/json'] /** * Builds an endpoint URL for the style_presets router @@ -36,13 +36,23 @@ export const stylePresetsApi = api.injectEndpoints({ }), createStylePreset: build.mutation< paths['/api/v1/style_presets/']['post']['responses']['200']['content']['application/json'], - paths['/api/v1/style_presets/']['post']['requestBody']['content']['application/json']['style_preset'] + paths['/api/v1/style_presets/']['post']['requestBody']['content']['multipart/form-data'] >({ - query: (style_preset) => ({ - url: buildStylePresetsUrl(), - method: 'POST', - body: { style_preset }, - }), + query: ({ name, positive_prompt, negative_prompt, image }) => { + const formData = new FormData(); + if (image) { + formData.append('image', image); + } + formData.append('name', name); + formData.append('positive_prompt', positive_prompt); + formData.append('negative_prompt', negative_prompt); + + return { + url: buildStylePresetsUrl(), + method: 'POST', + body: formData, + }; + }, invalidatesTags: [ { type: 'StylePreset', id: LIST_TAG }, { type: 'StylePreset', id: LIST_TAG }, @@ -50,16 +60,25 @@ export const stylePresetsApi = api.injectEndpoints({ }), updateStylePreset: build.mutation< paths['/api/v1/style_presets/i/{style_preset_id}']['patch']['responses']['200']['content']['application/json'], - { - id: string; - changes: paths['/api/v1/style_presets/i/{style_preset_id}']['patch']['requestBody']['content']['application/json']['changes']; - } + paths['/api/v1/style_presets/i/{style_preset_id}']['patch']['requestBody']['content']['multipart/form-data'] & { id: string } >({ - query: ({ id, changes }) => ({ - url: buildStylePresetsUrl(`i/${id}`), - method: 'PATCH', - body: { changes }, - }), + query: ({ id, name, positive_prompt, negative_prompt, image }) => { + const formData = new FormData(); + if (image) { + formData.append('image', image); + } + + formData.append('name', name); + formData.append('positive_prompt', positive_prompt); + formData.append('negative_prompt', negative_prompt); + + + return { + url: buildStylePresetsUrl(`i/${id}`), + method: 'PATCH', + body: formData + } + }, invalidatesTags: (response, error, { id }) => [ { type: 'StylePreset', id: LIST_TAG }, { type: 'StylePreset', id: id }, diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 6191582d22..0c41d337d6 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -561,6 +561,13 @@ export type paths = { */ post: operations["create_style_preset"]; }; + "/api/v1/style_presets/i/{style_preset_id}/image": { + /** + * Get Style Preset Image + * @description Gets an image file that previews the model + */ + get: operations["get_style_preset_image"]; + }; }; export type webhooks = Record; @@ -1146,8 +1153,26 @@ export type components = { }; /** Body_create_style_preset */ Body_create_style_preset: { - /** @description The style preset to create */ - style_preset: components["schemas"]["StylePresetWithoutId"]; + /** + * Name + * @description The name of the style preset to create + */ + name: string; + /** + * Positive Prompt + * @description The positive prompt of the style preset + */ + positive_prompt: string; + /** + * Negative Prompt + * @description The negative prompt of the style preset + */ + negative_prompt: string; + /** + * Image + * @description The image file to upload + */ + image?: Blob | null; }; /** Body_create_workflow */ Body_create_workflow: { @@ -1273,8 +1298,26 @@ export type components = { }; /** Body_update_style_preset */ Body_update_style_preset: { - /** @description The updated style preset */ - changes: components["schemas"]["StylePresetChanges"]; + /** + * Name + * @description The name of the style preset to create + */ + name: string; + /** + * Positive Prompt + * @description The positive prompt of the style preset + */ + positive_prompt: string; + /** + * Negative Prompt + * @description The negative prompt of the style preset + */ + negative_prompt: string; + /** + * Image + * @description The image file to upload + */ + image?: Blob | null; }; /** Body_update_workflow */ Body_update_workflow: { @@ -7345,147 +7388,147 @@ export type components = { project_id: string | null; }; InvocationOutputMap: { - esrgan: components["schemas"]["ImageOutput"]; - tiled_multi_diffusion_denoise_latents: components["schemas"]["LatentsOutput"]; - conditioning_collection: components["schemas"]["ConditioningCollectionOutput"]; - merge_metadata: components["schemas"]["MetadataOutput"]; - lresize: components["schemas"]["LatentsOutput"]; - string_split_neg: components["schemas"]["StringPosNegOutput"]; - img_channel_multiply: components["schemas"]["ImageOutput"]; - add: components["schemas"]["IntegerOutput"]; - lscale: components["schemas"]["LatentsOutput"]; - string_replace: components["schemas"]["StringOutput"]; - sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"]; - collect: components["schemas"]["CollectInvocationOutput"]; - face_off: components["schemas"]["FaceOffOutput"]; - ideal_size: components["schemas"]["IdealSizeOutput"]; - float_to_int: components["schemas"]["IntegerOutput"]; - mlsd_image_processor: components["schemas"]["ImageOutput"]; - sub: components["schemas"]["IntegerOutput"]; - midas_depth_image_processor: components["schemas"]["ImageOutput"]; - rectangle_mask: components["schemas"]["MaskOutput"]; - img_watermark: components["schemas"]["ImageOutput"]; - img_ilerp: components["schemas"]["ImageOutput"]; - pidi_image_processor: components["schemas"]["ImageOutput"]; - vae_loader: components["schemas"]["VAEOutput"]; - dynamic_prompt: components["schemas"]["StringCollectionOutput"]; - float_range: components["schemas"]["FloatCollectionOutput"]; - spandrel_image_to_image_autoscale: components["schemas"]["ImageOutput"]; - create_denoise_mask: components["schemas"]["DenoiseMaskOutput"]; - denoise_latents: components["schemas"]["LatentsOutput"]; - image_collection: components["schemas"]["ImageCollectionOutput"]; - t2i_adapter: components["schemas"]["T2IAdapterOutput"]; - normalbae_image_processor: components["schemas"]["ImageOutput"]; - calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"]; - mul: components["schemas"]["IntegerOutput"]; - image_mask_to_tensor: components["schemas"]["MaskOutput"]; - face_mask_detection: components["schemas"]["FaceMaskOutput"]; - save_image: components["schemas"]["ImageOutput"]; - blank_image: components["schemas"]["ImageOutput"]; - conditioning: components["schemas"]["ConditioningOutput"]; - img_chan: components["schemas"]["ImageOutput"]; - string_split: components["schemas"]["String2Output"]; - segment_anything_processor: components["schemas"]["ImageOutput"]; - unsharp_mask: components["schemas"]["ImageOutput"]; - boolean_collection: components["schemas"]["BooleanCollectionOutput"]; - color: components["schemas"]["ColorOutput"]; - range_of_size: components["schemas"]["IntegerCollectionOutput"]; - face_identifier: components["schemas"]["ImageOutput"]; - div: components["schemas"]["IntegerOutput"]; - invert_tensor_mask: components["schemas"]["MaskOutput"]; - step_param_easing: components["schemas"]["FloatCollectionOutput"]; - merge_tiles_to_image: components["schemas"]["ImageOutput"]; - latents: components["schemas"]["LatentsOutput"]; - lineart_anime_image_processor: components["schemas"]["ImageOutput"]; - mediapipe_face_processor: components["schemas"]["ImageOutput"]; - infill_lama: components["schemas"]["ImageOutput"]; - compel: components["schemas"]["ConditioningOutput"]; - round_float: components["schemas"]["FloatOutput"]; - string_join: components["schemas"]["StringOutput"]; - dw_openpose_image_processor: components["schemas"]["ImageOutput"]; - content_shuffle_image_processor: components["schemas"]["ImageOutput"]; - metadata: components["schemas"]["MetadataOutput"]; - lineart_image_processor: components["schemas"]["ImageOutput"]; - zoe_depth_image_processor: components["schemas"]["ImageOutput"]; - controlnet: components["schemas"]["ControlOutput"]; - sdxl_lora_collection_loader: components["schemas"]["SDXLLoRALoaderOutput"]; - img_lerp: components["schemas"]["ImageOutput"]; - scheduler: components["schemas"]["SchedulerOutput"]; - l2i: components["schemas"]["ImageOutput"]; - img_paste: components["schemas"]["ImageOutput"]; - tile_to_properties: components["schemas"]["TileToPropertiesOutput"]; - sdxl_refiner_compel_prompt: components["schemas"]["ConditioningOutput"]; - calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"]; - depth_anything_image_processor: components["schemas"]["ImageOutput"]; - prompt_from_file: components["schemas"]["StringCollectionOutput"]; integer: components["schemas"]["IntegerOutput"]; - img_mul: components["schemas"]["ImageOutput"]; + color_map_image_processor: components["schemas"]["ImageOutput"]; rand_int: components["schemas"]["IntegerOutput"]; - string_join_three: components["schemas"]["StringOutput"]; - img_blur: components["schemas"]["ImageOutput"]; - float: components["schemas"]["FloatOutput"]; - sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"]; - crop_latents: components["schemas"]["LatentsOutput"]; - model_identifier: components["schemas"]["ModelIdentifierOutput"]; - show_image: components["schemas"]["ImageOutput"]; - img_channel_offset: components["schemas"]["ImageOutput"]; - i2l: components["schemas"]["LatentsOutput"]; - core_metadata: components["schemas"]["MetadataOutput"]; infill_cv2: components["schemas"]["ImageOutput"]; - lora_selector: components["schemas"]["LoRASelectorOutput"]; - rand_float: components["schemas"]["FloatOutput"]; - float_math: components["schemas"]["FloatOutput"]; - main_model_loader: components["schemas"]["ModelLoaderOutput"]; - image: components["schemas"]["ImageOutput"]; - infill_patchmatch: components["schemas"]["ImageOutput"]; - noise: components["schemas"]["NoiseOutput"]; - tile_image_processor: components["schemas"]["ImageOutput"]; - spandrel_image_to_image: components["schemas"]["ImageOutput"]; - hed_image_processor: components["schemas"]["ImageOutput"]; - heuristic_resize: components["schemas"]["ImageOutput"]; - img_hue_adjust: components["schemas"]["ImageOutput"]; - create_gradient_mask: components["schemas"]["GradientMaskOutput"]; + img_scale: components["schemas"]["ImageOutput"]; + random_range: components["schemas"]["IntegerCollectionOutput"]; + latents: components["schemas"]["LatentsOutput"]; + cv_inpaint: components["schemas"]["ImageOutput"]; + float_range: components["schemas"]["FloatCollectionOutput"]; + img_pad_crop: components["schemas"]["ImageOutput"]; + range_of_size: components["schemas"]["IntegerCollectionOutput"]; + vae_loader: components["schemas"]["VAEOutput"]; + lora_collection_loader: components["schemas"]["LoRALoaderOutput"]; + freeu: components["schemas"]["UNetOutput"]; + float_collection: components["schemas"]["FloatCollectionOutput"]; + sdxl_compel_prompt: components["schemas"]["ConditioningOutput"]; calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"]; - canvas_paste_back: components["schemas"]["ImageOutput"]; - ip_adapter: components["schemas"]["IPAdapterOutput"]; - img_nsfw: components["schemas"]["ImageOutput"]; range: components["schemas"]["IntegerCollectionOutput"]; + string_replace: components["schemas"]["StringOutput"]; + boolean: components["schemas"]["BooleanOutput"]; + show_image: components["schemas"]["ImageOutput"]; + img_hue_adjust: components["schemas"]["ImageOutput"]; + metadata: components["schemas"]["MetadataOutput"]; + img_conv: components["schemas"]["ImageOutput"]; + sub: components["schemas"]["IntegerOutput"]; + pair_tile_image: components["schemas"]["PairTileImageOutput"]; + save_image: components["schemas"]["ImageOutput"]; + rectangle_mask: components["schemas"]["MaskOutput"]; + ideal_size: components["schemas"]["IdealSizeOutput"]; + lresize: components["schemas"]["LatentsOutput"]; + lblend: components["schemas"]["LatentsOutput"]; + conditioning_collection: components["schemas"]["ConditioningCollectionOutput"]; + rand_float: components["schemas"]["FloatOutput"]; + prompt_from_file: components["schemas"]["StringCollectionOutput"]; + tomask: components["schemas"]["ImageOutput"]; + boolean_collection: components["schemas"]["BooleanCollectionOutput"]; + color_correct: components["schemas"]["ImageOutput"]; + img_channel_offset: components["schemas"]["ImageOutput"]; + compel: components["schemas"]["ConditioningOutput"]; + infill_tile: components["schemas"]["ImageOutput"]; + img_resize: components["schemas"]["ImageOutput"]; + create_denoise_mask: components["schemas"]["DenoiseMaskOutput"]; + sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"]; + mlsd_image_processor: components["schemas"]["ImageOutput"]; + img_channel_multiply: components["schemas"]["ImageOutput"]; + latents_collection: components["schemas"]["LatentsCollectionOutput"]; + midas_depth_image_processor: components["schemas"]["ImageOutput"]; + string_collection: components["schemas"]["StringCollectionOutput"]; + mask_from_id: components["schemas"]["ImageOutput"]; string: components["schemas"]["StringOutput"]; + float: components["schemas"]["FloatOutput"]; + model_identifier: components["schemas"]["ModelIdentifierOutput"]; + pidi_image_processor: components["schemas"]["ImageOutput"]; + string_join: components["schemas"]["StringOutput"]; + spandrel_image_to_image_autoscale: components["schemas"]["ImageOutput"]; + lora_selector: components["schemas"]["LoRASelectorOutput"]; + clip_skip: components["schemas"]["CLIPSkipInvocationOutput"]; + image_mask_to_tensor: components["schemas"]["MaskOutput"]; + normalbae_image_processor: components["schemas"]["ImageOutput"]; + face_off: components["schemas"]["FaceOffOutput"]; + mul: components["schemas"]["IntegerOutput"]; + segment_anything_processor: components["schemas"]["ImageOutput"]; + round_float: components["schemas"]["FloatOutput"]; + sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"]; + denoise_latents: components["schemas"]["LatentsOutput"]; + string_split_neg: components["schemas"]["StringPosNegOutput"]; + string_split: components["schemas"]["String2Output"]; + invert_tensor_mask: components["schemas"]["MaskOutput"]; + main_model_loader: components["schemas"]["ModelLoaderOutput"]; + img_crop: components["schemas"]["ImageOutput"]; + img_watermark: components["schemas"]["ImageOutput"]; + dw_openpose_image_processor: components["schemas"]["ImageOutput"]; + add: components["schemas"]["IntegerOutput"]; + conditioning: components["schemas"]["ConditioningOutput"]; + esrgan: components["schemas"]["ImageOutput"]; + t2i_adapter: components["schemas"]["T2IAdapterOutput"]; + sdxl_refiner_compel_prompt: components["schemas"]["ConditioningOutput"]; + mediapipe_face_processor: components["schemas"]["ImageOutput"]; + img_chan: components["schemas"]["ImageOutput"]; + face_mask_detection: components["schemas"]["FaceMaskOutput"]; + lineart_image_processor: components["schemas"]["ImageOutput"]; + blank_image: components["schemas"]["ImageOutput"]; + image_collection: components["schemas"]["ImageCollectionOutput"]; + img_nsfw: components["schemas"]["ImageOutput"]; + unsharp_mask: components["schemas"]["ImageOutput"]; + scheduler: components["schemas"]["SchedulerOutput"]; + metadata_item: components["schemas"]["MetadataItemOutput"]; + crop_latents: components["schemas"]["LatentsOutput"]; + string_join_three: components["schemas"]["StringOutput"]; + content_shuffle_image_processor: components["schemas"]["ImageOutput"]; + zoe_depth_image_processor: components["schemas"]["ImageOutput"]; + depth_anything_image_processor: components["schemas"]["ImageOutput"]; + controlnet: components["schemas"]["ControlOutput"]; + mask_edge: components["schemas"]["ImageOutput"]; + img_ilerp: components["schemas"]["ImageOutput"]; + lora_loader: components["schemas"]["LoRALoaderOutput"]; + calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"]; + face_identifier: components["schemas"]["ImageOutput"]; + i2l: components["schemas"]["LatentsOutput"]; + infill_lama: components["schemas"]["ImageOutput"]; + sdxl_lora_collection_loader: components["schemas"]["SDXLLoRALoaderOutput"]; + mask_combine: components["schemas"]["ImageOutput"]; + noise: components["schemas"]["NoiseOutput"]; + div: components["schemas"]["IntegerOutput"]; + img_paste: components["schemas"]["ImageOutput"]; + create_gradient_mask: components["schemas"]["GradientMaskOutput"]; + iterate: components["schemas"]["IterateInvocationOutput"]; + merge_tiles_to_image: components["schemas"]["ImageOutput"]; + l2i: components["schemas"]["ImageOutput"]; + float_math: components["schemas"]["FloatOutput"]; + img_lerp: components["schemas"]["ImageOutput"]; + spandrel_image_to_image: components["schemas"]["ImageOutput"]; + tiled_multi_diffusion_denoise_latents: components["schemas"]["LatentsOutput"]; + ip_adapter: components["schemas"]["IPAdapterOutput"]; + step_param_easing: components["schemas"]["FloatCollectionOutput"]; + heuristic_resize: components["schemas"]["ImageOutput"]; + canny_image_processor: components["schemas"]["ImageOutput"]; + hed_image_processor: components["schemas"]["ImageOutput"]; + img_mul: components["schemas"]["ImageOutput"]; + merge_metadata: components["schemas"]["MetadataOutput"]; + color: components["schemas"]["ColorOutput"]; + lscale: components["schemas"]["LatentsOutput"]; + integer_math: components["schemas"]["IntegerOutput"]; + infill_rgba: components["schemas"]["ImageOutput"]; + lineart_anime_image_processor: components["schemas"]["ImageOutput"]; + tile_image_processor: components["schemas"]["ImageOutput"]; + img_blur: components["schemas"]["ImageOutput"]; + float_to_int: components["schemas"]["IntegerOutput"]; + alpha_mask_to_tensor: components["schemas"]["MaskOutput"]; + collect: components["schemas"]["CollectInvocationOutput"]; + tile_to_properties: components["schemas"]["TileToPropertiesOutput"]; + infill_patchmatch: components["schemas"]["ImageOutput"]; + image: components["schemas"]["ImageOutput"]; + leres_image_processor: components["schemas"]["ImageOutput"]; seamless: components["schemas"]["SeamlessModeOutput"]; integer_collection: components["schemas"]["IntegerCollectionOutput"]; - iterate: components["schemas"]["IterateInvocationOutput"]; - img_scale: components["schemas"]["ImageOutput"]; - pair_tile_image: components["schemas"]["PairTileImageOutput"]; - canny_image_processor: components["schemas"]["ImageOutput"]; - integer_math: components["schemas"]["IntegerOutput"]; - leres_image_processor: components["schemas"]["ImageOutput"]; - clip_skip: components["schemas"]["CLIPSkipInvocationOutput"]; - float_collection: components["schemas"]["FloatCollectionOutput"]; - string_collection: components["schemas"]["StringCollectionOutput"]; - lora_collection_loader: components["schemas"]["LoRALoaderOutput"]; - mask_combine: components["schemas"]["ImageOutput"]; - latents_collection: components["schemas"]["LatentsCollectionOutput"]; - mask_edge: components["schemas"]["ImageOutput"]; - alpha_mask_to_tensor: components["schemas"]["MaskOutput"]; - img_conv: components["schemas"]["ImageOutput"]; - color_correct: components["schemas"]["ImageOutput"]; - lblend: components["schemas"]["LatentsOutput"]; - mask_from_id: components["schemas"]["ImageOutput"]; - img_pad_crop: components["schemas"]["ImageOutput"]; - metadata_item: components["schemas"]["MetadataItemOutput"]; - lora_loader: components["schemas"]["LoRALoaderOutput"]; - cv_inpaint: components["schemas"]["ImageOutput"]; - color_map_image_processor: components["schemas"]["ImageOutput"]; - img_resize: components["schemas"]["ImageOutput"]; - random_range: components["schemas"]["IntegerCollectionOutput"]; - img_crop: components["schemas"]["ImageOutput"]; - freeu: components["schemas"]["UNetOutput"]; - infill_tile: components["schemas"]["ImageOutput"]; - infill_rgba: components["schemas"]["ImageOutput"]; - tomask: components["schemas"]["ImageOutput"]; - sdxl_compel_prompt: components["schemas"]["ConditioningOutput"]; - sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"]; - boolean: components["schemas"]["BooleanOutput"]; + core_metadata: components["schemas"]["MetadataOutput"]; + dynamic_prompt: components["schemas"]["StringCollectionOutput"]; + calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"]; + sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"]; + canvas_paste_back: components["schemas"]["ImageOutput"]; }; /** * InvocationStartedEvent @@ -12642,18 +12685,8 @@ export type components = { */ type: "string_split_neg"; }; - /** StylePresetChanges */ - StylePresetChanges: { - /** - * Name - * @description The style preset's new name. - */ - name?: string | null; - /** @description The updated data for style preset. */ - preset_data?: components["schemas"]["PresetData"] | null; - }; - /** StylePresetRecordDTO */ - StylePresetRecordDTO: { + /** StylePresetRecordWithImage */ + StylePresetRecordWithImage: { /** * Name * @description The name of the style preset. @@ -12671,16 +12704,11 @@ export type components = { * @description Whether or not the style preset is default */ is_default: boolean; - }; - /** StylePresetWithoutId */ - StylePresetWithoutId: { /** - * Name - * @description The name of the style preset. + * Image + * @description The path for image */ - name: string; - /** @description The preset data */ - preset_data: components["schemas"]["PresetData"]; + image: string | null; }; /** * SubModelType @@ -16224,7 +16252,7 @@ export type operations = { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["StylePresetRecordDTO"]; + "application/json": components["schemas"]["StylePresetRecordWithImage"]; }; }; /** @description Validation Error */ @@ -16274,14 +16302,14 @@ export type operations = { }; requestBody: { content: { - "application/json": components["schemas"]["Body_update_style_preset"]; + "multipart/form-data": components["schemas"]["Body_update_style_preset"]; }; }; responses: { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["StylePresetRecordDTO"]; + "application/json": components["schemas"]["StylePresetRecordWithImage"]; }; }; /** @description Validation Error */ @@ -16301,7 +16329,7 @@ export type operations = { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["StylePresetRecordDTO"][]; + "application/json": components["schemas"]["StylePresetRecordWithImage"][]; }; }; }; @@ -16313,14 +16341,14 @@ export type operations = { create_style_preset: { requestBody: { content: { - "application/json": components["schemas"]["Body_create_style_preset"]; + "multipart/form-data": components["schemas"]["Body_create_style_preset"]; }; }; responses: { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["StylePresetRecordDTO"]; + "application/json": components["schemas"]["StylePresetRecordWithImage"]; }; }; /** @description Validation Error */ @@ -16331,4 +16359,38 @@ export type operations = { }; }; }; + /** + * Get Style Preset Image + * @description Gets an image file that previews the model + */ + get_style_preset_image: { + parameters: { + path: { + /** @description The id of the style preset image to get */ + style_preset_id: string; + }; + }; + responses: { + /** @description The style preset image was fetched successfully */ + 200: { + content: { + "application/json": unknown; + }; + }; + /** @description Bad request */ + 400: { + content: never; + }; + /** @description The style preset image could not be found */ + 404: { + content: never; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; }; diff --git a/style_preset_images/style_preset_images/4b356226-204a-4bc4-b803-01a62af8b5e9.webp b/style_preset_images/style_preset_images/4b356226-204a-4bc4-b803-01a62af8b5e9.webp new file mode 100644 index 0000000000000000000000000000000000000000..07333aeb0487bb4fb93dc9d0fd54bc4931afe706 GIT binary patch literal 12594 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 UH|B5amYZOsNG!iT6W{;<0QT0GwEzGB literal 0 HcmV?d00001 diff --git a/style_preset_images/style_preset_images/8f6890b8-e1b1-41f2-8d78-e13d09341b3a.webp b/style_preset_images/style_preset_images/8f6890b8-e1b1-41f2-8d78-e13d09341b3a.webp new file mode 100644 index 0000000000000000000000000000000000000000..577c2e21a101f6b66de24c05a0991378794fa5ab GIT binary patch literal 4414 zcmV-E5y9?KNk&FC5dZ*JMM6+kP&gne5dZ*CRsfv=DgXfh0X}UumPe!`BB7|1xln)& ziA~>+j4fCQn)DpR{%`XS+gi>36Y?)dN(b#@&c3hxTz;betL_#0>C}vD8Q=?}a}}t|mAy0l4aJeZm5sT!+53i0$qz4rO5rNDW!`jRA8 zu)G`(hYU2bwJt}3IGzyfo{8`Li7g0YRbxLmX_WDiMY7iJndhx&s{n#50tJSzOx+j) zV3QJ1Hf~Q3C65`r)O}HG*LZ!%S9mn8Uh@ZmV38JJR~RpXX1R+T%vT8X6*yLScxt{Y z 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 literal 0 HcmV?d00001