diff --git a/.github/workflows/python-checks.yml b/.github/workflows/python-checks.yml
index 94232e1576..40d028826b 100644
--- a/.github/workflows/python-checks.yml
+++ b/.github/workflows/python-checks.yml
@@ -62,7 +62,7 @@ jobs:
- name: install ruff
if: ${{ steps.changed-files.outputs.python_any_changed == 'true' || inputs.always_run == true }}
- run: pip install ruff
+ run: pip install ruff==0.6.0
shell: bash
- name: ruff check
diff --git a/invokeai/app/api/routers/style_presets.py b/invokeai/app/api/routers/style_presets.py
index 786c522c20..14d8c666aa 100644
--- a/invokeai/app/api/routers/style_presets.py
+++ b/invokeai/app/api/routers/style_presets.py
@@ -13,12 +13,15 @@ from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.api.routers.model_manager import IMAGE_MAX_AGE
from invokeai.app.services.style_preset_images.style_preset_images_common import StylePresetImageFileNotFoundException
from invokeai.app.services.style_preset_records.style_preset_records_common import (
+ InvalidPresetImportDataError,
PresetData,
PresetType,
StylePresetChanges,
StylePresetNotFoundError,
StylePresetRecordWithImage,
StylePresetWithoutId,
+ UnsupportedFileTypeError,
+ parse_presets_from_file,
)
@@ -225,3 +228,19 @@ async def get_style_preset_image(
return response
except Exception:
raise HTTPException(status_code=404)
+
+
+@style_presets_router.post(
+ "/import",
+ operation_id="import_style_presets",
+)
+async def import_style_presets(file: UploadFile = File(description="The file to import")):
+ try:
+ style_presets = await parse_presets_from_file(file)
+ ApiDependencies.invoker.services.style_preset_records.create_many(style_presets)
+ except InvalidPresetImportDataError as e:
+ ApiDependencies.invoker.services.logger.error(traceback.format_exc())
+ raise HTTPException(status_code=400, detail=str(e))
+ except UnsupportedFileTypeError as e:
+ ApiDependencies.invoker.services.logger.error(traceback.format_exc())
+ raise HTTPException(status_code=415, detail=str(e))
diff --git a/invokeai/app/services/style_preset_records/default_style_presets.json b/invokeai/app/services/style_preset_records/default_style_presets.json
index aa9ede40dc..1daadfa8ff 100644
--- a/invokeai/app/services/style_preset_records/default_style_presets.json
+++ b/invokeai/app/services/style_preset_records/default_style_presets.json
@@ -3,7 +3,7 @@
"name": "Photography (General)",
"type": "default",
"preset_data": {
- "positive_prompt": "[prompt]. photography. f/2.8 macro photo, bokeh, photorealism",
+ "positive_prompt": "{prompt}. photography. f/2.8 macro photo, bokeh, photorealism",
"negative_prompt": "painting, digital art. sketch, blurry"
}
},
@@ -11,7 +11,7 @@
"name": "Photography (Studio Lighting)",
"type": "default",
"preset_data": {
- "positive_prompt": "[prompt], photography. f/8 photo. centered subject, studio lighting.",
+ "positive_prompt": "{prompt}, photography. f/8 photo. centered subject, studio lighting.",
"negative_prompt": "painting, digital art. sketch, blurry"
}
},
@@ -19,7 +19,7 @@
"name": "Photography (Landscape)",
"type": "default",
"preset_data": {
- "positive_prompt": "[prompt], landscape photograph, f/12, lifelike, highly detailed.",
+ "positive_prompt": "{prompt}, landscape photograph, f/12, lifelike, highly detailed.",
"negative_prompt": "painting, digital art. sketch, blurry"
}
},
@@ -27,7 +27,7 @@
"name": "Photography (Portrait)",
"type": "default",
"preset_data": {
- "positive_prompt": "[prompt]. photography. portraiture. catch light in eyes. one flash. rembrandt lighting. Soft box. dark shadows. High contrast. 80mm lens. F2.8.",
+ "positive_prompt": "{prompt}. photography. portraiture. catch light in eyes. one flash. rembrandt lighting. Soft box. dark shadows. High contrast. 80mm lens. F2.8.",
"negative_prompt": "painting, digital art. sketch, blurry"
}
},
@@ -35,7 +35,7 @@
"name": "Photography (Black and White)",
"type": "default",
"preset_data": {
- "positive_prompt": "[prompt] photography. natural light. 80mm lens. F1.4. strong contrast, hard light. dark contrast. blurred background. black and white",
+ "positive_prompt": "{prompt} photography. natural light. 80mm lens. F1.4. strong contrast, hard light. dark contrast. blurred background. black and white",
"negative_prompt": "painting, digital art. sketch, colour+"
}
},
@@ -43,7 +43,7 @@
"name": "Architectural Visualization",
"type": "default",
"preset_data": {
- "positive_prompt": "[prompt]. architectural photography, f/12, luxury, aesthetically pleasing form and function.",
+ "positive_prompt": "{prompt}. architectural photography, f/12, luxury, aesthetically pleasing form and function.",
"negative_prompt": "painting, digital art. sketch, blurry"
}
},
@@ -51,7 +51,7 @@
"name": "Concept Art (Fantasy)",
"type": "default",
"preset_data": {
- "positive_prompt": "concept artwork of a [prompt]. (digital painterly art style)++, mythological, (textured 2d dry media brushpack)++, glazed brushstrokes, otherworldly. painting+, illustration+",
+ "positive_prompt": "concept artwork of a {prompt}. (digital painterly art style)++, mythological, (textured 2d dry media brushpack)++, glazed brushstrokes, otherworldly. painting+, illustration+",
"negative_prompt": "photo. distorted, blurry, out of focus. sketch. (cgi, 3d.)++"
}
},
@@ -59,7 +59,7 @@
"name": "Concept Art (Sci-Fi)",
"type": "default",
"preset_data": {
- "positive_prompt": "(concept art)++, [prompt], (sleek futurism)++, (textured 2d dry media)++, metallic highlights, digital painting style",
+ "positive_prompt": "(concept art)++, {prompt}, (sleek futurism)++, (textured 2d dry media)++, metallic highlights, digital painting style",
"negative_prompt": "photo. distorted, blurry, out of focus. sketch. (cgi, 3d.)++"
}
},
@@ -67,7 +67,7 @@
"name": "Concept Art (Character)",
"type": "default",
"preset_data": {
- "positive_prompt": "(character concept art)++, stylized painterly digital painting of [prompt], (painterly, impasto. Dry brush.)++",
+ "positive_prompt": "(character concept art)++, stylized painterly digital painting of {prompt}, (painterly, impasto. Dry brush.)++",
"negative_prompt": "photo. distorted, blurry, out of focus. sketch. (cgi, 3d.)++"
}
},
@@ -75,7 +75,7 @@
"name": "Concept Art (Painterly)",
"type": "default",
"preset_data": {
- "positive_prompt": "[prompt] oil painting. high contrast. impasto. sfumato. chiaroscuro. Palette knife.",
+ "positive_prompt": "{prompt} oil painting. high contrast. impasto. sfumato. chiaroscuro. Palette knife.",
"negative_prompt": "photo. smooth. border. frame"
}
},
@@ -83,7 +83,7 @@
"name": "Environment Art",
"type": "default",
"preset_data": {
- "positive_prompt": "[prompt] environment artwork, hyper-realistic digital painting style with cinematic composition, atmospheric, depth and detail, voluminous. textured dry brush 2d media",
+ "positive_prompt": "{prompt} environment artwork, hyper-realistic digital painting style with cinematic composition, atmospheric, depth and detail, voluminous. textured dry brush 2d media",
"negative_prompt": "photo, distorted, blurry, out of focus. sketch."
}
},
@@ -91,7 +91,7 @@
"name": "Interior Design (Visualization)",
"type": "default",
"preset_data": {
- "positive_prompt": "[prompt] interior design photo, gentle shadows, light mid-tones, dimension, mix of smooth and textured surfaces, focus on negative space and clean lines, focus",
+ "positive_prompt": "{prompt} interior design photo, gentle shadows, light mid-tones, dimension, mix of smooth and textured surfaces, focus on negative space and clean lines, focus",
"negative_prompt": "photo, distorted. sketch."
}
},
@@ -99,7 +99,7 @@
"name": "Product Rendering",
"type": "default",
"preset_data": {
- "positive_prompt": "[prompt] high quality product photography, 3d rendering with key lighting, shallow depth of field, simple plain background, studio lighting.",
+ "positive_prompt": "{prompt} high quality product photography, 3d rendering with key lighting, shallow depth of field, simple plain background, studio lighting.",
"negative_prompt": "blurry, sketch, messy, dirty. unfinished."
}
},
@@ -107,7 +107,7 @@
"name": "Sketch",
"type": "default",
"preset_data": {
- "positive_prompt": "[prompt] black and white pencil drawing, off-center composition, cross-hatching for shadows, bold strokes, textured paper. sketch+++",
+ "positive_prompt": "{prompt} black and white pencil drawing, off-center composition, cross-hatching for shadows, bold strokes, textured paper. sketch+++",
"negative_prompt": "blurry, photo, painting, color. messy, dirty. unfinished. frame, borders."
}
},
@@ -115,7 +115,7 @@
"name": "Line Art",
"type": "default",
"preset_data": {
- "positive_prompt": "[prompt] Line art. bold outline. simplistic. white background. 2d",
+ "positive_prompt": "{prompt} Line art. bold outline. simplistic. white background. 2d",
"negative_prompt": "photo. digital art. greyscale. solid black. painting"
}
},
@@ -123,7 +123,7 @@
"name": "Anime",
"type": "default",
"preset_data": {
- "positive_prompt": "[prompt] anime++, bold outline, cel-shaded coloring, shounen, seinen",
+ "positive_prompt": "{prompt} anime++, bold outline, cel-shaded coloring, shounen, seinen",
"negative_prompt": "(photo)+++. greyscale. solid black. painting"
}
},
@@ -131,7 +131,7 @@
"name": "Illustration",
"type": "default",
"preset_data": {
- "positive_prompt": "[prompt] illustration, bold linework, illustrative details, vector art style, flat coloring",
+ "positive_prompt": "{prompt} illustration, bold linework, illustrative details, vector art style, flat coloring",
"negative_prompt": "(photo)+++. greyscale. painting, black and white."
}
},
@@ -139,7 +139,7 @@
"name": "Vehicles",
"type": "default",
"preset_data": {
- "positive_prompt": "A weird futuristic normal auto, [prompt] elegant design, nice color, nice wheels",
+ "positive_prompt": "A weird futuristic normal auto, {prompt} elegant design, nice color, nice wheels",
"negative_prompt": "sketch. digital art. greyscale. painting"
}
}
diff --git a/invokeai/app/services/style_preset_records/style_preset_records_base.py b/invokeai/app/services/style_preset_records/style_preset_records_base.py
index 9e3a504e06..282388c7e4 100644
--- a/invokeai/app/services/style_preset_records/style_preset_records_base.py
+++ b/invokeai/app/services/style_preset_records/style_preset_records_base.py
@@ -20,6 +20,11 @@ class StylePresetRecordsStorageBase(ABC):
"""Creates a style preset."""
pass
+ @abstractmethod
+ def create_many(self, style_presets: list[StylePresetWithoutId]) -> None:
+ """Creates many style presets."""
+ pass
+
@abstractmethod
def update(self, style_preset_id: str, changes: StylePresetChanges) -> StylePresetRecordDTO:
"""Updates a style preset."""
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 964489b54d..34a30d0377 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
@@ -1,7 +1,12 @@
+import codecs
+import csv
+import json
from enum import Enum
from typing import Any, Optional
-from pydantic import BaseModel, Field, TypeAdapter
+import pydantic
+from fastapi import UploadFile
+from pydantic import AliasChoices, BaseModel, ConfigDict, Field, TypeAdapter
from invokeai.app.util.metaenum import MetaEnum
@@ -49,3 +54,85 @@ StylePresetRecordDTOValidator = TypeAdapter(StylePresetRecordDTO)
class StylePresetRecordWithImage(StylePresetRecordDTO):
image: Optional[str] = Field(description="The path for image")
+
+
+class StylePresetImportRow(BaseModel):
+ name: str = Field(min_length=1, description="The name of the preset.")
+ positive_prompt: str = Field(
+ default="",
+ description="The positive prompt for the preset.",
+ validation_alias=AliasChoices("positive_prompt", "prompt"),
+ )
+ negative_prompt: str = Field(default="", description="The negative prompt for the preset.")
+
+ model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
+
+
+StylePresetImportList = list[StylePresetImportRow]
+StylePresetImportListTypeAdapter = TypeAdapter(StylePresetImportList)
+
+
+class UnsupportedFileTypeError(ValueError):
+ """Raised when an unsupported file type is encountered"""
+
+ pass
+
+
+class InvalidPresetImportDataError(ValueError):
+ """Raised when invalid preset import data is encountered"""
+
+ pass
+
+
+async def parse_presets_from_file(file: UploadFile) -> list[StylePresetWithoutId]:
+ """Parses style presets from a file. The file must be a CSV or JSON file.
+
+ If CSV, the file must have the following columns:
+ - name
+ - prompt (or positive_prompt)
+ - negative_prompt
+
+ If JSON, the file must be a list of objects with the following keys:
+ - name
+ - prompt (or positive_prompt)
+ - negative_prompt
+
+ Args:
+ file (UploadFile): The file to parse.
+
+ Returns:
+ list[StylePresetWithoutId]: The parsed style presets.
+
+ Raises:
+ UnsupportedFileTypeError: If the file type is not supported.
+ InvalidPresetImportDataError: If the data in the file is invalid.
+ """
+ if file.content_type not in ["text/csv", "application/json"]:
+ raise UnsupportedFileTypeError()
+
+ if file.content_type == "text/csv":
+ csv_reader = csv.DictReader(codecs.iterdecode(file.file, "utf-8"))
+ data = list(csv_reader)
+ else: # file.content_type == "application/json":
+ json_data = await file.read()
+ data = json.loads(json_data)
+
+ try:
+ imported_presets = StylePresetImportListTypeAdapter.validate_python(data)
+
+ style_presets: list[StylePresetWithoutId] = []
+
+ for imported in imported_presets:
+ preset_data = PresetData(positive_prompt=imported.positive_prompt, negative_prompt=imported.negative_prompt)
+ style_preset = StylePresetWithoutId(name=imported.name, preset_data=preset_data, type=PresetType.User)
+ style_presets.append(style_preset)
+ except pydantic.ValidationError as e:
+ if file.content_type == "text/csv":
+ msg = "Invalid CSV format: must include columns 'name', 'prompt', and 'negative_prompt'"
+ else: # file.content_type == "application/json":
+ msg = "Invalid JSON format: must be a list of objects with keys 'name', 'prompt', and 'negative_prompt'"
+ raise InvalidPresetImportDataError(msg) from e
+ finally:
+ file.file.close()
+
+ return style_presets
diff --git a/invokeai/app/services/style_preset_records/style_preset_records_sqlite.py b/invokeai/app/services/style_preset_records/style_preset_records_sqlite.py
index a98ff462f2..952cf35ba9 100644
--- a/invokeai/app/services/style_preset_records/style_preset_records_sqlite.py
+++ b/invokeai/app/services/style_preset_records/style_preset_records_sqlite.py
@@ -75,6 +75,39 @@ class SqliteStylePresetRecordsStorage(StylePresetRecordsStorageBase):
self._lock.release()
return self.get(style_preset_id)
+ def create_many(self, style_presets: list[StylePresetWithoutId]) -> None:
+ style_preset_ids = []
+ try:
+ self._lock.acquire()
+ for style_preset in style_presets:
+ style_preset_id = uuid_string()
+ style_preset_ids.append(style_preset_id)
+ self._cursor.execute(
+ """--sql
+ INSERT OR IGNORE INTO style_presets (
+ id,
+ name,
+ preset_data,
+ type
+ )
+ VALUES (?, ?, ?, ?);
+ """,
+ (
+ style_preset_id,
+ style_preset.name,
+ style_preset.preset_data.model_dump_json(),
+ style_preset.type,
+ ),
+ )
+ self._conn.commit()
+ except Exception:
+ self._conn.rollback()
+ raise
+ finally:
+ self._lock.release()
+
+ return None
+
def update(self, style_preset_id: str, changes: StylePresetChanges) -> StylePresetRecordDTO:
try:
self._lock.acquire()
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 5b99a7c11e..ec0ae472bb 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -1141,6 +1141,8 @@
"imageSavingFailed": "Image Saving Failed",
"imageUploaded": "Image Uploaded",
"imageUploadFailed": "Image Upload Failed",
+ "importFailed": "Import Failed",
+ "importSuccessful": "Import Successful",
"invalidUpload": "Invalid Upload",
"loadedWithWarnings": "Workflow Loaded with Warnings",
"maskSavedAssets": "Mask Saved to Assets",
@@ -1701,6 +1703,8 @@
"deleteTemplate2": "Are you sure you want to delete this template? This cannot be undone.",
"editTemplate": "Edit Template",
"flatten": "Flatten selected template into current prompt",
+ "importTemplates": "Import Prompt Templates",
+ "importTemplatesDesc": "Format must be either a CSV with columns: 'name', 'prompt' or 'positive_prompt', and 'negative_prompt' included, or a JSON file with keys 'name', 'prompt' or 'positive_prompt', and 'negative_prompt",
"insertPlaceholder": "Insert placeholder",
"myTemplates": "My Templates",
"name": "Name",
diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetImport.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetImport.tsx
new file mode 100644
index 0000000000..9a248e0e0f
--- /dev/null
+++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetImport.tsx
@@ -0,0 +1,69 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import { IconButton, spinAnimation, Text } from '@invoke-ai/ui-library';
+import { toast } from 'features/toast/toast';
+import { useCallback } from 'react';
+import { useDropzone } from 'react-dropzone';
+import { useTranslation } from 'react-i18next';
+import { PiSpinner, PiUploadBold } from 'react-icons/pi';
+import { useImportStylePresetsMutation } from 'services/api/endpoints/stylePresets';
+
+const loadingStyles: SystemStyleObject = {
+ svg: { animation: spinAnimation },
+};
+
+export const StylePresetImport = () => {
+ const [importStylePresets, { isLoading }] = useImportStylePresetsMutation();
+ const { t } = useTranslation();
+
+ const onDropAccepted = useCallback(
+ async (files: File[]) => {
+ const file = files[0];
+ if (!file) {
+ return;
+ }
+ try {
+ await importStylePresets(file).unwrap();
+ toast({
+ status: 'success',
+ title: t('toast.importSuccessful'),
+ });
+ } catch (error) {
+ toast({
+ status: 'error',
+ title: t('toast.importFailed'),
+ });
+ }
+ },
+ [importStylePresets, t]
+ );
+
+ const { getInputProps, getRootProps } = useDropzone({
+ accept: { 'text/csv': ['.csv'], 'application/json': ['.json'] },
+ onDropAccepted,
+ noDrag: true,
+ multiple: false,
+ });
+
+ return (
+ <>
+ : }
+ tooltip={
+ <>
+ {t('stylePresets.importTemplates')}
+ {t('stylePresets.importTemplatesDesc')}
+ >
+ }
+ aria-label={t('stylePresets.importTemplates')}
+ size="md"
+ variant="link"
+ w={8}
+ h={8}
+ sx={isLoading ? loadingStyles : undefined}
+ isDisabled={isLoading}
+ {...getRootProps()}
+ />
+
+ >
+ );
+};
diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenu.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenu.tsx
index 021c274048..b024a246fd 100644
--- a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenu.tsx
+++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenu.tsx
@@ -8,6 +8,7 @@ import { PiPlusBold } from 'react-icons/pi';
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
+import { StylePresetImport } from './StylePresetImport';
import { StylePresetList } from './StylePresetList';
import StylePresetSearch from './StylePresetSearch';
@@ -60,16 +61,20 @@ export const StylePresetMenu = () => {
- }
- tooltip={t('stylePresets.createPromptTemplate')}
- aria-label={t('stylePresets.createPromptTemplate')}
- onClick={handleClickAddNew}
- size="md"
- variant="link"
- w={8}
- h={8}
- />
+
+
+
+ }
+ tooltip={t('stylePresets.createPromptTemplate')}
+ aria-label={t('stylePresets.createPromptTemplate')}
+ onClick={handleClickAddNew}
+ size="md"
+ variant="link"
+ w={8}
+ h={8}
+ />
+
{data.presets.length === 0 && data.defaultPresets.length === 0 && (
diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenuTrigger.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenuTrigger.tsx
index ac623a4963..bb1c494d88 100644
--- a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenuTrigger.tsx
+++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenuTrigger.tsx
@@ -1,3 +1,4 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex, IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $isMenuOpen } from 'features/stylePresets/store/isMenuOpen';
@@ -7,6 +8,10 @@ import { PiCaretDownBold } from 'react-icons/pi';
import { ActiveStylePreset } from './ActiveStylePreset';
+const _hover: SystemStyleObject = {
+ bg: 'base.750',
+};
+
export const StylePresetMenuTrigger = () => {
const isMenuOpen = useStore($isMenuOpen);
const { t } = useTranslation();
@@ -26,6 +31,9 @@ export const StylePresetMenuTrigger = () => {
borderRadius="base"
gap={1}
role="button"
+ _hover={_hover}
+ transitionProperty="background-color"
+ transitionDuration="normal"
>
diff --git a/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts b/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts
index 0b4d97d53b..121840db67 100644
--- a/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts
+++ b/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts
@@ -1,7 +1,7 @@
import { useAppSelector } from 'app/store/storeHooks';
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
-export const PRESET_PLACEHOLDER = `[prompt]`;
+export const PRESET_PLACEHOLDER = '{prompt}';
export const buildPresetModifiedPrompt = (presetPrompt: string, currentPrompt: string) => {
return presetPrompt.includes(PRESET_PLACEHOLDER)
diff --git a/invokeai/frontend/web/src/services/api/endpoints/stylePresets.ts b/invokeai/frontend/web/src/services/api/endpoints/stylePresets.ts
index 2e1b1a7108..7820fbd1f7 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/stylePresets.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/stylePresets.ts
@@ -92,6 +92,23 @@ export const stylePresetsApi = api.injectEndpoints({
}),
providesTags: ['FetchOnReconnect', { type: 'StylePreset', id: LIST_TAG }],
}),
+ importStylePresets: build.mutation<
+ paths['/api/v1/style_presets/import']['post']['responses']['200']['content']['application/json'],
+ paths['/api/v1/style_presets/import']['post']['requestBody']['content']['multipart/form-data']['file']
+ >({
+ query: (file) => {
+ const formData = new FormData();
+
+ formData.append('file', file);
+
+ return {
+ url: buildStylePresetsUrl('import'),
+ method: 'POST',
+ body: formData,
+ };
+ },
+ invalidatesTags: [{ type: 'StylePreset', id: LIST_TAG }],
+ }),
}),
});
@@ -100,4 +117,5 @@ export const {
useDeleteStylePresetMutation,
useUpdateStylePresetMutation,
useListStylePresetsQuery,
+ useImportStylePresetsMutation,
} = stylePresetsApi;
diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts
index 02c1c88412..52e8f7dd13 100644
--- a/invokeai/frontend/web/src/services/api/schema.ts
+++ b/invokeai/frontend/web/src/services/api/schema.ts
@@ -1344,6 +1344,23 @@ export type paths = {
patch?: never;
trace?: never;
};
+ "/api/v1/style_presets/import": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Import Style Presets */
+ post: operations["import_style_presets"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
};
export type webhooks = Record;
export type components = {
@@ -1998,6 +2015,15 @@ export type components = {
*/
prepend?: boolean;
};
+ /** Body_import_style_presets */
+ Body_import_style_presets: {
+ /**
+ * File
+ * Format: binary
+ * @description The file to import
+ */
+ file: Blob;
+ };
/** Body_parse_dynamicprompts */
Body_parse_dynamicprompts: {
/**
@@ -18083,4 +18109,37 @@ export interface operations {
};
};
};
+ import_style_presets: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "multipart/form-data": components["schemas"]["Body_import_style_presets"];
+ };
+ };
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": unknown;
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
}
diff --git a/pyproject.toml b/pyproject.toml
index 733867949e..6da48e1d74 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -198,6 +198,7 @@ exclude = [
"dist",
"invokeai/frontend/web/node_modules/",
".venv*",
+ "*.ipynb",
]
[tool.ruff.lint]