style preset images

This commit is contained in:
Mary Hipp 2024-08-07 09:58:27 -04:00
parent 2604fd9fde
commit cc96dcf0ed
28 changed files with 786 additions and 255 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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=<path>", "syslog=path|address:host:port", "http=<url>".')

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -39,3 +39,7 @@ class StylePresetRecordDTO(StylePresetWithoutId):
StylePresetRecordDTOValidator = TypeAdapter(StylePresetRecordDTO)
class StylePresetRecordWithImage(StylePresetRecordDTO):
image: Optional[str] = Field(description="The path for image")

View File

@ -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

View File

@ -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"

View File

@ -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 (
<>
<MenuItem as="a" href={imageDTO.image_url} target="_blank" icon={<PiShareFatBold />}>
@ -182,6 +190,13 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
>
{t('parameters.useAll')}
</MenuItem>
<MenuItem
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiPaintBrushBold />}
onClickCapture={handleCreatePreset}
isDisabled={isLoadingMetadata || !hasPrompts}
>
Create Preset
</MenuItem>
<MenuDivider />
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToImageToImage} id="send-to-img2img">
{t('parameters.sendToImg2Img')}

View File

@ -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 = () => {
<>
<Flex justifyContent="space-between" w="full" alignItems="center">
<Flex gap="2">
<ModelImage image_url={null} />
<StylePresetImage presetImageUrl={activeStylePreset.image} />
<Flex flexDir="column">
<Text variant="subtext" fontSize="xs">
Prompt Style

View File

@ -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<SubmitHandler<StylePresetFormData>>(
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 (
<Flex flexDir="column" gap="4">
<FormControl>
<FormLabel>Name</FormLabel>
<Input size="md" {...register('name')} />
</FormControl>
<Flex flexDir="column" bgColor="base.750" borderRadius="base" padding="10px" gap="10px">
<Text variant="subtext">
Use the <Icon as={PiBracketsCurlyBold} /> 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.
</Text>
<StylePresetPromptField label="Positive Prompt" control={control} name="positivePrompt" />
<StylePresetPromptField label="Negative Prompt" control={control} name="negativePrompt" />
<Flex alignItems="center" gap="4">
<StylePresetImageField control={control} name="image" />
<FormControl orientation="vertical">
<FormLabel>Name</FormLabel>
<Input size="md" {...register('name')} />
</FormControl>
</Flex>
<StylePresetPromptField label="Positive Prompt" control={control} name="positivePrompt" />
<StylePresetPromptField label="Negative Prompt" control={control} name="negativePrompt" />
<Text variant="subtext">
Use the <Icon as={PiBracketsCurlyBold} /> 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.
</Text>
<Button onClick={handleSubmit(handleClickSave)}>Save</Button>
</Flex>
);

View File

@ -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 (
<Image
src={presetImageUrl || ''}
fallbackStrategy="beforeLoadOrError"
fallback={
<Flex
height={IMAGE_THUMBNAIL_SIZE}
minWidth={IMAGE_THUMBNAIL_SIZE}
bg="base.650"
borderRadius="base"
alignItems="center"
justifyContent="center"
>
<Icon color="base.500" as={PiImage} boxSize={FALLBACK_ICON_SIZE} />
</Flex>
}
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);

View File

@ -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<StylePresetFormData>) => {
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 (
<Box position="relative" flexShrink={0}>
<Image
src={URL.createObjectURL(field.value as File)}
objectFit="cover"
objectPosition="50% 50%"
w={65}
h={65}
minWidth={65}
borderRadius="base"
/>
<IconButton
position="absolute"
insetInlineEnd={0}
insetBlockStart={0}
onClick={handleResetImage}
aria-label={t('modelManager.deleteModelImage')}
tooltip={t('modelManager.deleteModelImage')}
icon={<PiArrowCounterClockwiseBold />}
size="md"
variant="ghost"
/>
</Box>
);
}
return (
<>
<Tooltip label={t('modelManager.uploadImage')}>
<Flex
as={Button}
w={65}
h={65}
opacity={0.3}
borderRadius="base"
alignItems="center"
justifyContent="center"
flexShrink={0}
{...getRootProps()}
>
<Icon as={PiUploadSimpleBold} w={8} h={8} />
</Flex>
</Tooltip>
<input {...getInputProps()} />
</>
);
};

View File

@ -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) {

View File

@ -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<HTMLButtonElement>) => {
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<HTMLButtonElement>) => {
e.stopPropagation();
try {
await deleteStylePreset(preset.id);
} catch (error) {}
},
[preset]
);
return (
<Flex
@ -40,7 +50,7 @@ export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordDTO }
alignItems="center"
w="full"
>
<ModelImage image_url={null} />
<StylePresetImage presetImageUrl={preset.image} />
<Flex flexDir="column" w="full">
<Flex w="full" justifyContent="space-between">
<Flex alignItems="center" gap="2">

View File

@ -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 {

View File

@ -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 (
<FormControl orientation="vertical">

View File

@ -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;
};

View File

@ -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<boolean>) => {
state.isModalOpen = action.payload;
},
updatingStylePresetChanged: (state, action: PayloadAction<StylePresetRecordDTO | null>) => {
updatingStylePresetChanged: (state, action: PayloadAction<StylePresetRecordWithImage | null>) => {
state.updatingStylePreset = action.payload;
},
createPresetFromImageChanged: (state, action: PayloadAction<ImageDTO | null>) => {
state.createPresetFromImage = action.payload;
},
},
});
export const { isModalOpenChanged, updatingStylePresetChanged } = stylePresetModalSlice.actions;
export const { isModalOpenChanged, updatingStylePresetChanged, createPresetFromImageChanged } = stylePresetModalSlice.actions;
export const selectStylePresetModalSlice = (state: RootState) => state.stylePresetModal;

View File

@ -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<boolean>) => {
state.isMenuOpen = action.payload;
},
activeStylePresetChanged: (state, action: PayloadAction<StylePresetRecordDTO | null>) => {
activeStylePresetChanged: (state, action: PayloadAction<StylePresetRecordWithImage | null>) => {
state.activeStylePreset = action.payload;
},
searchTermChanged: (state, action: PayloadAction<string>) => {

View File

@ -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
}

View File

@ -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 },

View File

@ -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<string, never>;
@ -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"];
};
};
};
};
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB