mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'main' into chainchompa/preselect-workflows
This commit is contained in:
commit
659019cfd6
2
.github/workflows/python-checks.yml
vendored
2
.github/workflows/python-checks.yml
vendored
@ -62,7 +62,7 @@ jobs:
|
|||||||
|
|
||||||
- name: install ruff
|
- name: install ruff
|
||||||
if: ${{ steps.changed-files.outputs.python_any_changed == 'true' || inputs.always_run == true }}
|
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
|
shell: bash
|
||||||
|
|
||||||
- name: ruff check
|
- name: ruff check
|
||||||
|
@ -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.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_images.style_preset_images_common import StylePresetImageFileNotFoundException
|
||||||
from invokeai.app.services.style_preset_records.style_preset_records_common import (
|
from invokeai.app.services.style_preset_records.style_preset_records_common import (
|
||||||
|
InvalidPresetImportDataError,
|
||||||
PresetData,
|
PresetData,
|
||||||
PresetType,
|
PresetType,
|
||||||
StylePresetChanges,
|
StylePresetChanges,
|
||||||
StylePresetNotFoundError,
|
StylePresetNotFoundError,
|
||||||
StylePresetRecordWithImage,
|
StylePresetRecordWithImage,
|
||||||
StylePresetWithoutId,
|
StylePresetWithoutId,
|
||||||
|
UnsupportedFileTypeError,
|
||||||
|
parse_presets_from_file,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -225,3 +228,19 @@ async def get_style_preset_image(
|
|||||||
return response
|
return response
|
||||||
except Exception:
|
except Exception:
|
||||||
raise HTTPException(status_code=404)
|
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))
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Photography (General)",
|
"name": "Photography (General)",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
"preset_data": {
|
"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"
|
"negative_prompt": "painting, digital art. sketch, blurry"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -11,7 +11,7 @@
|
|||||||
"name": "Photography (Studio Lighting)",
|
"name": "Photography (Studio Lighting)",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
"preset_data": {
|
"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"
|
"negative_prompt": "painting, digital art. sketch, blurry"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -19,7 +19,7 @@
|
|||||||
"name": "Photography (Landscape)",
|
"name": "Photography (Landscape)",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
"preset_data": {
|
"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"
|
"negative_prompt": "painting, digital art. sketch, blurry"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -27,7 +27,7 @@
|
|||||||
"name": "Photography (Portrait)",
|
"name": "Photography (Portrait)",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
"preset_data": {
|
"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"
|
"negative_prompt": "painting, digital art. sketch, blurry"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -35,7 +35,7 @@
|
|||||||
"name": "Photography (Black and White)",
|
"name": "Photography (Black and White)",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
"preset_data": {
|
"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+"
|
"negative_prompt": "painting, digital art. sketch, colour+"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -43,7 +43,7 @@
|
|||||||
"name": "Architectural Visualization",
|
"name": "Architectural Visualization",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
"preset_data": {
|
"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"
|
"negative_prompt": "painting, digital art. sketch, blurry"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -51,7 +51,7 @@
|
|||||||
"name": "Concept Art (Fantasy)",
|
"name": "Concept Art (Fantasy)",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
"preset_data": {
|
"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.)++"
|
"negative_prompt": "photo. distorted, blurry, out of focus. sketch. (cgi, 3d.)++"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -59,7 +59,7 @@
|
|||||||
"name": "Concept Art (Sci-Fi)",
|
"name": "Concept Art (Sci-Fi)",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
"preset_data": {
|
"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.)++"
|
"negative_prompt": "photo. distorted, blurry, out of focus. sketch. (cgi, 3d.)++"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -67,7 +67,7 @@
|
|||||||
"name": "Concept Art (Character)",
|
"name": "Concept Art (Character)",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
"preset_data": {
|
"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.)++"
|
"negative_prompt": "photo. distorted, blurry, out of focus. sketch. (cgi, 3d.)++"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -75,7 +75,7 @@
|
|||||||
"name": "Concept Art (Painterly)",
|
"name": "Concept Art (Painterly)",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
"preset_data": {
|
"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"
|
"negative_prompt": "photo. smooth. border. frame"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -83,7 +83,7 @@
|
|||||||
"name": "Environment Art",
|
"name": "Environment Art",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
"preset_data": {
|
"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."
|
"negative_prompt": "photo, distorted, blurry, out of focus. sketch."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -91,7 +91,7 @@
|
|||||||
"name": "Interior Design (Visualization)",
|
"name": "Interior Design (Visualization)",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
"preset_data": {
|
"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."
|
"negative_prompt": "photo, distorted. sketch."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -99,7 +99,7 @@
|
|||||||
"name": "Product Rendering",
|
"name": "Product Rendering",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
"preset_data": {
|
"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."
|
"negative_prompt": "blurry, sketch, messy, dirty. unfinished."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -107,7 +107,7 @@
|
|||||||
"name": "Sketch",
|
"name": "Sketch",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
"preset_data": {
|
"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."
|
"negative_prompt": "blurry, photo, painting, color. messy, dirty. unfinished. frame, borders."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -115,7 +115,7 @@
|
|||||||
"name": "Line Art",
|
"name": "Line Art",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
"preset_data": {
|
"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"
|
"negative_prompt": "photo. digital art. greyscale. solid black. painting"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -123,7 +123,7 @@
|
|||||||
"name": "Anime",
|
"name": "Anime",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
"preset_data": {
|
"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"
|
"negative_prompt": "(photo)+++. greyscale. solid black. painting"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -131,7 +131,7 @@
|
|||||||
"name": "Illustration",
|
"name": "Illustration",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
"preset_data": {
|
"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."
|
"negative_prompt": "(photo)+++. greyscale. painting, black and white."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -139,7 +139,7 @@
|
|||||||
"name": "Vehicles",
|
"name": "Vehicles",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
"preset_data": {
|
"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"
|
"negative_prompt": "sketch. digital art. greyscale. painting"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,11 @@ class StylePresetRecordsStorageBase(ABC):
|
|||||||
"""Creates a style preset."""
|
"""Creates a style preset."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def create_many(self, style_presets: list[StylePresetWithoutId]) -> None:
|
||||||
|
"""Creates many style presets."""
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def update(self, style_preset_id: str, changes: StylePresetChanges) -> StylePresetRecordDTO:
|
def update(self, style_preset_id: str, changes: StylePresetChanges) -> StylePresetRecordDTO:
|
||||||
"""Updates a style preset."""
|
"""Updates a style preset."""
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
|
import codecs
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Optional
|
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
|
from invokeai.app.util.metaenum import MetaEnum
|
||||||
|
|
||||||
@ -49,3 +54,85 @@ StylePresetRecordDTOValidator = TypeAdapter(StylePresetRecordDTO)
|
|||||||
|
|
||||||
class StylePresetRecordWithImage(StylePresetRecordDTO):
|
class StylePresetRecordWithImage(StylePresetRecordDTO):
|
||||||
image: Optional[str] = Field(description="The path for image")
|
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
|
||||||
|
@ -75,6 +75,39 @@ class SqliteStylePresetRecordsStorage(StylePresetRecordsStorageBase):
|
|||||||
self._lock.release()
|
self._lock.release()
|
||||||
return self.get(style_preset_id)
|
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:
|
def update(self, style_preset_id: str, changes: StylePresetChanges) -> StylePresetRecordDTO:
|
||||||
try:
|
try:
|
||||||
self._lock.acquire()
|
self._lock.acquire()
|
||||||
|
@ -1141,6 +1141,8 @@
|
|||||||
"imageSavingFailed": "Image Saving Failed",
|
"imageSavingFailed": "Image Saving Failed",
|
||||||
"imageUploaded": "Image Uploaded",
|
"imageUploaded": "Image Uploaded",
|
||||||
"imageUploadFailed": "Image Upload Failed",
|
"imageUploadFailed": "Image Upload Failed",
|
||||||
|
"importFailed": "Import Failed",
|
||||||
|
"importSuccessful": "Import Successful",
|
||||||
"invalidUpload": "Invalid Upload",
|
"invalidUpload": "Invalid Upload",
|
||||||
"loadedWithWarnings": "Workflow Loaded with Warnings",
|
"loadedWithWarnings": "Workflow Loaded with Warnings",
|
||||||
"maskSavedAssets": "Mask Saved to Assets",
|
"maskSavedAssets": "Mask Saved to Assets",
|
||||||
@ -1701,6 +1703,8 @@
|
|||||||
"deleteTemplate2": "Are you sure you want to delete this template? This cannot be undone.",
|
"deleteTemplate2": "Are you sure you want to delete this template? This cannot be undone.",
|
||||||
"editTemplate": "Edit Template",
|
"editTemplate": "Edit Template",
|
||||||
"flatten": "Flatten selected template into current prompt",
|
"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",
|
"insertPlaceholder": "Insert placeholder",
|
||||||
"myTemplates": "My Templates",
|
"myTemplates": "My Templates",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
icon={!isLoading ? <PiUploadBold /> : <PiSpinner />}
|
||||||
|
tooltip={
|
||||||
|
<>
|
||||||
|
<Text fontWeight="semibold">{t('stylePresets.importTemplates')}</Text>
|
||||||
|
<Text>{t('stylePresets.importTemplatesDesc')}</Text>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
aria-label={t('stylePresets.importTemplates')}
|
||||||
|
size="md"
|
||||||
|
variant="link"
|
||||||
|
w={8}
|
||||||
|
h={8}
|
||||||
|
sx={isLoading ? loadingStyles : undefined}
|
||||||
|
isDisabled={isLoading}
|
||||||
|
{...getRootProps()}
|
||||||
|
/>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -8,6 +8,7 @@ import { PiPlusBold } from 'react-icons/pi';
|
|||||||
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
|
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
|
||||||
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
|
import { StylePresetImport } from './StylePresetImport';
|
||||||
import { StylePresetList } from './StylePresetList';
|
import { StylePresetList } from './StylePresetList';
|
||||||
import StylePresetSearch from './StylePresetSearch';
|
import StylePresetSearch from './StylePresetSearch';
|
||||||
|
|
||||||
@ -60,6 +61,9 @@ export const StylePresetMenu = () => {
|
|||||||
<Flex flexDir="column" gap={2} padding={3} layerStyle="second" borderRadius="base">
|
<Flex flexDir="column" gap={2} padding={3} layerStyle="second" borderRadius="base">
|
||||||
<Flex alignItems="center" gap={2} w="full" justifyContent="space-between">
|
<Flex alignItems="center" gap={2} w="full" justifyContent="space-between">
|
||||||
<StylePresetSearch />
|
<StylePresetSearch />
|
||||||
|
<Flex alignItems="center" justifyContent="space-between">
|
||||||
|
<StylePresetImport />
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<PiPlusBold />}
|
icon={<PiPlusBold />}
|
||||||
tooltip={t('stylePresets.createPromptTemplate')}
|
tooltip={t('stylePresets.createPromptTemplate')}
|
||||||
@ -71,6 +75,7 @@ export const StylePresetMenu = () => {
|
|||||||
h={8}
|
h={8}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
{data.presets.length === 0 && data.defaultPresets.length === 0 && (
|
{data.presets.length === 0 && data.defaultPresets.length === 0 && (
|
||||||
<Text p={10} textAlign="center">
|
<Text p={10} textAlign="center">
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
import { Flex, IconButton } from '@invoke-ai/ui-library';
|
import { Flex, IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { $isMenuOpen } from 'features/stylePresets/store/isMenuOpen';
|
import { $isMenuOpen } from 'features/stylePresets/store/isMenuOpen';
|
||||||
@ -7,6 +8,10 @@ import { PiCaretDownBold } from 'react-icons/pi';
|
|||||||
|
|
||||||
import { ActiveStylePreset } from './ActiveStylePreset';
|
import { ActiveStylePreset } from './ActiveStylePreset';
|
||||||
|
|
||||||
|
const _hover: SystemStyleObject = {
|
||||||
|
bg: 'base.750',
|
||||||
|
};
|
||||||
|
|
||||||
export const StylePresetMenuTrigger = () => {
|
export const StylePresetMenuTrigger = () => {
|
||||||
const isMenuOpen = useStore($isMenuOpen);
|
const isMenuOpen = useStore($isMenuOpen);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -26,6 +31,9 @@ export const StylePresetMenuTrigger = () => {
|
|||||||
borderRadius="base"
|
borderRadius="base"
|
||||||
gap={1}
|
gap={1}
|
||||||
role="button"
|
role="button"
|
||||||
|
_hover={_hover}
|
||||||
|
transitionProperty="background-color"
|
||||||
|
transitionDuration="normal"
|
||||||
>
|
>
|
||||||
<ActiveStylePreset />
|
<ActiveStylePreset />
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
export const PRESET_PLACEHOLDER = `[prompt]`;
|
export const PRESET_PLACEHOLDER = '{prompt}';
|
||||||
|
|
||||||
export const buildPresetModifiedPrompt = (presetPrompt: string, currentPrompt: string) => {
|
export const buildPresetModifiedPrompt = (presetPrompt: string, currentPrompt: string) => {
|
||||||
return presetPrompt.includes(PRESET_PLACEHOLDER)
|
return presetPrompt.includes(PRESET_PLACEHOLDER)
|
||||||
|
@ -92,6 +92,23 @@ export const stylePresetsApi = api.injectEndpoints({
|
|||||||
}),
|
}),
|
||||||
providesTags: ['FetchOnReconnect', { type: 'StylePreset', id: LIST_TAG }],
|
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,
|
useDeleteStylePresetMutation,
|
||||||
useUpdateStylePresetMutation,
|
useUpdateStylePresetMutation,
|
||||||
useListStylePresetsQuery,
|
useListStylePresetsQuery,
|
||||||
|
useImportStylePresetsMutation,
|
||||||
} = stylePresetsApi;
|
} = stylePresetsApi;
|
||||||
|
@ -1344,6 +1344,23 @@ export type paths = {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: 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<string, never>;
|
export type webhooks = Record<string, never>;
|
||||||
export type components = {
|
export type components = {
|
||||||
@ -1998,6 +2015,15 @@ export type components = {
|
|||||||
*/
|
*/
|
||||||
prepend?: boolean;
|
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 */
|
||||||
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"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -198,6 +198,7 @@ exclude = [
|
|||||||
"dist",
|
"dist",
|
||||||
"invokeai/frontend/web/node_modules/",
|
"invokeai/frontend/web/node_modules/",
|
||||||
".venv*",
|
".venv*",
|
||||||
|
"*.ipynb",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
|
Loading…
Reference in New Issue
Block a user