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 0000000000..07333aeb04 Binary files /dev/null and b/style_preset_images/style_preset_images/4b356226-204a-4bc4-b803-01a62af8b5e9.webp differ 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 0000000000..577c2e21a1 Binary files /dev/null and b/style_preset_images/style_preset_images/8f6890b8-e1b1-41f2-8d78-e13d09341b3a.webp differ diff --git a/style_preset_images/style_preset_images/ab1a7540-be87-44fc-8844-00739d67cabc.webp b/style_preset_images/style_preset_images/ab1a7540-be87-44fc-8844-00739d67cabc.webp new file mode 100644 index 0000000000..ddd8f84cb1 Binary files /dev/null and b/style_preset_images/style_preset_images/ab1a7540-be87-44fc-8844-00739d67cabc.webp differ