Merge remote-tracking branch 'refs/remotes/origin/lstein/feat/diffusers-v0.30' into lstein/feat/diffusers-v0.30
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
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
# Ensure we're in the correct folder in case user's CWD is somewhere else
|
# Ensure we're in the correct folder in case user's CWD is somewhere else
|
||||||
scriptdir=$(dirname "$0")
|
scriptdir=$(dirname $(readlink -f "$0"))
|
||||||
cd "$scriptdir"
|
cd "$scriptdir"
|
||||||
|
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
|
@ -32,6 +32,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.session_queue.session_queue_sqlite import SqliteSessionQueue
|
||||||
from invokeai.app.services.shared.sqlite.sqlite_util import init_db
|
from invokeai.app.services.shared.sqlite.sqlite_util import init_db
|
||||||
|
from invokeai.app.services.style_preset_images.style_preset_images_disk 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.urls.urls_default import LocalUrlService
|
||||||
from invokeai.app.services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage
|
from invokeai.app.services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage
|
||||||
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData
|
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData
|
||||||
@ -80,6 +82,7 @@ class ApiDependencies:
|
|||||||
image_files = DiskImageFileStorage(f"{output_folder}/images")
|
image_files = DiskImageFileStorage(f"{output_folder}/images")
|
||||||
|
|
||||||
model_images_folder = config.models_path
|
model_images_folder = config.models_path
|
||||||
|
style_presets_folder = config.style_presets_path
|
||||||
|
|
||||||
db = init_db(config=config, logger=logger, image_files=image_files)
|
db = init_db(config=config, logger=logger, image_files=image_files)
|
||||||
|
|
||||||
@ -115,6 +118,8 @@ class ApiDependencies:
|
|||||||
session_queue = SqliteSessionQueue(db=db)
|
session_queue = SqliteSessionQueue(db=db)
|
||||||
urls = LocalUrlService()
|
urls = LocalUrlService()
|
||||||
workflow_records = SqliteWorkflowRecordsStorage(db=db)
|
workflow_records = SqliteWorkflowRecordsStorage(db=db)
|
||||||
|
style_preset_records = SqliteStylePresetRecordsStorage(db=db)
|
||||||
|
style_preset_image_files = StylePresetImageFileStorageDisk(style_presets_folder / "images")
|
||||||
|
|
||||||
services = InvocationServices(
|
services = InvocationServices(
|
||||||
board_image_records=board_image_records,
|
board_image_records=board_image_records,
|
||||||
@ -140,6 +145,8 @@ class ApiDependencies:
|
|||||||
workflow_records=workflow_records,
|
workflow_records=workflow_records,
|
||||||
tensors=tensors,
|
tensors=tensors,
|
||||||
conditioning=conditioning,
|
conditioning=conditioning,
|
||||||
|
style_preset_records=style_preset_records,
|
||||||
|
style_preset_image_files=style_preset_image_files,
|
||||||
)
|
)
|
||||||
|
|
||||||
ApiDependencies.invoker = Invoker(services)
|
ApiDependencies.invoker = Invoker(services)
|
||||||
|
276
invokeai/app/api/routers/style_presets.py
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import traceback
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import pydantic
|
||||||
|
from fastapi import APIRouter, File, Form, HTTPException, Path, Response, UploadFile
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from PIL import Image
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StylePresetUpdateFormData(BaseModel):
|
||||||
|
name: str = Field(description="Preset name")
|
||||||
|
positive_prompt: str = Field(description="Positive prompt")
|
||||||
|
negative_prompt: str = Field(description="Negative prompt")
|
||||||
|
|
||||||
|
|
||||||
|
class StylePresetCreateFormData(StylePresetUpdateFormData):
|
||||||
|
type: PresetType = Field(description="Preset type")
|
||||||
|
|
||||||
|
|
||||||
|
style_presets_router = APIRouter(prefix="/v1/style_presets", tags=["style_presets"])
|
||||||
|
|
||||||
|
|
||||||
|
@style_presets_router.get(
|
||||||
|
"/i/{style_preset_id}",
|
||||||
|
operation_id="get_style_preset",
|
||||||
|
responses={
|
||||||
|
200: {"model": StylePresetRecordWithImage},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
async def get_style_preset(
|
||||||
|
style_preset_id: str = Path(description="The style preset to get"),
|
||||||
|
) -> StylePresetRecordWithImage:
|
||||||
|
"""Gets a style preset"""
|
||||||
|
try:
|
||||||
|
image = ApiDependencies.invoker.services.style_preset_image_files.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")
|
||||||
|
|
||||||
|
|
||||||
|
@style_presets_router.patch(
|
||||||
|
"/i/{style_preset_id}",
|
||||||
|
operation_id="update_style_preset",
|
||||||
|
responses={
|
||||||
|
200: {"model": StylePresetRecordWithImage},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
async def update_style_preset(
|
||||||
|
image: Optional[UploadFile] = File(description="The image file to upload", default=None),
|
||||||
|
style_preset_id: str = Path(description="The id of the style preset to update"),
|
||||||
|
data: str = Form(description="The data of the style preset to update"),
|
||||||
|
) -> StylePresetRecordWithImage:
|
||||||
|
"""Updates a 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_image_files.save(style_preset_id, pil_image)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=409, detail=str(e))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
ApiDependencies.invoker.services.style_preset_image_files.delete(style_preset_id)
|
||||||
|
except StylePresetImageFileNotFoundException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed_data = json.loads(data)
|
||||||
|
validated_data = StylePresetUpdateFormData(**parsed_data)
|
||||||
|
|
||||||
|
name = validated_data.name
|
||||||
|
positive_prompt = validated_data.positive_prompt
|
||||||
|
negative_prompt = validated_data.negative_prompt
|
||||||
|
|
||||||
|
except pydantic.ValidationError:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid preset data")
|
||||||
|
|
||||||
|
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_image_files.get_url(style_preset_id)
|
||||||
|
style_preset = ApiDependencies.invoker.services.style_preset_records.update(
|
||||||
|
style_preset_id=style_preset_id, changes=changes
|
||||||
|
)
|
||||||
|
return StylePresetRecordWithImage(image=style_preset_image, **style_preset.model_dump())
|
||||||
|
|
||||||
|
|
||||||
|
@style_presets_router.delete(
|
||||||
|
"/i/{style_preset_id}",
|
||||||
|
operation_id="delete_style_preset",
|
||||||
|
)
|
||||||
|
async def delete_style_preset(
|
||||||
|
style_preset_id: str = Path(description="The style preset to delete"),
|
||||||
|
) -> None:
|
||||||
|
"""Deletes a style preset"""
|
||||||
|
try:
|
||||||
|
ApiDependencies.invoker.services.style_preset_image_files.delete(style_preset_id)
|
||||||
|
except StylePresetImageFileNotFoundException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ApiDependencies.invoker.services.style_preset_records.delete(style_preset_id)
|
||||||
|
|
||||||
|
|
||||||
|
@style_presets_router.post(
|
||||||
|
"/",
|
||||||
|
operation_id="create_style_preset",
|
||||||
|
responses={
|
||||||
|
200: {"model": StylePresetRecordWithImage},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
async def create_style_preset(
|
||||||
|
image: Optional[UploadFile] = File(description="The image file to upload", default=None),
|
||||||
|
data: str = Form(description="The data of the style preset to create"),
|
||||||
|
) -> StylePresetRecordWithImage:
|
||||||
|
"""Creates a style preset"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed_data = json.loads(data)
|
||||||
|
validated_data = StylePresetCreateFormData(**parsed_data)
|
||||||
|
|
||||||
|
name = validated_data.name
|
||||||
|
type = validated_data.type
|
||||||
|
positive_prompt = validated_data.positive_prompt
|
||||||
|
negative_prompt = validated_data.negative_prompt
|
||||||
|
|
||||||
|
except pydantic.ValidationError:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid preset data")
|
||||||
|
|
||||||
|
preset_data = PresetData(positive_prompt=positive_prompt, negative_prompt=negative_prompt)
|
||||||
|
style_preset = StylePresetWithoutId(name=name, preset_data=preset_data, type=type)
|
||||||
|
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_image_files.save(new_style_preset.id, pil_image)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=409, detail=str(e))
|
||||||
|
|
||||||
|
preset_image = ApiDependencies.invoker.services.style_preset_image_files.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[StylePresetRecordWithImage]},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
async def list_style_presets() -> list[StylePresetRecordWithImage]:
|
||||||
|
"""Gets a page of style presets"""
|
||||||
|
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_image_files.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_image_files.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)
|
||||||
|
|
||||||
|
|
||||||
|
@style_presets_router.get(
|
||||||
|
"/export",
|
||||||
|
operation_id="export_style_presets",
|
||||||
|
responses={200: {"content": {"text/csv": {}}, "description": "A CSV file with the requested data."}},
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
async def export_style_presets():
|
||||||
|
# Create an in-memory stream to store the CSV data
|
||||||
|
output = io.StringIO()
|
||||||
|
writer = csv.writer(output)
|
||||||
|
|
||||||
|
# Write the header
|
||||||
|
writer.writerow(["name", "prompt", "negative_prompt"])
|
||||||
|
|
||||||
|
style_presets = ApiDependencies.invoker.services.style_preset_records.get_many(type=PresetType.User)
|
||||||
|
|
||||||
|
for preset in style_presets:
|
||||||
|
writer.writerow([preset.name, preset.preset_data.positive_prompt, preset.preset_data.negative_prompt])
|
||||||
|
|
||||||
|
csv_data = output.getvalue()
|
||||||
|
output.close()
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
content=csv_data,
|
||||||
|
media_type="text/csv",
|
||||||
|
headers={"Content-Disposition": "attachment; filename=prompt_templates.csv"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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))
|
@ -30,6 +30,7 @@ from invokeai.app.api.routers import (
|
|||||||
images,
|
images,
|
||||||
model_manager,
|
model_manager,
|
||||||
session_queue,
|
session_queue,
|
||||||
|
style_presets,
|
||||||
utilities,
|
utilities,
|
||||||
workflows,
|
workflows,
|
||||||
)
|
)
|
||||||
@ -108,6 +109,7 @@ app.include_router(board_images.board_images_router, prefix="/api")
|
|||||||
app.include_router(app_info.app_router, prefix="/api")
|
app.include_router(app_info.app_router, prefix="/api")
|
||||||
app.include_router(session_queue.session_queue_router, prefix="/api")
|
app.include_router(session_queue.session_queue_router, prefix="/api")
|
||||||
app.include_router(workflows.workflows_router, prefix="/api")
|
app.include_router(workflows.workflows_router, prefix="/api")
|
||||||
|
app.include_router(style_presets.style_presets_router, prefix="/api")
|
||||||
|
|
||||||
app.openapi = get_openapi_func(app)
|
app.openapi = get_openapi_func(app)
|
||||||
|
|
||||||
|
@ -91,6 +91,7 @@ class InvokeAIAppConfig(BaseSettings):
|
|||||||
db_dir: Path to InvokeAI databases directory.
|
db_dir: Path to InvokeAI databases directory.
|
||||||
outputs_dir: Path to directory for outputs.
|
outputs_dir: Path to directory for outputs.
|
||||||
custom_nodes_dir: Path to directory for custom nodes.
|
custom_nodes_dir: Path to directory for custom nodes.
|
||||||
|
style_presets_dir: Path to directory for style presets.
|
||||||
log_handlers: Log handler. Valid options are "console", "file=<path>", "syslog=path|address:host:port", "http=<url>".
|
log_handlers: Log handler. Valid options are "console", "file=<path>", "syslog=path|address:host:port", "http=<url>".
|
||||||
log_format: Log format. Use "plain" for text-only, "color" for colorized output, "legacy" for 2.3-style logging and "syslog" for syslog-style.<br>Valid values: `plain`, `color`, `syslog`, `legacy`
|
log_format: Log format. Use "plain" for text-only, "color" for colorized output, "legacy" for 2.3-style logging and "syslog" for syslog-style.<br>Valid values: `plain`, `color`, `syslog`, `legacy`
|
||||||
log_level: Emit logging messages at this level or higher.<br>Valid values: `debug`, `info`, `warning`, `error`, `critical`
|
log_level: Emit logging messages at this level or higher.<br>Valid values: `debug`, `info`, `warning`, `error`, `critical`
|
||||||
@ -153,6 +154,7 @@ class InvokeAIAppConfig(BaseSettings):
|
|||||||
db_dir: Path = Field(default=Path("databases"), description="Path to InvokeAI databases directory.")
|
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.")
|
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.")
|
custom_nodes_dir: Path = Field(default=Path("nodes"), description="Path to directory for custom nodes.")
|
||||||
|
style_presets_dir: Path = Field(default=Path("style_presets"), description="Path to directory for style presets.")
|
||||||
|
|
||||||
# LOGGING
|
# LOGGING
|
||||||
log_handlers: list[str] = Field(default=["console"], description='Log handler. Valid options are "console", "file=<path>", "syslog=path|address:host:port", "http=<url>".')
|
log_handlers: list[str] = Field(default=["console"], description='Log handler. Valid options are "console", "file=<path>", "syslog=path|address:host:port", "http=<url>".')
|
||||||
@ -300,6 +302,11 @@ class InvokeAIAppConfig(BaseSettings):
|
|||||||
"""Path to the models directory, resolved to an absolute path.."""
|
"""Path to the models directory, resolved to an absolute path.."""
|
||||||
return self._resolve(self.models_dir)
|
return self._resolve(self.models_dir)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def style_presets_path(self) -> Path:
|
||||||
|
"""Path to the style presets directory, resolved to an absolute path.."""
|
||||||
|
return self._resolve(self.style_presets_dir)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def convert_cache_path(self) -> Path:
|
def convert_cache_path(self) -> Path:
|
||||||
"""Path to the converted cache models directory, resolved to an absolute path.."""
|
"""Path to the converted cache models directory, resolved to an absolute path.."""
|
||||||
|
@ -4,6 +4,8 @@ from __future__ import annotations
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from invokeai.app.services.object_serializer.object_serializer_base import ObjectSerializerBase
|
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:
|
if TYPE_CHECKING:
|
||||||
from logging import Logger
|
from logging import Logger
|
||||||
@ -61,6 +63,8 @@ class InvocationServices:
|
|||||||
workflow_records: "WorkflowRecordsStorageBase",
|
workflow_records: "WorkflowRecordsStorageBase",
|
||||||
tensors: "ObjectSerializerBase[torch.Tensor]",
|
tensors: "ObjectSerializerBase[torch.Tensor]",
|
||||||
conditioning: "ObjectSerializerBase[ConditioningFieldData]",
|
conditioning: "ObjectSerializerBase[ConditioningFieldData]",
|
||||||
|
style_preset_records: "StylePresetRecordsStorageBase",
|
||||||
|
style_preset_image_files: "StylePresetImageFileStorageBase",
|
||||||
):
|
):
|
||||||
self.board_images = board_images
|
self.board_images = board_images
|
||||||
self.board_image_records = board_image_records
|
self.board_image_records = board_image_records
|
||||||
@ -85,3 +89,5 @@ class InvocationServices:
|
|||||||
self.workflow_records = workflow_records
|
self.workflow_records = workflow_records
|
||||||
self.tensors = tensors
|
self.tensors = tensors
|
||||||
self.conditioning = conditioning
|
self.conditioning = conditioning
|
||||||
|
self.style_preset_records = style_preset_records
|
||||||
|
self.style_preset_image_files = style_preset_image_files
|
||||||
|
@ -16,6 +16,7 @@ from invokeai.app.services.shared.sqlite_migrator.migrations.migration_10 import
|
|||||||
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_11 import build_migration_11
|
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_11 import build_migration_11
|
||||||
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_12 import build_migration_12
|
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_12 import build_migration_12
|
||||||
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_13 import build_migration_13
|
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_13 import build_migration_13
|
||||||
|
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_14 import build_migration_14
|
||||||
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import SqliteMigrator
|
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import SqliteMigrator
|
||||||
|
|
||||||
|
|
||||||
@ -49,6 +50,7 @@ def init_db(config: InvokeAIAppConfig, logger: Logger, image_files: ImageFileSto
|
|||||||
migrator.register_migration(build_migration_11(app_config=config, logger=logger))
|
migrator.register_migration(build_migration_11(app_config=config, logger=logger))
|
||||||
migrator.register_migration(build_migration_12(app_config=config))
|
migrator.register_migration(build_migration_12(app_config=config))
|
||||||
migrator.register_migration(build_migration_13())
|
migrator.register_migration(build_migration_13())
|
||||||
|
migrator.register_migration(build_migration_14())
|
||||||
migrator.run_migrations()
|
migrator.run_migrations()
|
||||||
|
|
||||||
return db
|
return db
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
|
||||||
|
|
||||||
|
|
||||||
|
class Migration14Callback:
|
||||||
|
def __call__(self, cursor: sqlite3.Cursor) -> None:
|
||||||
|
self._create_style_presets(cursor)
|
||||||
|
|
||||||
|
def _create_style_presets(self, cursor: sqlite3.Cursor) -> None:
|
||||||
|
"""Create the table used to store style presets."""
|
||||||
|
tables = [
|
||||||
|
"""--sql
|
||||||
|
CREATE TABLE IF NOT EXISTS style_presets (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
preset_data TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL DEFAULT "user",
|
||||||
|
created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
|
||||||
|
-- Updated via trigger
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW'))
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add trigger for `updated_at`.
|
||||||
|
triggers = [
|
||||||
|
"""--sql
|
||||||
|
CREATE TRIGGER IF NOT EXISTS style_presets
|
||||||
|
AFTER UPDATE
|
||||||
|
ON style_presets FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE style_presets SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
|
||||||
|
WHERE id = old.id;
|
||||||
|
END;
|
||||||
|
"""
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add indexes for searchable fields
|
||||||
|
indices = [
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_style_presets_name ON style_presets(name);",
|
||||||
|
]
|
||||||
|
|
||||||
|
for stmt in tables + indices + triggers:
|
||||||
|
cursor.execute(stmt)
|
||||||
|
|
||||||
|
|
||||||
|
def build_migration_14() -> Migration:
|
||||||
|
"""
|
||||||
|
Build the migration from database version 13 to 14..
|
||||||
|
|
||||||
|
This migration does the following:
|
||||||
|
- Create the table used to store style presets.
|
||||||
|
"""
|
||||||
|
migration_14 = Migration(
|
||||||
|
from_version=13,
|
||||||
|
to_version=14,
|
||||||
|
callback=Migration14Callback(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return migration_14
|
After Width: | Height: | Size: 98 KiB |
After Width: | Height: | Size: 138 KiB |
After Width: | Height: | Size: 122 KiB |
After Width: | Height: | Size: 123 KiB |
After Width: | Height: | Size: 160 KiB |
After Width: | Height: | Size: 146 KiB |
After Width: | Height: | Size: 119 KiB |
After Width: | Height: | Size: 117 KiB |
After Width: | Height: | Size: 110 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 79 KiB |
After Width: | Height: | Size: 156 KiB |
After Width: | Height: | Size: 141 KiB |
After Width: | Height: | Size: 96 KiB |
After Width: | Height: | Size: 91 KiB |
After Width: | Height: | Size: 88 KiB |
After Width: | Height: | Size: 107 KiB |
After Width: | Height: | Size: 132 KiB |
@ -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, style_preset_id: str, image: PILImageType) -> None:
|
||||||
|
"""Saves a style preset image."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete(self, style_preset_id: str) -> None:
|
||||||
|
"""Deletes a style preset image."""
|
||||||
|
pass
|
@ -0,0 +1,19 @@
|
|||||||
|
class StylePresetImageFileNotFoundException(Exception):
|
||||||
|
"""Raised when an image file is not found in storage."""
|
||||||
|
|
||||||
|
def __init__(self, message: str = "Style preset image file not found"):
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
|
||||||
|
class StylePresetImageFileSaveException(Exception):
|
||||||
|
"""Raised when an image cannot be saved."""
|
||||||
|
|
||||||
|
def __init__(self, message: str = "Style preset image file not saved"):
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
|
||||||
|
class StylePresetImageFileDeleteException(Exception):
|
||||||
|
"""Raised when an image cannot be deleted."""
|
||||||
|
|
||||||
|
def __init__(self, message: str = "Style preset image file not deleted"):
|
||||||
|
super().__init__(message)
|
@ -0,0 +1,88 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
from PIL.Image import Image as PILImageType
|
||||||
|
|
||||||
|
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.services.style_preset_records.style_preset_records_common import PresetType
|
||||||
|
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)
|
||||||
|
|
||||||
|
return Image.open(path)
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
raise StylePresetImageFileNotFoundException from e
|
||||||
|
|
||||||
|
def save(self, style_preset_id: str, image: PILImageType) -> 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:
|
||||||
|
style_preset = self._invoker.services.style_preset_records.get(style_preset_id)
|
||||||
|
if style_preset.type is PresetType.Default:
|
||||||
|
default_images_dir = Path(__file__).parent / Path("default_style_preset_images")
|
||||||
|
path = default_images_dir / (style_preset.name + ".png")
|
||||||
|
else:
|
||||||
|
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
|
||||||
|
|
||||||
|
path.unlink()
|
||||||
|
|
||||||
|
except StylePresetImageFileNotFoundException as e:
|
||||||
|
raise StylePresetImageFileNotFoundException from e
|
||||||
|
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)
|
@ -0,0 +1,146 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Photography (General)",
|
||||||
|
"type": "default",
|
||||||
|
"preset_data": {
|
||||||
|
"positive_prompt": "{prompt}. photography. f/2.8 macro photo, bokeh, photorealism",
|
||||||
|
"negative_prompt": "painting, digital art. sketch, blurry"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Photography (Studio Lighting)",
|
||||||
|
"type": "default",
|
||||||
|
"preset_data": {
|
||||||
|
"positive_prompt": "{prompt}, photography. f/8 photo. centered subject, studio lighting.",
|
||||||
|
"negative_prompt": "painting, digital art. sketch, blurry"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Photography (Landscape)",
|
||||||
|
"type": "default",
|
||||||
|
"preset_data": {
|
||||||
|
"positive_prompt": "{prompt}, landscape photograph, f/12, lifelike, highly detailed.",
|
||||||
|
"negative_prompt": "painting, digital art. sketch, blurry"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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.",
|
||||||
|
"negative_prompt": "painting, digital art. sketch, blurry"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"negative_prompt": "painting, digital art. sketch, colour+"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Architectural Visualization",
|
||||||
|
"type": "default",
|
||||||
|
"preset_data": {
|
||||||
|
"positive_prompt": "{prompt}. architectural photography, f/12, luxury, aesthetically pleasing form and function.",
|
||||||
|
"negative_prompt": "painting, digital art. sketch, blurry"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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+",
|
||||||
|
"negative_prompt": "photo. distorted, blurry, out of focus. sketch. (cgi, 3d.)++"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"negative_prompt": "photo. distorted, blurry, out of focus. sketch. (cgi, 3d.)++"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Concept Art (Character)",
|
||||||
|
"type": "default",
|
||||||
|
"preset_data": {
|
||||||
|
"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.)++"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Concept Art (Painterly)",
|
||||||
|
"type": "default",
|
||||||
|
"preset_data": {
|
||||||
|
"positive_prompt": "{prompt} oil painting. high contrast. impasto. sfumato. chiaroscuro. Palette knife.",
|
||||||
|
"negative_prompt": "photo. smooth. border. frame"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"negative_prompt": "photo, distorted, blurry, out of focus. sketch."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"negative_prompt": "photo, distorted. sketch."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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.",
|
||||||
|
"negative_prompt": "blurry, sketch, messy, dirty. unfinished."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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+++",
|
||||||
|
"negative_prompt": "blurry, photo, painting, color. messy, dirty. unfinished. frame, borders."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Line Art",
|
||||||
|
"type": "default",
|
||||||
|
"preset_data": {
|
||||||
|
"positive_prompt": "{prompt} Line art. bold outline. simplistic. white background. 2d",
|
||||||
|
"negative_prompt": "photo. digital art. greyscale. solid black. painting"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Anime",
|
||||||
|
"type": "default",
|
||||||
|
"preset_data": {
|
||||||
|
"positive_prompt": "{prompt} anime++, bold outline, cel-shaded coloring, shounen, seinen",
|
||||||
|
"negative_prompt": "(photo)+++. greyscale. solid black. painting"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Illustration",
|
||||||
|
"type": "default",
|
||||||
|
"preset_data": {
|
||||||
|
"positive_prompt": "{prompt} illustration, bold linework, illustrative details, vector art style, flat coloring",
|
||||||
|
"negative_prompt": "(photo)+++. greyscale. painting, black and white."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vehicles",
|
||||||
|
"type": "default",
|
||||||
|
"preset_data": {
|
||||||
|
"positive_prompt": "A weird futuristic normal auto, {prompt} elegant design, nice color, nice wheels",
|
||||||
|
"negative_prompt": "sketch. digital art. greyscale. painting"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,42 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from invokeai.app.services.style_preset_records.style_preset_records_common import (
|
||||||
|
PresetType,
|
||||||
|
StylePresetChanges,
|
||||||
|
StylePresetRecordDTO,
|
||||||
|
StylePresetWithoutId,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StylePresetRecordsStorageBase(ABC):
|
||||||
|
"""Base class for style preset storage services."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get(self, style_preset_id: str) -> StylePresetRecordDTO:
|
||||||
|
"""Get style preset by id."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def create(self, style_preset: StylePresetWithoutId) -> StylePresetRecordDTO:
|
||||||
|
"""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."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete(self, style_preset_id: str) -> None:
|
||||||
|
"""Deletes a style preset."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_many(self, type: PresetType | None = None) -> list[StylePresetRecordDTO]:
|
||||||
|
"""Gets many workflows."""
|
||||||
|
pass
|
@ -0,0 +1,138 @@
|
|||||||
|
import codecs
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import pydantic
|
||||||
|
from fastapi import UploadFile
|
||||||
|
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, TypeAdapter
|
||||||
|
|
||||||
|
from invokeai.app.util.metaenum import MetaEnum
|
||||||
|
|
||||||
|
|
||||||
|
class StylePresetNotFoundError(Exception):
|
||||||
|
"""Raised when a style preset is not found"""
|
||||||
|
|
||||||
|
|
||||||
|
class PresetData(BaseModel, extra="forbid"):
|
||||||
|
positive_prompt: str = Field(description="Positive prompt")
|
||||||
|
negative_prompt: str = Field(description="Negative prompt")
|
||||||
|
|
||||||
|
|
||||||
|
PresetDataValidator = TypeAdapter(PresetData)
|
||||||
|
|
||||||
|
|
||||||
|
class PresetType(str, Enum, metaclass=MetaEnum):
|
||||||
|
User = "user"
|
||||||
|
Default = "default"
|
||||||
|
Project = "project"
|
||||||
|
|
||||||
|
|
||||||
|
class StylePresetChanges(BaseModel, extra="forbid"):
|
||||||
|
name: Optional[str] = Field(default=None, description="The style preset's new name.")
|
||||||
|
preset_data: Optional[PresetData] = Field(default=None, description="The updated data for style preset.")
|
||||||
|
|
||||||
|
|
||||||
|
class StylePresetWithoutId(BaseModel):
|
||||||
|
name: str = Field(description="The name of the style preset.")
|
||||||
|
preset_data: PresetData = Field(description="The preset data")
|
||||||
|
type: PresetType = Field(description="The type of style preset")
|
||||||
|
|
||||||
|
|
||||||
|
class StylePresetRecordDTO(StylePresetWithoutId):
|
||||||
|
id: str = Field(description="The style preset ID.")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict[str, Any]) -> "StylePresetRecordDTO":
|
||||||
|
data["preset_data"] = PresetDataValidator.validate_json(data.get("preset_data", ""))
|
||||||
|
return StylePresetRecordDTOValidator.validate_python(data)
|
||||||
|
|
||||||
|
|
||||||
|
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' and name cannot be blank"
|
||||||
|
else: # file.content_type == "application/json":
|
||||||
|
msg = "Invalid JSON format: must be a list of objects with keys 'name', 'prompt', and 'negative_prompt' and name cannot be blank"
|
||||||
|
raise InvalidPresetImportDataError(msg) from e
|
||||||
|
finally:
|
||||||
|
file.file.close()
|
||||||
|
|
||||||
|
return style_presets
|
@ -0,0 +1,215 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from invokeai.app.services.invoker import Invoker
|
||||||
|
from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
|
||||||
|
from invokeai.app.services.style_preset_records.style_preset_records_base import StylePresetRecordsStorageBase
|
||||||
|
from invokeai.app.services.style_preset_records.style_preset_records_common import (
|
||||||
|
PresetType,
|
||||||
|
StylePresetChanges,
|
||||||
|
StylePresetNotFoundError,
|
||||||
|
StylePresetRecordDTO,
|
||||||
|
StylePresetWithoutId,
|
||||||
|
)
|
||||||
|
from invokeai.app.util.misc import uuid_string
|
||||||
|
|
||||||
|
|
||||||
|
class SqliteStylePresetRecordsStorage(StylePresetRecordsStorageBase):
|
||||||
|
def __init__(self, db: SqliteDatabase) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._lock = db.lock
|
||||||
|
self._conn = db.conn
|
||||||
|
self._cursor = self._conn.cursor()
|
||||||
|
|
||||||
|
def start(self, invoker: Invoker) -> None:
|
||||||
|
self._invoker = invoker
|
||||||
|
self._sync_default_style_presets()
|
||||||
|
|
||||||
|
def get(self, style_preset_id: str) -> StylePresetRecordDTO:
|
||||||
|
"""Gets a style preset by ID."""
|
||||||
|
try:
|
||||||
|
self._lock.acquire()
|
||||||
|
self._cursor.execute(
|
||||||
|
"""--sql
|
||||||
|
SELECT *
|
||||||
|
FROM style_presets
|
||||||
|
WHERE id = ?;
|
||||||
|
""",
|
||||||
|
(style_preset_id,),
|
||||||
|
)
|
||||||
|
row = self._cursor.fetchone()
|
||||||
|
if row is None:
|
||||||
|
raise StylePresetNotFoundError(f"Style preset with id {style_preset_id} not found")
|
||||||
|
return StylePresetRecordDTO.from_dict(dict(row))
|
||||||
|
except Exception:
|
||||||
|
self._conn.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
self._lock.release()
|
||||||
|
|
||||||
|
def create(self, style_preset: StylePresetWithoutId) -> StylePresetRecordDTO:
|
||||||
|
style_preset_id = uuid_string()
|
||||||
|
try:
|
||||||
|
self._lock.acquire()
|
||||||
|
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 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()
|
||||||
|
# Change the name of a style preset
|
||||||
|
if changes.name is not None:
|
||||||
|
self._cursor.execute(
|
||||||
|
"""--sql
|
||||||
|
UPDATE style_presets
|
||||||
|
SET name = ?
|
||||||
|
WHERE id = ?;
|
||||||
|
""",
|
||||||
|
(changes.name, style_preset_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Change the preset data for a style preset
|
||||||
|
if changes.preset_data is not None:
|
||||||
|
self._cursor.execute(
|
||||||
|
"""--sql
|
||||||
|
UPDATE style_presets
|
||||||
|
SET preset_data = ?
|
||||||
|
WHERE id = ?;
|
||||||
|
""",
|
||||||
|
(changes.preset_data.model_dump_json(), style_preset_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
self._conn.commit()
|
||||||
|
except Exception:
|
||||||
|
self._conn.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
self._lock.release()
|
||||||
|
return self.get(style_preset_id)
|
||||||
|
|
||||||
|
def delete(self, style_preset_id: str) -> None:
|
||||||
|
try:
|
||||||
|
self._lock.acquire()
|
||||||
|
self._cursor.execute(
|
||||||
|
"""--sql
|
||||||
|
DELETE from style_presets
|
||||||
|
WHERE id = ?;
|
||||||
|
""",
|
||||||
|
(style_preset_id,),
|
||||||
|
)
|
||||||
|
self._conn.commit()
|
||||||
|
except Exception:
|
||||||
|
self._conn.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
self._lock.release()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_many(self, type: PresetType | None = None) -> list[StylePresetRecordDTO]:
|
||||||
|
try:
|
||||||
|
self._lock.acquire()
|
||||||
|
main_query = """
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM style_presets
|
||||||
|
"""
|
||||||
|
|
||||||
|
if type is not None:
|
||||||
|
main_query += "WHERE type = ? "
|
||||||
|
|
||||||
|
main_query += "ORDER BY LOWER(name) ASC"
|
||||||
|
|
||||||
|
if type is not None:
|
||||||
|
self._cursor.execute(main_query, (type,))
|
||||||
|
else:
|
||||||
|
self._cursor.execute(main_query)
|
||||||
|
|
||||||
|
rows = self._cursor.fetchall()
|
||||||
|
style_presets = [StylePresetRecordDTO.from_dict(dict(row)) for row in rows]
|
||||||
|
|
||||||
|
return style_presets
|
||||||
|
except Exception:
|
||||||
|
self._conn.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
self._lock.release()
|
||||||
|
|
||||||
|
def _sync_default_style_presets(self) -> None:
|
||||||
|
"""Syncs default style presets to the database. Internal use only."""
|
||||||
|
|
||||||
|
# First delete all existing default style presets
|
||||||
|
try:
|
||||||
|
self._lock.acquire()
|
||||||
|
self._cursor.execute(
|
||||||
|
"""--sql
|
||||||
|
DELETE FROM style_presets
|
||||||
|
WHERE type = "default";
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self._conn.commit()
|
||||||
|
except Exception:
|
||||||
|
self._conn.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
self._lock.release()
|
||||||
|
# Next, parse and create the default style presets
|
||||||
|
with self._lock, open(Path(__file__).parent / Path("default_style_presets.json"), "r") as file:
|
||||||
|
presets = json.load(file)
|
||||||
|
for preset in presets:
|
||||||
|
style_preset = StylePresetWithoutId.model_validate(preset)
|
||||||
|
self.create(style_preset)
|
@ -13,3 +13,8 @@ class UrlServiceBase(ABC):
|
|||||||
def get_model_image_url(self, model_key: str) -> str:
|
def get_model_image_url(self, model_key: str) -> str:
|
||||||
"""Gets the URL for a model image"""
|
"""Gets the URL for a model image"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_style_preset_image_url(self, style_preset_id: str) -> str:
|
||||||
|
"""Gets the URL for a style preset image"""
|
||||||
|
pass
|
||||||
|
@ -19,3 +19,6 @@ class LocalUrlService(UrlServiceBase):
|
|||||||
|
|
||||||
def get_model_image_url(self, model_key: str) -> str:
|
def get_model_image_url(self, model_key: str) -> str:
|
||||||
return f"{self._base_url_v2}/models/i/{model_key}/image"
|
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"
|
||||||
|
@ -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",
|
||||||
@ -1689,6 +1691,52 @@
|
|||||||
"missingUpscaleModel": "Missing upscale model",
|
"missingUpscaleModel": "Missing upscale model",
|
||||||
"missingTileControlNetModel": "No valid tile ControlNet models installed"
|
"missingTileControlNetModel": "No valid tile ControlNet models installed"
|
||||||
},
|
},
|
||||||
|
"stylePresets": {
|
||||||
|
"active": "Active",
|
||||||
|
"choosePromptTemplate": "Choose Prompt Template",
|
||||||
|
"clearTemplateSelection": "Clear Template Selection",
|
||||||
|
"copyTemplate": "Copy Template",
|
||||||
|
"createPromptTemplate": "Create Prompt Template",
|
||||||
|
"defaultTemplates": "Default Templates",
|
||||||
|
"deleteImage": "Delete Image",
|
||||||
|
"deleteTemplate": "Delete Template",
|
||||||
|
"deleteTemplate2": "Are you sure you want to delete this template? This cannot be undone.",
|
||||||
|
"exportPromptTemplates": "Export My Prompt Templates (CSV)",
|
||||||
|
"editTemplate": "Edit Template",
|
||||||
|
"exportDownloaded": "Export Downloaded",
|
||||||
|
"exportFailed": "Unable to generate and download CSV",
|
||||||
|
"flatten": "Flatten selected template into current prompt",
|
||||||
|
"importTemplates": "Import Prompt Templates (CSV/JSON)",
|
||||||
|
"acceptedColumnsKeys": "Accepted columns/keys:",
|
||||||
|
"nameColumn": "'name'",
|
||||||
|
"positivePromptColumn": "'prompt' or 'positive_prompt'",
|
||||||
|
"negativePromptColumn": "'negative_prompt'",
|
||||||
|
"insertPlaceholder": "Insert placeholder",
|
||||||
|
"myTemplates": "My Templates",
|
||||||
|
"name": "Name",
|
||||||
|
"negativePrompt": "Negative Prompt",
|
||||||
|
"noTemplates": "No templates",
|
||||||
|
"noMatchingTemplates": "No matching templates",
|
||||||
|
"promptTemplatesDesc1": "Prompt templates add text to the prompts you write in the prompt box.",
|
||||||
|
"promptTemplatesDesc2": "Use the placeholder string <Pre>{{placeholder}}</Pre> to specify where your prompt should be included in the template.",
|
||||||
|
"promptTemplatesDesc3": "If you omit the placeholder, the template will be appended to the end of your prompt.",
|
||||||
|
"positivePrompt": "Positive Prompt",
|
||||||
|
"preview": "Preview",
|
||||||
|
"private": "Private",
|
||||||
|
"searchByName": "Search by name",
|
||||||
|
"shared": "Shared",
|
||||||
|
"sharedTemplates": "Shared Templates",
|
||||||
|
"templateActions": "Template Actions",
|
||||||
|
"templateDeleted": "Prompt template deleted",
|
||||||
|
"toggleViewMode": "Toggle View Mode",
|
||||||
|
"type": "Type",
|
||||||
|
"unableToDeleteTemplate": "Unable to delete prompt template",
|
||||||
|
"updatePromptTemplate": "Update Prompt Template",
|
||||||
|
"uploadImage": "Upload Image",
|
||||||
|
"useForTemplate": "Use For Prompt Template",
|
||||||
|
"viewList": "View Template List",
|
||||||
|
"viewModeTooltip": "This is how your prompt will look with your currently selected template. To edit your prompt, click anywhere in the text box."
|
||||||
|
},
|
||||||
"upsell": {
|
"upsell": {
|
||||||
"inviteTeammates": "Invite Teammates",
|
"inviteTeammates": "Invite Teammates",
|
||||||
"professional": "Professional",
|
"professional": "Professional",
|
||||||
|
@ -90,7 +90,7 @@
|
|||||||
"disabled": "Disabilitato",
|
"disabled": "Disabilitato",
|
||||||
"comparingDesc": "Confronta due immagini",
|
"comparingDesc": "Confronta due immagini",
|
||||||
"comparing": "Confronta",
|
"comparing": "Confronta",
|
||||||
"dontShowMeThese": "Non mostrarmi questi"
|
"dontShowMeThese": "Non mostrare più"
|
||||||
},
|
},
|
||||||
"gallery": {
|
"gallery": {
|
||||||
"galleryImageSize": "Dimensione dell'immagine",
|
"galleryImageSize": "Dimensione dell'immagine",
|
||||||
@ -701,7 +701,9 @@
|
|||||||
"baseModelChanged": "Modello base modificato",
|
"baseModelChanged": "Modello base modificato",
|
||||||
"sessionRef": "Sessione: {{sessionId}}",
|
"sessionRef": "Sessione: {{sessionId}}",
|
||||||
"somethingWentWrong": "Qualcosa è andato storto",
|
"somethingWentWrong": "Qualcosa è andato storto",
|
||||||
"outOfMemoryErrorDesc": "Le impostazioni della generazione attuale superano la capacità del sistema. Modifica le impostazioni e riprova."
|
"outOfMemoryErrorDesc": "Le impostazioni della generazione attuale superano la capacità del sistema. Modifica le impostazioni e riprova.",
|
||||||
|
"importFailed": "Importazione non riuscita",
|
||||||
|
"importSuccessful": "Importazione riuscita"
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"feature": {
|
"feature": {
|
||||||
@ -1526,7 +1528,7 @@
|
|||||||
},
|
},
|
||||||
"upscaleModel": {
|
"upscaleModel": {
|
||||||
"paragraphs": [
|
"paragraphs": [
|
||||||
"Il modello di ampliamento ridimensiona l'immagine alle dimensioni di uscita prima che vengano aggiunti i dettagli. È possibile utilizzare qualsiasi modello di ampliamento supportato, ma alcuni sono specializzati per diversi tipi di immagini, come foto o disegni al tratto."
|
"Il modello di ampliamento (Upscale), scala l'immagine alle dimensioni di uscita prima di aggiungere i dettagli. È possibile utilizzare qualsiasi modello di ampliamento supportato, ma alcuni sono specializzati per diversi tipi di immagini, come foto o disegni al tratto."
|
||||||
],
|
],
|
||||||
"heading": "Modello di ampliamento"
|
"heading": "Modello di ampliamento"
|
||||||
},
|
},
|
||||||
@ -1735,12 +1737,52 @@
|
|||||||
"missingUpscaleModel": "Modello per l’ampliamento mancante",
|
"missingUpscaleModel": "Modello per l’ampliamento mancante",
|
||||||
"missingTileControlNetModel": "Nessun modello ControlNet Tile valido installato",
|
"missingTileControlNetModel": "Nessun modello ControlNet Tile valido installato",
|
||||||
"postProcessingModel": "Modello di post-elaborazione",
|
"postProcessingModel": "Modello di post-elaborazione",
|
||||||
"postProcessingMissingModelWarning": "Visita <LinkComponent>Gestione modelli</LinkComponent> per installare un modello di post-elaborazione (da immagine a immagine)."
|
"postProcessingMissingModelWarning": "Visita <LinkComponent>Gestione modelli</LinkComponent> per installare un modello di post-elaborazione (da immagine a immagine).",
|
||||||
|
"exceedsMaxSize": "Le impostazioni di ampliamento superano il limite massimo delle dimensioni",
|
||||||
|
"exceedsMaxSizeDetails": "Il limite massimo di ampliamento è {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixel. Prova un'immagine più piccola o diminuisci la scala selezionata."
|
||||||
},
|
},
|
||||||
"upsell": {
|
"upsell": {
|
||||||
"inviteTeammates": "Invita collaboratori",
|
"inviteTeammates": "Invita collaboratori",
|
||||||
"shareAccess": "Condividi l'accesso",
|
"shareAccess": "Condividi l'accesso",
|
||||||
"professional": "Professionale",
|
"professional": "Professionale",
|
||||||
"professionalUpsell": "Disponibile nell'edizione Professional di Invoke. Fai clic qui o visita invoke.com/pricing per ulteriori dettagli."
|
"professionalUpsell": "Disponibile nell'edizione Professional di Invoke. Fai clic qui o visita invoke.com/pricing per ulteriori dettagli."
|
||||||
|
},
|
||||||
|
"stylePresets": {
|
||||||
|
"active": "Attivo",
|
||||||
|
"choosePromptTemplate": "Scegli un modello di prompt",
|
||||||
|
"clearTemplateSelection": "Cancella selezione modello",
|
||||||
|
"copyTemplate": "Copia modello",
|
||||||
|
"createPromptTemplate": "Crea modello di prompt",
|
||||||
|
"defaultTemplates": "Modelli predefiniti",
|
||||||
|
"deleteImage": "Elimina immagine",
|
||||||
|
"deleteTemplate": "Elimina modello",
|
||||||
|
"editTemplate": "Modifica modello",
|
||||||
|
"flatten": "Unisci il modello selezionato al prompt corrente",
|
||||||
|
"insertPlaceholder": "Inserisci segnaposto",
|
||||||
|
"myTemplates": "I miei modelli",
|
||||||
|
"name": "Nome",
|
||||||
|
"negativePrompt": "Prompt Negativo",
|
||||||
|
"noMatchingTemplates": "Nessun modello corrispondente",
|
||||||
|
"promptTemplatesDesc1": "I modelli di prompt aggiungono testo ai prompt che scrivi nelle caselle dei prompt.",
|
||||||
|
"promptTemplatesDesc3": "Se si omette il segnaposto, il modello verrà aggiunto alla fine del prompt.",
|
||||||
|
"positivePrompt": "Prompt Positivo",
|
||||||
|
"preview": "Anteprima",
|
||||||
|
"private": "Privato",
|
||||||
|
"searchByName": "Cerca per nome",
|
||||||
|
"shared": "Condiviso",
|
||||||
|
"sharedTemplates": "Modelli condivisi",
|
||||||
|
"templateDeleted": "Modello di prompt eliminato",
|
||||||
|
"toggleViewMode": "Attiva/disattiva visualizzazione",
|
||||||
|
"uploadImage": "Carica immagine",
|
||||||
|
"useForTemplate": "Usa per modello di prompt",
|
||||||
|
"viewList": "Visualizza l'elenco dei modelli",
|
||||||
|
"viewModeTooltip": "Ecco come apparirà il tuo prompt con il modello attualmente selezionato. Per modificare il tuo prompt, clicca in un punto qualsiasi della casella di testo.",
|
||||||
|
"deleteTemplate2": "Vuoi davvero eliminare questo modello? Questa operazione non può essere annullata.",
|
||||||
|
"unableToDeleteTemplate": "Impossibile eliminare il modello di prompt",
|
||||||
|
"updatePromptTemplate": "Aggiorna il modello di prompt",
|
||||||
|
"type": "Tipo",
|
||||||
|
"promptTemplatesDesc2": "Utilizza la stringa segnaposto <Pre>{{placeholder}}</Pre> per specificare dove inserire il tuo prompt nel modello.",
|
||||||
|
"importTemplates": "Importa modelli di prompt",
|
||||||
|
"importTemplatesDesc": "Il formato deve essere un CSV con colonne 'name' e 'prompt' o 'positive_prompt' e 'negative_prompt' incluse, oppure un file JSON con chiavi 'name' e 'prompt' o 'positive_prompt' e 'negative_prompt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -493,7 +493,8 @@
|
|||||||
"defaultSettingsSaved": "默认设置已保存",
|
"defaultSettingsSaved": "默认设置已保存",
|
||||||
"huggingFacePlaceholder": "所有者或模型名称",
|
"huggingFacePlaceholder": "所有者或模型名称",
|
||||||
"huggingFaceRepoID": "HuggingFace仓库ID",
|
"huggingFaceRepoID": "HuggingFace仓库ID",
|
||||||
"loraTriggerPhrases": "LoRA 触发词"
|
"loraTriggerPhrases": "LoRA 触发词",
|
||||||
|
"ipAdapters": "IP适配器"
|
||||||
},
|
},
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"images": "图像",
|
"images": "图像",
|
||||||
@ -1702,7 +1703,9 @@
|
|||||||
"upscaleModelDesc": "图像放大(图像到图像转换)模型",
|
"upscaleModelDesc": "图像放大(图像到图像转换)模型",
|
||||||
"postProcessingMissingModelWarning": "请访问 <LinkComponent>模型管理器</LinkComponent>来安装一个后处理(图像到图像转换)模型.",
|
"postProcessingMissingModelWarning": "请访问 <LinkComponent>模型管理器</LinkComponent>来安装一个后处理(图像到图像转换)模型.",
|
||||||
"missingModelsWarning": "请访问<LinkComponent>模型管理器</LinkComponent> 安装所需的模型:",
|
"missingModelsWarning": "请访问<LinkComponent>模型管理器</LinkComponent> 安装所需的模型:",
|
||||||
"mainModelDesc": "主模型(SD1.5或SDXL架构)"
|
"mainModelDesc": "主模型(SD1.5或SDXL架构)",
|
||||||
|
"exceedsMaxSize": "放大设置超出了最大尺寸限制",
|
||||||
|
"exceedsMaxSizeDetails": "最大放大限制是 {{maxUpscaleDimension}}x{{maxUpscaleDimension}} 像素.请尝试一个较小的图像或减少您的缩放选择."
|
||||||
},
|
},
|
||||||
"upsell": {
|
"upsell": {
|
||||||
"inviteTeammates": "邀请团队成员",
|
"inviteTeammates": "邀请团队成员",
|
||||||
|
@ -13,11 +13,13 @@ import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardMo
|
|||||||
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
|
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
|
||||||
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
|
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
|
||||||
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
|
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
|
||||||
|
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
|
||||||
import { configChanged } from 'features/system/store/configSlice';
|
import { configChanged } from 'features/system/store/configSlice';
|
||||||
import { languageSelector } from 'features/system/store/systemSelectors';
|
import { languageSelector } from 'features/system/store/systemSelectors';
|
||||||
import InvokeTabs from 'features/ui/components/InvokeTabs';
|
import InvokeTabs from 'features/ui/components/InvokeTabs';
|
||||||
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
|
import { useGetAndLoadLibraryWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow';
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import i18n from 'i18n';
|
import i18n from 'i18n';
|
||||||
import { size } from 'lodash-es';
|
import { size } from 'lodash-es';
|
||||||
@ -36,10 +38,11 @@ interface Props {
|
|||||||
imageName: string;
|
imageName: string;
|
||||||
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
|
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
|
||||||
};
|
};
|
||||||
|
selectedWorkflowId?: string;
|
||||||
destination?: InvokeTabName | undefined;
|
destination?: InvokeTabName | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const App = ({ config = DEFAULT_CONFIG, selectedImage, destination }: Props) => {
|
const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, destination }: Props) => {
|
||||||
const language = useAppSelector(languageSelector);
|
const language = useAppSelector(languageSelector);
|
||||||
const logger = useLogger('system');
|
const logger = useLogger('system');
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -70,6 +73,14 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage, destination }: Props) =>
|
|||||||
}
|
}
|
||||||
}, [dispatch, config, logger]);
|
}, [dispatch, config, logger]);
|
||||||
|
|
||||||
|
const { getAndLoadWorkflow } = useGetAndLoadLibraryWorkflow();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedWorkflowId) {
|
||||||
|
getAndLoadWorkflow(selectedWorkflowId);
|
||||||
|
}
|
||||||
|
}, [selectedWorkflowId, getAndLoadWorkflow]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (destination) {
|
if (destination) {
|
||||||
dispatch(setActiveTab(destination));
|
dispatch(setActiveTab(destination));
|
||||||
@ -104,6 +115,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage, destination }: Props) =>
|
|||||||
<DeleteImageModal />
|
<DeleteImageModal />
|
||||||
<ChangeBoardModal />
|
<ChangeBoardModal />
|
||||||
<DynamicPromptsModal />
|
<DynamicPromptsModal />
|
||||||
|
<StylePresetModal />
|
||||||
<PreselectedImage selectedImage={selectedImage} />
|
<PreselectedImage selectedImage={selectedImage} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
@ -44,6 +44,7 @@ interface Props extends PropsWithChildren {
|
|||||||
imageName: string;
|
imageName: string;
|
||||||
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
|
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
|
||||||
};
|
};
|
||||||
|
selectedWorkflowId?: string;
|
||||||
destination?: InvokeTabName;
|
destination?: InvokeTabName;
|
||||||
customStarUi?: CustomStarUi;
|
customStarUi?: CustomStarUi;
|
||||||
socketOptions?: Partial<ManagerOptions & SocketOptions>;
|
socketOptions?: Partial<ManagerOptions & SocketOptions>;
|
||||||
@ -64,6 +65,7 @@ const InvokeAIUI = ({
|
|||||||
projectUrl,
|
projectUrl,
|
||||||
queueId,
|
queueId,
|
||||||
selectedImage,
|
selectedImage,
|
||||||
|
selectedWorkflowId,
|
||||||
destination,
|
destination,
|
||||||
customStarUi,
|
customStarUi,
|
||||||
socketOptions,
|
socketOptions,
|
||||||
@ -221,7 +223,12 @@ const InvokeAIUI = ({
|
|||||||
<React.Suspense fallback={<Loading />}>
|
<React.Suspense fallback={<Loading />}>
|
||||||
<ThemeLocaleProvider>
|
<ThemeLocaleProvider>
|
||||||
<AppDndContext>
|
<AppDndContext>
|
||||||
<App config={config} selectedImage={selectedImage} destination={destination} />
|
<App
|
||||||
|
config={config}
|
||||||
|
selectedImage={selectedImage}
|
||||||
|
selectedWorkflowId={selectedWorkflowId}
|
||||||
|
destination={destination}
|
||||||
|
/>
|
||||||
</AppDndContext>
|
</AppDndContext>
|
||||||
</ThemeLocaleProvider>
|
</ThemeLocaleProvider>
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
|
@ -11,6 +11,8 @@ import {
|
|||||||
promptsChanged,
|
promptsChanged,
|
||||||
} from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
} from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||||
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
|
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
|
||||||
|
import { getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
|
||||||
|
import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
|
||||||
import { utilitiesApi } from 'services/api/endpoints/utilities';
|
import { utilitiesApi } from 'services/api/endpoints/utilities';
|
||||||
import { socketConnected } from 'services/events/actions';
|
import { socketConnected } from 'services/events/actions';
|
||||||
|
|
||||||
@ -19,7 +21,8 @@ const matcher = isAnyOf(
|
|||||||
combinatorialToggled,
|
combinatorialToggled,
|
||||||
maxPromptsChanged,
|
maxPromptsChanged,
|
||||||
maxPromptsReset,
|
maxPromptsReset,
|
||||||
socketConnected
|
socketConnected,
|
||||||
|
activeStylePresetIdChanged
|
||||||
);
|
);
|
||||||
|
|
||||||
export const addDynamicPromptsListener = (startAppListening: AppStartListening) => {
|
export const addDynamicPromptsListener = (startAppListening: AppStartListening) => {
|
||||||
@ -28,7 +31,7 @@ export const addDynamicPromptsListener = (startAppListening: AppStartListening)
|
|||||||
effect: async (action, { dispatch, getState, cancelActiveListeners, delay }) => {
|
effect: async (action, { dispatch, getState, cancelActiveListeners, delay }) => {
|
||||||
cancelActiveListeners();
|
cancelActiveListeners();
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const { positivePrompt } = state.controlLayers.present;
|
const { positivePrompt } = getPresetModifiedPrompts(state);
|
||||||
const { maxPrompts } = state.dynamicPrompts;
|
const { maxPrompts } = state.dynamicPrompts;
|
||||||
|
|
||||||
if (state.config.disabledFeatures.includes('dynamicPrompting')) {
|
if (state.config.disabledFeatures.includes('dynamicPrompting')) {
|
||||||
|
@ -28,6 +28,7 @@ import { generationPersistConfig, generationSlice } from 'features/parameters/st
|
|||||||
import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/upscaleSlice';
|
import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/upscaleSlice';
|
||||||
import { queueSlice } from 'features/queue/store/queueSlice';
|
import { queueSlice } from 'features/queue/store/queueSlice';
|
||||||
import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice';
|
import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice';
|
||||||
|
import { stylePresetPersistConfig, stylePresetSlice } from 'features/stylePresets/store/stylePresetSlice';
|
||||||
import { configSlice } from 'features/system/store/configSlice';
|
import { configSlice } from 'features/system/store/configSlice';
|
||||||
import { systemPersistConfig, systemSlice } from 'features/system/store/systemSlice';
|
import { systemPersistConfig, systemSlice } from 'features/system/store/systemSlice';
|
||||||
import { uiPersistConfig, uiSlice } from 'features/ui/store/uiSlice';
|
import { uiPersistConfig, uiSlice } from 'features/ui/store/uiSlice';
|
||||||
@ -69,6 +70,7 @@ const allReducers = {
|
|||||||
[workflowSettingsSlice.name]: workflowSettingsSlice.reducer,
|
[workflowSettingsSlice.name]: workflowSettingsSlice.reducer,
|
||||||
[api.reducerPath]: api.reducer,
|
[api.reducerPath]: api.reducer,
|
||||||
[upscaleSlice.name]: upscaleSlice.reducer,
|
[upscaleSlice.name]: upscaleSlice.reducer,
|
||||||
|
[stylePresetSlice.name]: stylePresetSlice.reducer,
|
||||||
};
|
};
|
||||||
|
|
||||||
const rootReducer = combineReducers(allReducers);
|
const rootReducer = combineReducers(allReducers);
|
||||||
@ -114,6 +116,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
|
|||||||
[controlLayersPersistConfig.name]: controlLayersPersistConfig,
|
[controlLayersPersistConfig.name]: controlLayersPersistConfig,
|
||||||
[workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig,
|
[workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig,
|
||||||
[upscalePersistConfig.name]: upscalePersistConfig,
|
[upscalePersistConfig.name]: upscalePersistConfig,
|
||||||
|
[stylePresetPersistConfig.name]: stylePresetPersistConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
const unserialize: UnserializeFunction = (data, key) => {
|
const unserialize: UnserializeFunction = (data, key) => {
|
||||||
@ -164,8 +167,8 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
|
|||||||
reducer: rememberedRootReducer,
|
reducer: rememberedRootReducer,
|
||||||
middleware: (getDefaultMiddleware) =>
|
middleware: (getDefaultMiddleware) =>
|
||||||
getDefaultMiddleware({
|
getDefaultMiddleware({
|
||||||
serializableCheck: false,
|
serializableCheck: import.meta.env.MODE === 'development',
|
||||||
immutableCheck: false,
|
immutableCheck: import.meta.env.MODE === 'development',
|
||||||
})
|
})
|
||||||
.concat(api.middleware)
|
.concat(api.middleware)
|
||||||
.concat(dynamicMiddlewares)
|
.concat(dynamicMiddlewares)
|
||||||
|
@ -71,6 +71,7 @@ export type AppConfig = {
|
|||||||
*/
|
*/
|
||||||
maxUpscaleDimension?: number;
|
maxUpscaleDimension?: number;
|
||||||
allowPrivateBoards: boolean;
|
allowPrivateBoards: boolean;
|
||||||
|
allowPrivateStylePresets: boolean;
|
||||||
disabledTabs: InvokeTabName[];
|
disabledTabs: InvokeTabName[];
|
||||||
disabledFeatures: AppFeature[];
|
disabledFeatures: AppFeature[];
|
||||||
disabledSDFeatures: SDFeature[];
|
disabledSDFeatures: SDFeature[];
|
||||||
|
@ -47,6 +47,7 @@ export const IAINoContentFallback = memo((props: IAINoImageFallbackProps) => {
|
|||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
color: 'base.500',
|
color: 'base.500',
|
||||||
|
fontSize: 'md',
|
||||||
...sx,
|
...sx,
|
||||||
}),
|
}),
|
||||||
[sx]
|
[sx]
|
||||||
@ -55,11 +56,7 @@ export const IAINoContentFallback = memo((props: IAINoImageFallbackProps) => {
|
|||||||
return (
|
return (
|
||||||
<Flex sx={styles} {...rest}>
|
<Flex sx={styles} {...rest}>
|
||||||
{icon && <Icon as={icon} boxSize={boxSize} opacity={0.7} />}
|
{icon && <Icon as={icon} boxSize={boxSize} opacity={0.7} />}
|
||||||
{props.label && (
|
{props.label && <Text textAlign="center">{props.label}</Text>}
|
||||||
<Text textAlign="center" fontSize="md">
|
|
||||||
{props.label}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useImageUrlToBlob } from 'common/hooks/useImageUrlToBlob';
|
import { convertImageUrlToBlob } from 'common/util/convertImageUrlToBlob';
|
||||||
import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard';
|
import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard';
|
||||||
import { toast } from 'features/toast/toast';
|
import { toast } from 'features/toast/toast';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
export const useCopyImageToClipboard = () => {
|
export const useCopyImageToClipboard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const imageUrlToBlob = useImageUrlToBlob();
|
|
||||||
|
|
||||||
const isClipboardAPIAvailable = useMemo(() => {
|
const isClipboardAPIAvailable = useMemo(() => {
|
||||||
return Boolean(navigator.clipboard) && Boolean(window.ClipboardItem);
|
return Boolean(navigator.clipboard) && Boolean(window.ClipboardItem);
|
||||||
@ -23,7 +22,7 @@ export const useCopyImageToClipboard = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const blob = await imageUrlToBlob(image_url);
|
const blob = await convertImageUrlToBlob(image_url);
|
||||||
|
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
throw new Error('Unable to create Blob');
|
throw new Error('Unable to create Blob');
|
||||||
@ -45,7 +44,7 @@ export const useCopyImageToClipboard = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[imageUrlToBlob, isClipboardAPIAvailable, t]
|
[isClipboardAPIAvailable, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { isClipboardAPIAvailable, copyImageToClipboard };
|
return { isClipboardAPIAvailable, copyImageToClipboard };
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
import { $authToken } from 'app/store/nanostores/authToken';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts an image URL to a Blob by creating an <img /> element, drawing it to canvas
|
|
||||||
* and then converting the canvas to a Blob.
|
|
||||||
*
|
|
||||||
* @returns A function that takes a URL and returns a Promise that resolves with a Blob
|
|
||||||
*/
|
|
||||||
export const useImageUrlToBlob = () => {
|
|
||||||
const imageUrlToBlob = useCallback(
|
|
||||||
async (url: string) =>
|
|
||||||
new Promise<Blob | null>((resolve) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => {
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = img.width;
|
|
||||||
canvas.height = img.height;
|
|
||||||
|
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
if (!context) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
context.drawImage(img, 0, 0);
|
|
||||||
resolve(
|
|
||||||
new Promise<Blob | null>((resolve) => {
|
|
||||||
canvas.toBlob(function (blob) {
|
|
||||||
resolve(blob);
|
|
||||||
}, 'image/png');
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
img.crossOrigin = $authToken.get() ? 'use-credentials' : 'anonymous';
|
|
||||||
img.src = url;
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return imageUrlToBlob;
|
|
||||||
};
|
|
@ -0,0 +1,33 @@
|
|||||||
|
import { $authToken } from 'app/store/nanostores/authToken';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an image URL to a Blob by creating an <img /> element, drawing it to canvas
|
||||||
|
* and then converting the canvas to a Blob.
|
||||||
|
*
|
||||||
|
* @returns A function that takes a URL and returns a Promise that resolves with a Blob
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const convertImageUrlToBlob = async (url: string) =>
|
||||||
|
new Promise<Blob | null>((resolve) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
if (!context) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context.drawImage(img, 0, 0);
|
||||||
|
resolve(
|
||||||
|
new Promise<Blob | null>((resolve) => {
|
||||||
|
canvas.toBlob(function (blob) {
|
||||||
|
resolve(blob);
|
||||||
|
}, 'image/png');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
img.crossOrigin = $authToken.get() ? 'use-credentials' : 'anonymous';
|
||||||
|
img.src = url;
|
||||||
|
});
|
@ -30,6 +30,7 @@ import {
|
|||||||
PiFlowArrowBold,
|
PiFlowArrowBold,
|
||||||
PiFoldersBold,
|
PiFoldersBold,
|
||||||
PiImagesBold,
|
PiImagesBold,
|
||||||
|
PiPaintBrushBold,
|
||||||
PiPlantBold,
|
PiPlantBold,
|
||||||
PiQuotesBold,
|
PiQuotesBold,
|
||||||
PiShareFatBold,
|
PiShareFatBold,
|
||||||
@ -55,8 +56,17 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
const { downloadImage } = useDownloadImage();
|
const { downloadImage } = useDownloadImage();
|
||||||
const templates = useStore($templates);
|
const templates = useStore($templates);
|
||||||
|
|
||||||
const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, isLoadingMetadata } =
|
const {
|
||||||
useImageActions(imageDTO?.image_name);
|
recallAll,
|
||||||
|
remix,
|
||||||
|
recallSeed,
|
||||||
|
recallPrompts,
|
||||||
|
hasMetadata,
|
||||||
|
hasSeed,
|
||||||
|
hasPrompts,
|
||||||
|
isLoadingMetadata,
|
||||||
|
createAsPreset,
|
||||||
|
} = useImageActions(imageDTO?.image_name);
|
||||||
|
|
||||||
const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({});
|
const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({});
|
||||||
|
|
||||||
@ -182,6 +192,13 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
>
|
>
|
||||||
{t('parameters.useAll')}
|
{t('parameters.useAll')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiPaintBrushBold />}
|
||||||
|
onClickCapture={createAsPreset}
|
||||||
|
isDisabled={isLoadingMetadata || !hasPrompts}
|
||||||
|
>
|
||||||
|
{t('stylePresets.useForTemplate')}
|
||||||
|
</MenuItem>
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToImageToImage} id="send-to-img2img">
|
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToImageToImage} id="send-to-img2img">
|
||||||
{t('parameters.sendToImg2Img')}
|
{t('parameters.sendToImg2Img')}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { handlers, parseAndRecallAllMetadata, parseAndRecallPrompts } from 'features/metadata/util/handlers';
|
import { handlers, parseAndRecallAllMetadata, parseAndRecallPrompts } from 'features/metadata/util/handlers';
|
||||||
|
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||||
|
|
||||||
export const useImageActions = (image_name?: string) => {
|
export const useImageActions = (image_name?: string) => {
|
||||||
@ -10,6 +13,7 @@ export const useImageActions = (image_name?: string) => {
|
|||||||
const [hasMetadata, setHasMetadata] = useState(false);
|
const [hasMetadata, setHasMetadata] = useState(false);
|
||||||
const [hasSeed, setHasSeed] = useState(false);
|
const [hasSeed, setHasSeed] = useState(false);
|
||||||
const [hasPrompts, setHasPrompts] = useState(false);
|
const [hasPrompts, setHasPrompts] = useState(false);
|
||||||
|
const { data: imageDTO } = useGetImageDTOQuery(image_name ?? skipToken);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const parseMetadata = async () => {
|
const parseMetadata = async () => {
|
||||||
@ -61,5 +65,34 @@ export const useImageActions = (image_name?: string) => {
|
|||||||
parseAndRecallPrompts(metadata);
|
parseAndRecallPrompts(metadata);
|
||||||
}, [metadata]);
|
}, [metadata]);
|
||||||
|
|
||||||
return { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, isLoadingMetadata };
|
const createAsPreset = useCallback(async () => {
|
||||||
|
if (image_name && metadata && imageDTO) {
|
||||||
|
const positivePrompt = await handlers.positivePrompt.parse(metadata);
|
||||||
|
const negativePrompt = await handlers.negativePrompt.parse(metadata);
|
||||||
|
|
||||||
|
$stylePresetModalState.set({
|
||||||
|
prefilledFormData: {
|
||||||
|
name: '',
|
||||||
|
positivePrompt,
|
||||||
|
negativePrompt,
|
||||||
|
imageUrl: imageDTO.image_url,
|
||||||
|
type: 'user',
|
||||||
|
},
|
||||||
|
updatingStylePresetId: null,
|
||||||
|
isModalOpen: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [image_name, metadata, imageDTO]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
recallAll,
|
||||||
|
remix,
|
||||||
|
recallSeed,
|
||||||
|
recallPrompts,
|
||||||
|
hasMetadata,
|
||||||
|
hasSeed,
|
||||||
|
hasPrompts,
|
||||||
|
isLoadingMetadata,
|
||||||
|
createAsPreset,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -22,11 +22,10 @@ import {
|
|||||||
} from './constants';
|
} from './constants';
|
||||||
import { addLoRAs } from './generation/addLoRAs';
|
import { addLoRAs } from './generation/addLoRAs';
|
||||||
import { addSDXLLoRas } from './generation/addSDXLLoRAs';
|
import { addSDXLLoRas } from './generation/addSDXLLoRAs';
|
||||||
import { getBoardField, getSDXLStylePrompts } from './graphBuilderUtils';
|
import { getBoardField, getPresetModifiedPrompts } from './graphBuilderUtils';
|
||||||
|
|
||||||
export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise<GraphType> => {
|
export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise<GraphType> => {
|
||||||
const { model, cfgScale: cfg_scale, scheduler, steps, vaePrecision, seed, vae } = state.generation;
|
const { model, cfgScale: cfg_scale, scheduler, steps, vaePrecision, seed, vae } = state.generation;
|
||||||
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
|
||||||
const { upscaleModel, upscaleInitialImage, structure, creativity, tileControlnetModel, scale } = state.upscale;
|
const { upscaleModel, upscaleInitialImage, structure, creativity, tileControlnetModel, scale } = state.upscale;
|
||||||
|
|
||||||
assert(model, 'No model found in state');
|
assert(model, 'No model found in state');
|
||||||
@ -99,7 +98,8 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise
|
|||||||
let modelNode;
|
let modelNode;
|
||||||
|
|
||||||
if (model.base === 'sdxl') {
|
if (model.base === 'sdxl') {
|
||||||
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
|
const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } =
|
||||||
|
getPresetModifiedPrompts(state);
|
||||||
|
|
||||||
posCondNode = g.addNode({
|
posCondNode = g.addNode({
|
||||||
type: 'sdxl_compel_prompt',
|
type: 'sdxl_compel_prompt',
|
||||||
@ -132,6 +132,8 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise
|
|||||||
negative_style_prompt: negativeStylePrompt,
|
negative_style_prompt: negativeStylePrompt,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
|
||||||
|
|
||||||
posCondNode = g.addNode({
|
posCondNode = g.addNode({
|
||||||
type: 'compel',
|
type: 'compel',
|
||||||
id: POSITIVE_CONDITIONING,
|
id: POSITIVE_CONDITIONING,
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
SDXL_REFINER_POSITIVE_CONDITIONING,
|
SDXL_REFINER_POSITIVE_CONDITIONING,
|
||||||
SDXL_REFINER_SEAMLESS,
|
SDXL_REFINER_SEAMLESS,
|
||||||
} from 'features/nodes/util/graph/constants';
|
} from 'features/nodes/util/graph/constants';
|
||||||
import { getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils';
|
import { getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
|
||||||
import type { NonNullableGraph } from 'services/api/types';
|
import type { NonNullableGraph } from 'services/api/types';
|
||||||
import { isRefinerMainModelModelConfig } from 'services/api/types';
|
import { isRefinerMainModelModelConfig } from 'services/api/types';
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ export const addSDXLRefinerToGraph = async (
|
|||||||
const modelLoaderId = modelLoaderNodeId ? modelLoaderNodeId : SDXL_MODEL_LOADER;
|
const modelLoaderId = modelLoaderNodeId ? modelLoaderNodeId : SDXL_MODEL_LOADER;
|
||||||
|
|
||||||
// Construct Style Prompt
|
// Construct Style Prompt
|
||||||
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
|
const { positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state);
|
||||||
|
|
||||||
// Unplug SDXL Latents Generation To Latents To Image
|
// Unplug SDXL Latents Generation To Latents To Image
|
||||||
graph.edges = graph.edges.filter((e) => !(e.source.node_id === baseNodeId && ['latents'].includes(e.source.field)));
|
graph.edges = graph.edges.filter((e) => !(e.source.node_id === baseNodeId && ['latents'].includes(e.source.field)));
|
||||||
|
@ -16,7 +16,11 @@ import {
|
|||||||
POSITIVE_CONDITIONING,
|
POSITIVE_CONDITIONING,
|
||||||
SEAMLESS,
|
SEAMLESS,
|
||||||
} from 'features/nodes/util/graph/constants';
|
} from 'features/nodes/util/graph/constants';
|
||||||
import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils';
|
import {
|
||||||
|
getBoardField,
|
||||||
|
getIsIntermediate,
|
||||||
|
getPresetModifiedPrompts,
|
||||||
|
} from 'features/nodes/util/graph/graphBuilderUtils';
|
||||||
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
|
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
|
||||||
import { isNonRefinerMainModelConfig } from 'services/api/types';
|
import { isNonRefinerMainModelConfig } from 'services/api/types';
|
||||||
|
|
||||||
@ -51,7 +55,6 @@ export const buildCanvasImageToImageGraph = async (
|
|||||||
seamlessXAxis,
|
seamlessXAxis,
|
||||||
seamlessYAxis,
|
seamlessYAxis,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
|
||||||
|
|
||||||
// The bounding box determines width and height, not the width and height params
|
// The bounding box determines width and height, not the width and height params
|
||||||
const { width, height } = state.canvas.boundingBoxDimensions;
|
const { width, height } = state.canvas.boundingBoxDimensions;
|
||||||
@ -71,6 +74,8 @@ export const buildCanvasImageToImageGraph = async (
|
|||||||
|
|
||||||
const use_cpu = shouldUseCpuNoise;
|
const use_cpu = shouldUseCpuNoise;
|
||||||
|
|
||||||
|
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the
|
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the
|
||||||
* full graph here as a template. Then use the parameters from app state and set friendlier node
|
* full graph here as a template. Then use the parameters from app state and set friendlier node
|
||||||
|
@ -19,7 +19,11 @@ import {
|
|||||||
POSITIVE_CONDITIONING,
|
POSITIVE_CONDITIONING,
|
||||||
SEAMLESS,
|
SEAMLESS,
|
||||||
} from 'features/nodes/util/graph/constants';
|
} from 'features/nodes/util/graph/constants';
|
||||||
import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils';
|
import {
|
||||||
|
getBoardField,
|
||||||
|
getIsIntermediate,
|
||||||
|
getPresetModifiedPrompts,
|
||||||
|
} from 'features/nodes/util/graph/graphBuilderUtils';
|
||||||
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
|
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
|
||||||
|
|
||||||
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
|
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
|
||||||
@ -58,7 +62,6 @@ export const buildCanvasInpaintGraph = async (
|
|||||||
canvasCoherenceEdgeSize,
|
canvasCoherenceEdgeSize,
|
||||||
maskBlur,
|
maskBlur,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
log.error('No model found in state');
|
log.error('No model found in state');
|
||||||
@ -79,6 +82,8 @@ export const buildCanvasInpaintGraph = async (
|
|||||||
|
|
||||||
const use_cpu = shouldUseCpuNoise;
|
const use_cpu = shouldUseCpuNoise;
|
||||||
|
|
||||||
|
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
|
||||||
|
|
||||||
const graph: NonNullableGraph = {
|
const graph: NonNullableGraph = {
|
||||||
id: CANVAS_INPAINT_GRAPH,
|
id: CANVAS_INPAINT_GRAPH,
|
||||||
nodes: {
|
nodes: {
|
||||||
|
@ -23,7 +23,11 @@ import {
|
|||||||
POSITIVE_CONDITIONING,
|
POSITIVE_CONDITIONING,
|
||||||
SEAMLESS,
|
SEAMLESS,
|
||||||
} from 'features/nodes/util/graph/constants';
|
} from 'features/nodes/util/graph/constants';
|
||||||
import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils';
|
import {
|
||||||
|
getBoardField,
|
||||||
|
getIsIntermediate,
|
||||||
|
getPresetModifiedPrompts,
|
||||||
|
} from 'features/nodes/util/graph/graphBuilderUtils';
|
||||||
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
|
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
|
||||||
|
|
||||||
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
|
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
|
||||||
@ -70,7 +74,6 @@ export const buildCanvasOutpaintGraph = async (
|
|||||||
canvasCoherenceEdgeSize,
|
canvasCoherenceEdgeSize,
|
||||||
maskBlur,
|
maskBlur,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
log.error('No model found in state');
|
log.error('No model found in state');
|
||||||
@ -91,6 +94,8 @@ export const buildCanvasOutpaintGraph = async (
|
|||||||
|
|
||||||
const use_cpu = shouldUseCpuNoise;
|
const use_cpu = shouldUseCpuNoise;
|
||||||
|
|
||||||
|
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
|
||||||
|
|
||||||
const graph: NonNullableGraph = {
|
const graph: NonNullableGraph = {
|
||||||
id: CANVAS_OUTPAINT_GRAPH,
|
id: CANVAS_OUTPAINT_GRAPH,
|
||||||
nodes: {
|
nodes: {
|
||||||
|
@ -16,7 +16,11 @@ import {
|
|||||||
SDXL_REFINER_SEAMLESS,
|
SDXL_REFINER_SEAMLESS,
|
||||||
SEAMLESS,
|
SEAMLESS,
|
||||||
} from 'features/nodes/util/graph/constants';
|
} from 'features/nodes/util/graph/constants';
|
||||||
import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils';
|
import {
|
||||||
|
getBoardField,
|
||||||
|
getIsIntermediate,
|
||||||
|
getPresetModifiedPrompts,
|
||||||
|
} from 'features/nodes/util/graph/graphBuilderUtils';
|
||||||
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
|
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
|
||||||
import { isNonRefinerMainModelConfig } from 'services/api/types';
|
import { isNonRefinerMainModelConfig } from 'services/api/types';
|
||||||
|
|
||||||
@ -51,7 +55,6 @@ export const buildCanvasSDXLImageToImageGraph = async (
|
|||||||
seamlessYAxis,
|
seamlessYAxis,
|
||||||
img2imgStrength: strength,
|
img2imgStrength: strength,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
|
||||||
|
|
||||||
const { refinerModel, refinerStart } = state.sdxl;
|
const { refinerModel, refinerStart } = state.sdxl;
|
||||||
|
|
||||||
@ -75,7 +78,7 @@ export const buildCanvasSDXLImageToImageGraph = async (
|
|||||||
const use_cpu = shouldUseCpuNoise;
|
const use_cpu = shouldUseCpuNoise;
|
||||||
|
|
||||||
// Construct Style Prompt
|
// Construct Style Prompt
|
||||||
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
|
const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the
|
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the
|
||||||
|
@ -19,7 +19,11 @@ import {
|
|||||||
SDXL_REFINER_SEAMLESS,
|
SDXL_REFINER_SEAMLESS,
|
||||||
SEAMLESS,
|
SEAMLESS,
|
||||||
} from 'features/nodes/util/graph/constants';
|
} from 'features/nodes/util/graph/constants';
|
||||||
import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils';
|
import {
|
||||||
|
getBoardField,
|
||||||
|
getIsIntermediate,
|
||||||
|
getPresetModifiedPrompts,
|
||||||
|
} from 'features/nodes/util/graph/graphBuilderUtils';
|
||||||
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
|
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
|
||||||
|
|
||||||
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
|
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
|
||||||
@ -58,7 +62,6 @@ export const buildCanvasSDXLInpaintGraph = async (
|
|||||||
canvasCoherenceEdgeSize,
|
canvasCoherenceEdgeSize,
|
||||||
maskBlur,
|
maskBlur,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
|
||||||
|
|
||||||
const { refinerModel, refinerStart } = state.sdxl;
|
const { refinerModel, refinerStart } = state.sdxl;
|
||||||
|
|
||||||
@ -83,7 +86,7 @@ export const buildCanvasSDXLInpaintGraph = async (
|
|||||||
const use_cpu = shouldUseCpuNoise;
|
const use_cpu = shouldUseCpuNoise;
|
||||||
|
|
||||||
// Construct Style Prompt
|
// Construct Style Prompt
|
||||||
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
|
const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state);
|
||||||
|
|
||||||
const graph: NonNullableGraph = {
|
const graph: NonNullableGraph = {
|
||||||
id: SDXL_CANVAS_INPAINT_GRAPH,
|
id: SDXL_CANVAS_INPAINT_GRAPH,
|
||||||
|
@ -23,7 +23,11 @@ import {
|
|||||||
SDXL_REFINER_SEAMLESS,
|
SDXL_REFINER_SEAMLESS,
|
||||||
SEAMLESS,
|
SEAMLESS,
|
||||||
} from 'features/nodes/util/graph/constants';
|
} from 'features/nodes/util/graph/constants';
|
||||||
import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils';
|
import {
|
||||||
|
getBoardField,
|
||||||
|
getIsIntermediate,
|
||||||
|
getPresetModifiedPrompts,
|
||||||
|
} from 'features/nodes/util/graph/graphBuilderUtils';
|
||||||
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
|
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
|
||||||
|
|
||||||
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
|
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
|
||||||
@ -70,7 +74,6 @@ export const buildCanvasSDXLOutpaintGraph = async (
|
|||||||
canvasCoherenceEdgeSize,
|
canvasCoherenceEdgeSize,
|
||||||
maskBlur,
|
maskBlur,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
|
||||||
|
|
||||||
const { refinerModel, refinerStart } = state.sdxl;
|
const { refinerModel, refinerStart } = state.sdxl;
|
||||||
|
|
||||||
@ -94,7 +97,7 @@ export const buildCanvasSDXLOutpaintGraph = async (
|
|||||||
const use_cpu = shouldUseCpuNoise;
|
const use_cpu = shouldUseCpuNoise;
|
||||||
|
|
||||||
// Construct Style Prompt
|
// Construct Style Prompt
|
||||||
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
|
const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state);
|
||||||
|
|
||||||
const graph: NonNullableGraph = {
|
const graph: NonNullableGraph = {
|
||||||
id: SDXL_CANVAS_OUTPAINT_GRAPH,
|
id: SDXL_CANVAS_OUTPAINT_GRAPH,
|
||||||
|
@ -14,7 +14,11 @@ import {
|
|||||||
SDXL_REFINER_SEAMLESS,
|
SDXL_REFINER_SEAMLESS,
|
||||||
SEAMLESS,
|
SEAMLESS,
|
||||||
} from 'features/nodes/util/graph/constants';
|
} from 'features/nodes/util/graph/constants';
|
||||||
import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils';
|
import {
|
||||||
|
getBoardField,
|
||||||
|
getIsIntermediate,
|
||||||
|
getPresetModifiedPrompts,
|
||||||
|
} from 'features/nodes/util/graph/graphBuilderUtils';
|
||||||
import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types';
|
import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types';
|
||||||
|
|
||||||
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
|
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
|
||||||
@ -44,7 +48,6 @@ export const buildCanvasSDXLTextToImageGraph = async (state: RootState): Promise
|
|||||||
seamlessXAxis,
|
seamlessXAxis,
|
||||||
seamlessYAxis,
|
seamlessYAxis,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
|
||||||
|
|
||||||
// The bounding box determines width and height, not the width and height params
|
// The bounding box determines width and height, not the width and height params
|
||||||
const { width, height } = state.canvas.boundingBoxDimensions;
|
const { width, height } = state.canvas.boundingBoxDimensions;
|
||||||
@ -67,7 +70,7 @@ export const buildCanvasSDXLTextToImageGraph = async (state: RootState): Promise
|
|||||||
let modelLoaderNodeId = SDXL_MODEL_LOADER;
|
let modelLoaderNodeId = SDXL_MODEL_LOADER;
|
||||||
|
|
||||||
// Construct Style Prompt
|
// Construct Style Prompt
|
||||||
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
|
const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the
|
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the
|
||||||
|
@ -14,7 +14,11 @@ import {
|
|||||||
POSITIVE_CONDITIONING,
|
POSITIVE_CONDITIONING,
|
||||||
SEAMLESS,
|
SEAMLESS,
|
||||||
} from 'features/nodes/util/graph/constants';
|
} from 'features/nodes/util/graph/constants';
|
||||||
import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils';
|
import {
|
||||||
|
getBoardField,
|
||||||
|
getIsIntermediate,
|
||||||
|
getPresetModifiedPrompts,
|
||||||
|
} from 'features/nodes/util/graph/graphBuilderUtils';
|
||||||
import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types';
|
import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types';
|
||||||
|
|
||||||
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
|
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
|
||||||
@ -44,7 +48,6 @@ export const buildCanvasTextToImageGraph = async (state: RootState): Promise<Non
|
|||||||
seamlessXAxis,
|
seamlessXAxis,
|
||||||
seamlessYAxis,
|
seamlessYAxis,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
|
||||||
|
|
||||||
// The bounding box determines width and height, not the width and height params
|
// The bounding box determines width and height, not the width and height params
|
||||||
const { width, height } = state.canvas.boundingBoxDimensions;
|
const { width, height } = state.canvas.boundingBoxDimensions;
|
||||||
@ -64,6 +67,8 @@ export const buildCanvasTextToImageGraph = async (state: RootState): Promise<Non
|
|||||||
|
|
||||||
let modelLoaderNodeId = MAIN_MODEL_LOADER;
|
let modelLoaderNodeId = MAIN_MODEL_LOADER;
|
||||||
|
|
||||||
|
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the
|
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the
|
||||||
* full graph here as a template. Then use the parameters from app state and set friendlier node
|
* full graph here as a template. Then use the parameters from app state and set friendlier node
|
||||||
|
@ -22,7 +22,7 @@ import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless';
|
|||||||
import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker';
|
import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker';
|
||||||
import type { GraphType } from 'features/nodes/util/graph/generation/Graph';
|
import type { GraphType } from 'features/nodes/util/graph/generation/Graph';
|
||||||
import { Graph } from 'features/nodes/util/graph/generation/Graph';
|
import { Graph } from 'features/nodes/util/graph/generation/Graph';
|
||||||
import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils';
|
import { getBoardField, getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
|
||||||
import type { Invocation } from 'services/api/types';
|
import type { Invocation } from 'services/api/types';
|
||||||
import { isNonRefinerMainModelConfig } from 'services/api/types';
|
import { isNonRefinerMainModelConfig } from 'services/api/types';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
@ -40,11 +40,12 @@ export const buildGenerationTabGraph = async (state: RootState): Promise<GraphTy
|
|||||||
seed,
|
seed,
|
||||||
vae,
|
vae,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
|
||||||
const { width, height } = state.controlLayers.present.size;
|
const { width, height } = state.controlLayers.present.size;
|
||||||
|
|
||||||
assert(model, 'No model found in state');
|
assert(model, 'No model found in state');
|
||||||
|
|
||||||
|
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
|
||||||
|
|
||||||
const g = new Graph(CONTROL_LAYERS_GRAPH);
|
const g = new Graph(CONTROL_LAYERS_GRAPH);
|
||||||
const modelLoader = g.addNode({
|
const modelLoader = g.addNode({
|
||||||
type: 'main_model_loader',
|
type: 'main_model_loader',
|
||||||
|
@ -19,7 +19,7 @@ import { addSDXLRefiner } from 'features/nodes/util/graph/generation/addSDXLRefi
|
|||||||
import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless';
|
import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless';
|
||||||
import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker';
|
import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker';
|
||||||
import { Graph } from 'features/nodes/util/graph/generation/Graph';
|
import { Graph } from 'features/nodes/util/graph/generation/Graph';
|
||||||
import { getBoardField, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils';
|
import { getBoardField, getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
|
||||||
import type { Invocation, NonNullableGraph } from 'services/api/types';
|
import type { Invocation, NonNullableGraph } from 'services/api/types';
|
||||||
import { isNonRefinerMainModelConfig } from 'services/api/types';
|
import { isNonRefinerMainModelConfig } from 'services/api/types';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
@ -36,14 +36,13 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise<Non
|
|||||||
vaePrecision,
|
vaePrecision,
|
||||||
vae,
|
vae,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
|
||||||
const { width, height } = state.controlLayers.present.size;
|
const { width, height } = state.controlLayers.present.size;
|
||||||
|
|
||||||
const { refinerModel, refinerStart } = state.sdxl;
|
const { refinerModel, refinerStart } = state.sdxl;
|
||||||
|
|
||||||
assert(model, 'No model found in state');
|
assert(model, 'No model found in state');
|
||||||
|
|
||||||
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
|
const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state);
|
||||||
|
|
||||||
const g = new Graph(SDXL_CONTROL_LAYERS_GRAPH);
|
const g = new Graph(SDXL_CONTROL_LAYERS_GRAPH);
|
||||||
const modelLoader = g.addNode({
|
const modelLoader = g.addNode({
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import type { RootState } from 'app/store/store';
|
import type { RootState } from 'app/store/store';
|
||||||
import type { BoardField } from 'features/nodes/types/common';
|
import type { BoardField } from 'features/nodes/types/common';
|
||||||
|
import { buildPresetModifiedPrompt } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
|
import { stylePresetsApi } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the board field, based on the autoAddBoardId setting.
|
* Gets the board field, based on the autoAddBoardId setting.
|
||||||
@ -14,13 +16,43 @@ export const getBoardField = (state: RootState): BoardField | undefined => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the SDXL style prompts, based on the concat setting.
|
* Gets the prompts, modified for the active style preset.
|
||||||
*/
|
*/
|
||||||
export const getSDXLStylePrompts = (state: RootState): { positiveStylePrompt: string; negativeStylePrompt: string } => {
|
export const getPresetModifiedPrompts = (
|
||||||
|
state: RootState
|
||||||
|
): { positivePrompt: string; negativePrompt: string; positiveStylePrompt?: string; negativeStylePrompt?: string } => {
|
||||||
const { positivePrompt, negativePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } =
|
const { positivePrompt, negativePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } =
|
||||||
state.controlLayers.present;
|
state.controlLayers.present;
|
||||||
|
const { activeStylePresetId } = state.stylePreset;
|
||||||
|
|
||||||
|
if (activeStylePresetId) {
|
||||||
|
const { data } = stylePresetsApi.endpoints.listStylePresets.select()(state);
|
||||||
|
|
||||||
|
const activeStylePreset = data?.find((item) => item.id === activeStylePresetId);
|
||||||
|
|
||||||
|
if (activeStylePreset) {
|
||||||
|
const presetModifiedPositivePrompt = buildPresetModifiedPrompt(
|
||||||
|
activeStylePreset.preset_data.positive_prompt,
|
||||||
|
positivePrompt
|
||||||
|
);
|
||||||
|
|
||||||
|
const presetModifiedNegativePrompt = buildPresetModifiedPrompt(
|
||||||
|
activeStylePreset.preset_data.negative_prompt,
|
||||||
|
negativePrompt
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
positivePrompt: presetModifiedPositivePrompt,
|
||||||
|
negativePrompt: presetModifiedNegativePrompt,
|
||||||
|
positiveStylePrompt: shouldConcatPrompts ? presetModifiedPositivePrompt : positivePrompt2,
|
||||||
|
negativeStylePrompt: shouldConcatPrompts ? presetModifiedNegativePrompt : negativePrompt2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
positivePrompt,
|
||||||
|
negativePrompt,
|
||||||
positiveStylePrompt: shouldConcatPrompts ? positivePrompt : positivePrompt2,
|
positiveStylePrompt: shouldConcatPrompts ? positivePrompt : positivePrompt2,
|
||||||
negativeStylePrompt: shouldConcatPrompts ? negativePrompt : negativePrompt2,
|
negativeStylePrompt: shouldConcatPrompts ? negativePrompt : negativePrompt2,
|
||||||
};
|
};
|
||||||
|
@ -1,16 +1,32 @@
|
|||||||
import { Box, Textarea } from '@invoke-ai/ui-library';
|
import { Box, Textarea } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { negativePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
|
import { negativePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
|
||||||
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
||||||
|
import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt';
|
||||||
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
||||||
import { PromptPopover } from 'features/prompt/PromptPopover';
|
import { PromptPopover } from 'features/prompt/PromptPopover';
|
||||||
import { usePrompt } from 'features/prompt/usePrompt';
|
import { usePrompt } from 'features/prompt/usePrompt';
|
||||||
import { memo, useCallback, useRef } from 'react';
|
import { memo, useCallback, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
export const ParamNegativePrompt = memo(() => {
|
export const ParamNegativePrompt = memo(() => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const prompt = useAppSelector((s) => s.controlLayers.present.negativePrompt);
|
const prompt = useAppSelector((s) => s.controlLayers.present.negativePrompt);
|
||||||
|
const viewMode = useAppSelector((s) => s.stylePreset.viewMode);
|
||||||
|
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);
|
||||||
|
|
||||||
|
const { activeStylePreset } = useListStylePresetsQuery(undefined, {
|
||||||
|
selectFromResult: ({ data }) => {
|
||||||
|
let activeStylePreset = null;
|
||||||
|
if (data) {
|
||||||
|
activeStylePreset = data.find((sp) => sp.id === activeStylePresetId);
|
||||||
|
}
|
||||||
|
return { activeStylePreset };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const _onChange = useCallback(
|
const _onChange = useCallback(
|
||||||
@ -27,22 +43,34 @@ export const ParamNegativePrompt = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PromptPopover isOpen={isOpen} onClose={onClose} onSelect={onSelect} width={textareaRef.current?.clientWidth}>
|
<PromptPopover isOpen={isOpen} onClose={onClose} onSelect={onSelect} width={textareaRef.current?.clientWidth}>
|
||||||
<Box pos="relative">
|
<Box pos="relative" w="full">
|
||||||
<Textarea
|
<Textarea
|
||||||
id="negativePrompt"
|
id="negativePrompt"
|
||||||
name="negativePrompt"
|
name="negativePrompt"
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={prompt}
|
value={prompt}
|
||||||
placeholder={t('parameters.globalNegativePromptPlaceholder')}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
variant="darkFilled"
|
variant="darkFilled"
|
||||||
paddingRight={30}
|
minH={28}
|
||||||
|
borderTopWidth={24} // This prevents the prompt from being hidden behind the header
|
||||||
|
paddingInlineEnd={10}
|
||||||
|
paddingInlineStart={3}
|
||||||
|
paddingTop={0}
|
||||||
|
paddingBottom={3}
|
||||||
/>
|
/>
|
||||||
<PromptOverlayButtonWrapper>
|
<PromptOverlayButtonWrapper>
|
||||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||||
</PromptOverlayButtonWrapper>
|
</PromptOverlayButtonWrapper>
|
||||||
|
<PromptLabel label={t('parameters.negativePromptPlaceholder')} />
|
||||||
|
{viewMode && (
|
||||||
|
<ViewModePrompt
|
||||||
|
prompt={prompt}
|
||||||
|
presetPrompt={activeStylePreset?.preset_data.negative_prompt || ''}
|
||||||
|
label={`${t('parameters.negativePromptPlaceholder')} (${t('stylePresets.preview')})`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</PromptPopover>
|
</PromptPopover>
|
||||||
);
|
);
|
||||||
|
@ -2,7 +2,9 @@ import { Box, Textarea } from '@invoke-ai/ui-library';
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
|
import { positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton';
|
import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton';
|
||||||
|
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
|
||||||
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
||||||
|
import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt';
|
||||||
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
||||||
import { PromptPopover } from 'features/prompt/PromptPopover';
|
import { PromptPopover } from 'features/prompt/PromptPopover';
|
||||||
import { usePrompt } from 'features/prompt/usePrompt';
|
import { usePrompt } from 'features/prompt/usePrompt';
|
||||||
@ -11,11 +13,24 @@ import { memo, useCallback, useRef } from 'react';
|
|||||||
import type { HotkeyCallback } from 'react-hotkeys-hook';
|
import type { HotkeyCallback } from 'react-hotkeys-hook';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
export const ParamPositivePrompt = memo(() => {
|
export const ParamPositivePrompt = memo(() => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const prompt = useAppSelector((s) => s.controlLayers.present.positivePrompt);
|
const prompt = useAppSelector((s) => s.controlLayers.present.positivePrompt);
|
||||||
const baseModel = useAppSelector((s) => s.generation.model)?.base;
|
const baseModel = useAppSelector((s) => s.generation.model)?.base;
|
||||||
|
const viewMode = useAppSelector((s) => s.stylePreset.viewMode);
|
||||||
|
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);
|
||||||
|
|
||||||
|
const { activeStylePreset } = useListStylePresetsQuery(undefined, {
|
||||||
|
selectFromResult: ({ data }) => {
|
||||||
|
let activeStylePreset = null;
|
||||||
|
if (data) {
|
||||||
|
activeStylePreset = data.find((sp) => sp.id === activeStylePresetId);
|
||||||
|
}
|
||||||
|
return { activeStylePreset };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -49,18 +64,29 @@ export const ParamPositivePrompt = memo(() => {
|
|||||||
name="prompt"
|
name="prompt"
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={prompt}
|
value={prompt}
|
||||||
placeholder={t('parameters.globalPositivePromptPlaceholder')}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
minH={28}
|
minH={40}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
variant="darkFilled"
|
variant="darkFilled"
|
||||||
paddingRight={30}
|
borderTopWidth={24} // This prevents the prompt from being hidden behind the header
|
||||||
|
paddingInlineEnd={10}
|
||||||
|
paddingInlineStart={3}
|
||||||
|
paddingTop={0}
|
||||||
|
paddingBottom={3}
|
||||||
/>
|
/>
|
||||||
<PromptOverlayButtonWrapper>
|
<PromptOverlayButtonWrapper>
|
||||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||||
{baseModel === 'sdxl' && <SDXLConcatButton />}
|
{baseModel === 'sdxl' && <SDXLConcatButton />}
|
||||||
<ShowDynamicPromptsPreviewButton />
|
<ShowDynamicPromptsPreviewButton />
|
||||||
</PromptOverlayButtonWrapper>
|
</PromptOverlayButtonWrapper>
|
||||||
|
<PromptLabel label={t('parameters.positivePromptPlaceholder')} />
|
||||||
|
{viewMode && (
|
||||||
|
<ViewModePrompt
|
||||||
|
prompt={prompt}
|
||||||
|
presetPrompt={activeStylePreset?.preset_data.positive_prompt || ''}
|
||||||
|
label={`${t('parameters.positivePromptPlaceholder')} (${t('stylePresets.preview')})`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</PromptPopover>
|
</PromptPopover>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import { Text } from '@invoke-ai/ui-library';
|
||||||
|
|
||||||
|
export const PromptLabel = ({ label }: { label: string }) => {
|
||||||
|
return (
|
||||||
|
<Text variant="subtext" fontWeight="semibold" pos="absolute" top={1} left={2}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
};
|
@ -1,13 +1,29 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt';
|
import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt';
|
||||||
import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt';
|
import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt';
|
||||||
|
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
|
||||||
|
import { ParamSDXLNegativeStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt';
|
||||||
|
import { ParamSDXLPositiveStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
const concatPromptsSelector = createSelector(
|
||||||
|
[selectGenerationSlice, selectControlLayersSlice],
|
||||||
|
(generation, controlLayers) => {
|
||||||
|
return generation.model?.base !== 'sdxl' || controlLayers.present.shouldConcatPrompts;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const Prompts = memo(() => {
|
export const Prompts = memo(() => {
|
||||||
|
const shouldConcatPrompts = useAppSelector(concatPromptsSelector);
|
||||||
return (
|
return (
|
||||||
<Flex flexDir="column" gap={2}>
|
<Flex flexDir="column" gap={2}>
|
||||||
<ParamPositivePrompt />
|
<ParamPositivePrompt />
|
||||||
|
{!shouldConcatPrompts && <ParamSDXLPositiveStylePrompt />}
|
||||||
<ParamNegativePrompt />
|
<ParamNegativePrompt />
|
||||||
|
{!shouldConcatPrompts && <ParamSDXLNegativeStylePrompt />}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
import { Box, Flex, Icon, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { viewModeChanged } from 'features/stylePresets/store/stylePresetSlice';
|
||||||
|
import { getViewModeChunks } from 'features/stylePresets/util/getViewModeChunks';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiEyeBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
import { PromptLabel } from './PromptLabel';
|
||||||
|
|
||||||
|
export const ViewModePrompt = ({
|
||||||
|
presetPrompt,
|
||||||
|
prompt,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
presetPrompt: string;
|
||||||
|
prompt: string;
|
||||||
|
label: string;
|
||||||
|
}) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const presetChunks = useMemo(() => {
|
||||||
|
return getViewModeChunks(prompt, presetPrompt);
|
||||||
|
}, [presetPrompt, prompt]);
|
||||||
|
|
||||||
|
const handleExitViewMode = useCallback(() => {
|
||||||
|
dispatch(viewModeChanged(false));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box position="absolute" top={0} bottom={0} left={0} right={0} layerStyle="second" borderRadius="base">
|
||||||
|
<Flex
|
||||||
|
flexDir="column"
|
||||||
|
onClick={handleExitViewMode}
|
||||||
|
justifyContent="space-between"
|
||||||
|
h="full"
|
||||||
|
borderWidth={1}
|
||||||
|
borderTopWidth={24} // This prevents the prompt from being hidden behind the header
|
||||||
|
borderColor="transparent"
|
||||||
|
paddingInlineEnd={10}
|
||||||
|
paddingInlineStart={3}
|
||||||
|
paddingTop={0}
|
||||||
|
paddingBottom={3}
|
||||||
|
>
|
||||||
|
<PromptLabel label={label} />
|
||||||
|
<Flex overflow="scroll">
|
||||||
|
<Text w="full" lineHeight="short">
|
||||||
|
{presetChunks.map((chunk, index) => (
|
||||||
|
<Text
|
||||||
|
as="span"
|
||||||
|
color={index === 1 ? 'white' : 'base.200'}
|
||||||
|
fontWeight={index === 1 ? 'semibold' : 'normal'}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
{chunk}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Tooltip label={t('stylePresets.viewModeTooltip')}>
|
||||||
|
<Flex
|
||||||
|
position="absolute"
|
||||||
|
insetInlineEnd={0}
|
||||||
|
insetBlockStart={0}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
p={2}
|
||||||
|
bg="base.900"
|
||||||
|
opacity={0.8}
|
||||||
|
backgroundClip="border-box"
|
||||||
|
borderBottomStartRadius="base"
|
||||||
|
>
|
||||||
|
<Icon as={PiEyeBold} color="base.500" boxSize={4} />
|
||||||
|
</Flex>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,7 @@
|
|||||||
import { Box, Textarea } from '@invoke-ai/ui-library';
|
import { Box, Textarea } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { negativePrompt2Changed } from 'features/controlLayers/store/controlLayersSlice';
|
import { negativePrompt2Changed } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
|
||||||
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
||||||
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
||||||
import { PromptPopover } from 'features/prompt/PromptPopover';
|
import { PromptPopover } from 'features/prompt/PromptPopover';
|
||||||
@ -36,16 +37,21 @@ export const ParamSDXLNegativeStylePrompt = memo(() => {
|
|||||||
name="prompt"
|
name="prompt"
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={prompt}
|
value={prompt}
|
||||||
placeholder={t('sdxl.negStylePrompt')}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
variant="darkFilled"
|
variant="darkFilled"
|
||||||
paddingRight={30}
|
minH={24}
|
||||||
|
borderTopWidth={24} // This prevents the prompt from being hidden behind the header
|
||||||
|
paddingInlineEnd={10}
|
||||||
|
paddingInlineStart={3}
|
||||||
|
paddingTop={0}
|
||||||
|
paddingBottom={3}
|
||||||
/>
|
/>
|
||||||
<PromptOverlayButtonWrapper>
|
<PromptOverlayButtonWrapper>
|
||||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||||
</PromptOverlayButtonWrapper>
|
</PromptOverlayButtonWrapper>
|
||||||
|
<PromptLabel label={t('sdxl.negStylePrompt')} />
|
||||||
</Box>
|
</Box>
|
||||||
</PromptPopover>
|
</PromptPopover>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Box, Textarea } from '@invoke-ai/ui-library';
|
import { Box, Textarea } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { positivePrompt2Changed } from 'features/controlLayers/store/controlLayersSlice';
|
import { positivePrompt2Changed } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
|
||||||
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
||||||
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
||||||
import { PromptPopover } from 'features/prompt/PromptPopover';
|
import { PromptPopover } from 'features/prompt/PromptPopover';
|
||||||
@ -33,16 +34,21 @@ export const ParamSDXLPositiveStylePrompt = memo(() => {
|
|||||||
name="prompt"
|
name="prompt"
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={prompt}
|
value={prompt}
|
||||||
placeholder={t('sdxl.posStylePrompt')}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
variant="darkFilled"
|
variant="darkFilled"
|
||||||
paddingRight={30}
|
minH={24}
|
||||||
|
borderTopWidth={24} // This prevents the prompt from being hidden behind the header
|
||||||
|
paddingInlineEnd={10}
|
||||||
|
paddingInlineStart={3}
|
||||||
|
paddingTop={0}
|
||||||
|
paddingBottom={3}
|
||||||
/>
|
/>
|
||||||
<PromptOverlayButtonWrapper>
|
<PromptOverlayButtonWrapper>
|
||||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||||
</PromptOverlayButtonWrapper>
|
</PromptOverlayButtonWrapper>
|
||||||
|
<PromptLabel label={t('sdxl.posStylePrompt')} />
|
||||||
</Box>
|
</Box>
|
||||||
</PromptPopover>
|
</PromptPopover>
|
||||||
);
|
);
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react';
|
|
||||||
|
|
||||||
import { SDXLPrompts } from './SDXLPrompts';
|
|
||||||
|
|
||||||
const meta: Meta<typeof SDXLPrompts> = {
|
|
||||||
title: 'Feature/Prompt/SDXLPrompts',
|
|
||||||
tags: ['autodocs'],
|
|
||||||
component: SDXLPrompts,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof SDXLPrompts>;
|
|
||||||
|
|
||||||
const Component = () => {
|
|
||||||
return <SDXLPrompts />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Default: Story = {
|
|
||||||
render: Component,
|
|
||||||
};
|
|
@ -1,22 +0,0 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt';
|
|
||||||
import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt';
|
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
import { ParamSDXLNegativeStylePrompt } from './ParamSDXLNegativeStylePrompt';
|
|
||||||
import { ParamSDXLPositiveStylePrompt } from './ParamSDXLPositiveStylePrompt';
|
|
||||||
|
|
||||||
export const SDXLPrompts = memo(() => {
|
|
||||||
const shouldConcatPrompts = useAppSelector((s) => s.controlLayers.present.shouldConcatPrompts);
|
|
||||||
return (
|
|
||||||
<Flex flexDir="column" gap={2} pos="relative">
|
|
||||||
<ParamPositivePrompt />
|
|
||||||
{!shouldConcatPrompts && <ParamSDXLPositiveStylePrompt />}
|
|
||||||
<ParamNegativePrompt />
|
|
||||||
{!shouldConcatPrompts && <ParamSDXLNegativeStylePrompt />}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
SDXLPrompts.displayName = 'SDXLPrompts';
|
|
@ -0,0 +1,113 @@
|
|||||||
|
import { Badge, Flex, IconButton, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { negativePromptChanged, positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { usePresetModifiedPrompts } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
|
||||||
|
import { activeStylePresetIdChanged, viewModeChanged } from 'features/stylePresets/store/stylePresetSlice';
|
||||||
|
import type { MouseEventHandler } from 'react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiEyeBold, PiStackSimpleBold, PiXBold } from 'react-icons/pi';
|
||||||
|
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
|
import StylePresetImage from './StylePresetImage';
|
||||||
|
|
||||||
|
export const ActiveStylePreset = () => {
|
||||||
|
const viewMode = useAppSelector((s) => s.stylePreset.viewMode);
|
||||||
|
|
||||||
|
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);
|
||||||
|
|
||||||
|
const { activeStylePreset } = useListStylePresetsQuery(undefined, {
|
||||||
|
selectFromResult: ({ data }) => {
|
||||||
|
let activeStylePreset = null;
|
||||||
|
if (data) {
|
||||||
|
activeStylePreset = data.find((sp) => sp.id === activeStylePresetId);
|
||||||
|
}
|
||||||
|
return { activeStylePreset };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { presetModifiedPositivePrompt, presetModifiedNegativePrompt } = usePresetModifiedPrompts();
|
||||||
|
|
||||||
|
const handleClearActiveStylePreset = useCallback<MouseEventHandler<HTMLButtonElement>>(
|
||||||
|
(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dispatch(viewModeChanged(false));
|
||||||
|
dispatch(activeStylePresetIdChanged(null));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFlattenPrompts = useCallback<MouseEventHandler<HTMLButtonElement>>(
|
||||||
|
(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dispatch(positivePromptChanged(presetModifiedPositivePrompt));
|
||||||
|
dispatch(negativePromptChanged(presetModifiedNegativePrompt));
|
||||||
|
dispatch(viewModeChanged(false));
|
||||||
|
dispatch(activeStylePresetIdChanged(null));
|
||||||
|
},
|
||||||
|
[dispatch, presetModifiedPositivePrompt, presetModifiedNegativePrompt]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggleViewMode = useCallback<MouseEventHandler<HTMLButtonElement>>(
|
||||||
|
(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dispatch(viewModeChanged(!viewMode));
|
||||||
|
},
|
||||||
|
[dispatch, viewMode]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!activeStylePreset) {
|
||||||
|
return (
|
||||||
|
<Flex h={25} alignItems="center">
|
||||||
|
<Text fontSize="sm" fontWeight="semibold" color="base.300">
|
||||||
|
{t('stylePresets.choosePromptTemplate')}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Flex justifyContent="space-between" w="full" alignItems="center">
|
||||||
|
<Flex gap={2} alignItems="center">
|
||||||
|
<StylePresetImage imageWidth={25} presetImageUrl={activeStylePreset.image} />
|
||||||
|
<Flex flexDir="column">
|
||||||
|
<Badge colorScheme="invokeBlue" variant="subtle">
|
||||||
|
{activeStylePreset.name}
|
||||||
|
</Badge>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex gap={1}>
|
||||||
|
<Tooltip label={t('stylePresets.toggleViewMode')}>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleToggleViewMode}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
aria-label={t('stylePresets.toggleViewMode')}
|
||||||
|
colorScheme={viewMode ? 'invokeBlue' : 'base'}
|
||||||
|
icon={<PiEyeBold />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label={t('stylePresets.flatten')}>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleFlattenPrompts}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
aria-label={t('stylePresets.flatten')}
|
||||||
|
icon={<PiStackSimpleBold />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label={t('stylePresets.clearTemplateSelection')}>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleClearActiveStylePreset}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
aria-label={t('stylePresets.clearTemplateSelection')}
|
||||||
|
icon={<PiXBold />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,30 @@
|
|||||||
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
|
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
export const StylePresetCreateButton = () => {
|
||||||
|
const handleClickAddNew = useCallback(() => {
|
||||||
|
$stylePresetModalState.set({
|
||||||
|
prefilledFormData: null,
|
||||||
|
updatingStylePresetId: null,
|
||||||
|
isModalOpen: true,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
icon={<PiPlusBold />}
|
||||||
|
tooltip={t('stylePresets.createPromptTemplate')}
|
||||||
|
aria-label={t('stylePresets.createPromptTemplate')}
|
||||||
|
onClick={handleClickAddNew}
|
||||||
|
size="md"
|
||||||
|
variant="ghost"
|
||||||
|
w={8}
|
||||||
|
h={8}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,68 @@
|
|||||||
|
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
|
import { IconButton, spinAnimation } from '@invoke-ai/ui-library';
|
||||||
|
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||||
|
import { toast } from 'features/toast/toast';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiDownloadSimpleBold, PiSpinner } from 'react-icons/pi';
|
||||||
|
import { useLazyExportStylePresetsQuery, useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
|
const loadingStyles: SystemStyleObject = {
|
||||||
|
svg: { animation: spinAnimation },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StylePresetExportButton = () => {
|
||||||
|
const [exportStylePresets, { isLoading }] = useLazyExportStylePresetsQuery();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { presetCount } = useListStylePresetsQuery(undefined, {
|
||||||
|
selectFromResult: ({ data }) => {
|
||||||
|
const userPresets = data?.filter((preset) => preset.type === 'user') ?? EMPTY_ARRAY;
|
||||||
|
return {
|
||||||
|
presetCount: userPresets.length,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const handleClickDownloadCsv = useCallback(async () => {
|
||||||
|
let blob;
|
||||||
|
try {
|
||||||
|
const response = await exportStylePresets().unwrap();
|
||||||
|
blob = new Blob([response], { type: 'text/csv' });
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
status: 'error',
|
||||||
|
title: t('stylePresets.exportFailed'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blob) {
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'data.csv';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
toast({
|
||||||
|
status: 'success',
|
||||||
|
title: t('stylePresets.exportDownloaded'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [exportStylePresets, t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
onClick={handleClickDownloadCsv}
|
||||||
|
icon={!isLoading ? <PiDownloadSimpleBold /> : <PiSpinner />}
|
||||||
|
tooltip={t('stylePresets.exportPromptTemplates')}
|
||||||
|
aria-label={t('stylePresets.exportPromptTemplates')}
|
||||||
|
size="md"
|
||||||
|
variant="link"
|
||||||
|
w={8}
|
||||||
|
h={8}
|
||||||
|
sx={isLoading ? loadingStyles : undefined}
|
||||||
|
isDisabled={isLoading || presetCount === 0}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,124 @@
|
|||||||
|
import { Box, Button, Flex, FormControl, FormLabel, Input, Spacer, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { PRESET_PLACEHOLDER } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
|
||||||
|
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
|
||||||
|
import { toast } from 'features/toast/toast';
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import type { SubmitHandler } from 'react-hook-form';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
import type { PresetType } from 'services/api/endpoints/stylePresets';
|
||||||
|
import { useCreateStylePresetMutation, useUpdateStylePresetMutation } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
|
import { StylePresetImageField } from './StylePresetImageField';
|
||||||
|
import { StylePresetPromptField } from './StylePresetPromptField';
|
||||||
|
import { StylePresetTypeField } from './StylePresetTypeField';
|
||||||
|
|
||||||
|
export type StylePresetFormData = {
|
||||||
|
name: string;
|
||||||
|
positivePrompt: string;
|
||||||
|
negativePrompt: string;
|
||||||
|
image: File | null;
|
||||||
|
type: PresetType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StylePresetForm = ({
|
||||||
|
updatingStylePresetId,
|
||||||
|
formData,
|
||||||
|
}: {
|
||||||
|
updatingStylePresetId: string | null;
|
||||||
|
formData: StylePresetFormData | null;
|
||||||
|
}) => {
|
||||||
|
const [createStylePreset] = useCreateStylePresetMutation();
|
||||||
|
const [updateStylePreset] = useUpdateStylePresetMutation();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const allowPrivateStylePresets = useAppSelector((s) => s.config.allowPrivateStylePresets);
|
||||||
|
|
||||||
|
const { handleSubmit, control, register, formState } = useForm<StylePresetFormData>({
|
||||||
|
defaultValues: formData || {
|
||||||
|
name: '',
|
||||||
|
positivePrompt: '',
|
||||||
|
negativePrompt: '',
|
||||||
|
image: null,
|
||||||
|
type: 'user',
|
||||||
|
},
|
||||||
|
mode: 'onChange',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClickSave = useCallback<SubmitHandler<StylePresetFormData>>(
|
||||||
|
async (data) => {
|
||||||
|
const payload = {
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
positive_prompt: data.positivePrompt,
|
||||||
|
negative_prompt: data.negativePrompt,
|
||||||
|
type: data.type,
|
||||||
|
},
|
||||||
|
image: data.image,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (updatingStylePresetId) {
|
||||||
|
await updateStylePreset({
|
||||||
|
id: updatingStylePresetId,
|
||||||
|
...payload,
|
||||||
|
}).unwrap();
|
||||||
|
} else {
|
||||||
|
await createStylePreset(payload).unwrap();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
status: 'error',
|
||||||
|
title: 'Failed to save style preset',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$stylePresetModalState.set({
|
||||||
|
prefilledFormData: null,
|
||||||
|
updatingStylePresetId: null,
|
||||||
|
isModalOpen: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[updatingStylePresetId, updateStylePreset, createStylePreset]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column" gap={4}>
|
||||||
|
<Flex alignItems="center" gap={4}>
|
||||||
|
<StylePresetImageField control={control} name="image" />
|
||||||
|
<FormControl orientation="vertical">
|
||||||
|
<FormLabel>{t('stylePresets.name')}</FormLabel>
|
||||||
|
<Input size="md" {...register('name', { required: true, minLength: 1 })} />
|
||||||
|
</FormControl>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<StylePresetPromptField label={t('stylePresets.positivePrompt')} control={control} name="positivePrompt" />
|
||||||
|
<StylePresetPromptField label={t('stylePresets.negativePrompt')} control={control} name="negativePrompt" />
|
||||||
|
<Box>
|
||||||
|
<Text variant="subtext">{t('stylePresets.promptTemplatesDesc1')}</Text>
|
||||||
|
<Text variant="subtext">
|
||||||
|
<Trans
|
||||||
|
i18nKey="stylePresets.promptTemplatesDesc2"
|
||||||
|
components={{ Pre: <Pre /> }}
|
||||||
|
values={{ placeholder: PRESET_PLACEHOLDER }}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
<Text variant="subtext">{t('stylePresets.promptTemplatesDesc3')}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Flex justifyContent="space-between" alignItems="flex-end" gap={10}>
|
||||||
|
{allowPrivateStylePresets ? <StylePresetTypeField control={control} name="type" /> : <Spacer />}
|
||||||
|
<Button onClick={handleSubmit(handleClickSave)} isDisabled={!formState.isValid}>
|
||||||
|
{t('common.save')}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Pre = (props: PropsWithChildren) => (
|
||||||
|
<Text as="span" fontFamily="monospace" fontWeight="semibold">
|
||||||
|
{props.children}
|
||||||
|
</Text>
|
||||||
|
);
|
@ -0,0 +1,82 @@
|
|||||||
|
import { Box, Button, Flex, Icon, IconButton, Image, Tooltip } from '@invoke-ai/ui-library';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useDropzone } from 'react-dropzone';
|
||||||
|
import type { UseControllerProps } from 'react-hook-form';
|
||||||
|
import { useController } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiArrowCounterClockwiseBold, PiUploadSimpleBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
import type { StylePresetFormData } from './StylePresetForm';
|
||||||
|
|
||||||
|
export const StylePresetImageField = (props: UseControllerProps<StylePresetFormData, 'image'>) => {
|
||||||
|
const { field } = useController(props);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const onDropAccepted = useCallback(
|
||||||
|
(files: File[]) => {
|
||||||
|
const file = files[0];
|
||||||
|
if (file) {
|
||||||
|
field.onChange(file);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[field]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleResetImage = useCallback(() => {
|
||||||
|
field.onChange(null);
|
||||||
|
}, [field]);
|
||||||
|
|
||||||
|
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)}
|
||||||
|
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('stylePresets.deleteImage')}
|
||||||
|
tooltip={t('stylePresets.deleteImage')}
|
||||||
|
icon={<PiArrowCounterClockwiseBold />}
|
||||||
|
size="md"
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip label={t('stylePresets.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()} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,82 @@
|
|||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
Spinner,
|
||||||
|
} from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { convertImageUrlToBlob } from 'common/util/convertImageUrlToBlob';
|
||||||
|
import type { PrefilledFormData } from 'features/stylePresets/store/stylePresetModal';
|
||||||
|
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import type { StylePresetFormData } from './StylePresetForm';
|
||||||
|
import { StylePresetForm } from './StylePresetForm';
|
||||||
|
|
||||||
|
export const StylePresetModal = () => {
|
||||||
|
const [formData, setFormData] = useState<StylePresetFormData | null>(null);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const stylePresetModalState = useStore($stylePresetModalState);
|
||||||
|
|
||||||
|
const modalTitle = useMemo(() => {
|
||||||
|
return stylePresetModalState.updatingStylePresetId
|
||||||
|
? t('stylePresets.updatePromptTemplate')
|
||||||
|
: t('stylePresets.createPromptTemplate');
|
||||||
|
}, [stylePresetModalState.updatingStylePresetId, t]);
|
||||||
|
|
||||||
|
const handleCloseModal = useCallback(() => {
|
||||||
|
$stylePresetModalState.set({
|
||||||
|
prefilledFormData: null,
|
||||||
|
updatingStylePresetId: null,
|
||||||
|
isModalOpen: false,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFormData(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const convertImageToBlob = async (data: PrefilledFormData | null) => {
|
||||||
|
if (!data) {
|
||||||
|
setFormData(null);
|
||||||
|
} else {
|
||||||
|
let file = null;
|
||||||
|
if (data.imageUrl) {
|
||||||
|
const blob = await convertImageUrlToBlob(data.imageUrl);
|
||||||
|
if (blob) {
|
||||||
|
file = new File([blob], 'style_preset.png', { type: 'image/png' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setFormData({
|
||||||
|
...data,
|
||||||
|
image: file,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
convertImageToBlob(stylePresetModalState.prefilledFormData);
|
||||||
|
}, [stylePresetModalState.prefilledFormData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={stylePresetModalState.isModalOpen} onClose={handleCloseModal} isCentered size="2xl">
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>{modalTitle}</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody display="flex" flexDir="column" gap={4}>
|
||||||
|
{!stylePresetModalState.prefilledFormData || formData ? (
|
||||||
|
<StylePresetForm updatingStylePresetId={stylePresetModalState.updatingStylePresetId} formData={formData} />
|
||||||
|
) : (
|
||||||
|
<Spinner />
|
||||||
|
)}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter p={2} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,65 @@
|
|||||||
|
import { Button, Flex, FormControl, FormLabel, Textarea } from '@invoke-ai/ui-library';
|
||||||
|
import { PRESET_PLACEHOLDER } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
|
||||||
|
import type { ChangeEventHandler } from 'react';
|
||||||
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
|
import type { UseControllerProps } from 'react-hook-form';
|
||||||
|
import { useController } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import type { StylePresetFormData } from './StylePresetForm';
|
||||||
|
|
||||||
|
interface Props extends UseControllerProps<StylePresetFormData, 'negativePrompt' | 'positivePrompt'> {
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StylePresetPromptField = (props: Props) => {
|
||||||
|
const { field } = useController(props);
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const onChange = useCallback<ChangeEventHandler<HTMLTextAreaElement>>(
|
||||||
|
(v) => {
|
||||||
|
field.onChange(v.target.value);
|
||||||
|
},
|
||||||
|
[field]
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = useMemo(() => {
|
||||||
|
return field.value;
|
||||||
|
}, [field.value]);
|
||||||
|
|
||||||
|
const insertPromptPlaceholder = useCallback(() => {
|
||||||
|
if (textareaRef.current) {
|
||||||
|
const cursorPos = textareaRef.current.selectionStart;
|
||||||
|
const textBeforeCursor = value.slice(0, cursorPos);
|
||||||
|
const textAfterCursor = value.slice(cursorPos);
|
||||||
|
const newValue = textBeforeCursor + PRESET_PLACEHOLDER + textAfterCursor;
|
||||||
|
|
||||||
|
field.onChange(newValue);
|
||||||
|
} else {
|
||||||
|
field.onChange(value + PRESET_PLACEHOLDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
textareaRef.current?.focus();
|
||||||
|
}, [value, field, textareaRef]);
|
||||||
|
|
||||||
|
const isPromptPresent = useMemo(() => value?.includes(PRESET_PLACEHOLDER), [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl orientation="vertical" gap={3}>
|
||||||
|
<Flex alignItems="center" gap={2}>
|
||||||
|
<FormLabel>{props.label}</FormLabel>
|
||||||
|
<Button
|
||||||
|
onClick={insertPromptPlaceholder}
|
||||||
|
size="xs"
|
||||||
|
aria-label={t('stylePresets.insertPlaceholder')}
|
||||||
|
isDisabled={isPromptPresent}
|
||||||
|
>
|
||||||
|
{t('stylePresets.insertPlaceholder')}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Textarea size="sm" ref={textareaRef} value={value} onChange={onChange} />
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,46 @@
|
|||||||
|
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
|
||||||
|
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import type { UseControllerProps } from 'react-hook-form';
|
||||||
|
import { useController } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import type { StylePresetFormData } from './StylePresetForm';
|
||||||
|
|
||||||
|
const OPTIONS = [
|
||||||
|
{ label: t('stylePresets.private'), value: 'user' },
|
||||||
|
{ label: t('stylePresets.shared'), value: 'project' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const StylePresetTypeField = (props: UseControllerProps<StylePresetFormData, 'type'>) => {
|
||||||
|
const { field } = useController(props);
|
||||||
|
const stylePresetModalState = useStore($stylePresetModalState);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const onChange = useCallback<ComboboxOnChange>(
|
||||||
|
(v) => {
|
||||||
|
if (v) {
|
||||||
|
field.onChange(v.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[field]
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = useMemo(() => {
|
||||||
|
return OPTIONS.find((opt) => opt.value === field.value);
|
||||||
|
}, [field.value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
orientation="vertical"
|
||||||
|
maxW={48}
|
||||||
|
isDisabled={stylePresetModalState.prefilledFormData?.type === 'project'}
|
||||||
|
>
|
||||||
|
<FormLabel>{t('stylePresets.type')}</FormLabel>
|
||||||
|
<Combobox value={value} options={OPTIONS} onChange={onChange} />
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,52 @@
|
|||||||
|
import { Flex, Icon, Image, Tooltip } 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, imageWidth }: { presetImageUrl: string | null; imageWidth?: number }) => {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
label={
|
||||||
|
presetImageUrl && (
|
||||||
|
<Image
|
||||||
|
src={presetImageUrl}
|
||||||
|
draggable={false}
|
||||||
|
objectFit="cover"
|
||||||
|
maxW={150}
|
||||||
|
aspectRatio="1/1"
|
||||||
|
borderRadius="base"
|
||||||
|
borderBottomRadius="lg"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={presetImageUrl || ''}
|
||||||
|
fallbackStrategy="beforeLoadOrError"
|
||||||
|
fallback={
|
||||||
|
<Flex
|
||||||
|
height={imageWidth || IMAGE_THUMBNAIL_SIZE}
|
||||||
|
minWidth={imageWidth || IMAGE_THUMBNAIL_SIZE}
|
||||||
|
bg="base.650"
|
||||||
|
borderRadius="base"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<Icon color="base.500" as={PiImage} boxSize={imageWidth ? imageWidth / 2 : FALLBACK_ICON_SIZE} />
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
objectFit="cover"
|
||||||
|
objectPosition="50% 50%"
|
||||||
|
height={imageWidth || IMAGE_THUMBNAIL_SIZE}
|
||||||
|
width={imageWidth || IMAGE_THUMBNAIL_SIZE}
|
||||||
|
minHeight={imageWidth || IMAGE_THUMBNAIL_SIZE}
|
||||||
|
minWidth={imageWidth || IMAGE_THUMBNAIL_SIZE}
|
||||||
|
borderRadius="base"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default typedMemo(StylePresetImage);
|
@ -0,0 +1,84 @@
|
|||||||
|
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
|
import { Flex, IconButton, ListItem, spinAnimation, Text, UnorderedList } 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, PiUploadSimpleBold } from 'react-icons/pi';
|
||||||
|
import { useImportStylePresetsMutation } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
|
const loadingStyles: SystemStyleObject = {
|
||||||
|
svg: { animation: spinAnimation },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StylePresetImportButton = () => {
|
||||||
|
const [importStylePresets, { isLoading }] = useImportStylePresetsMutation();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const onDropAccepted = useCallback(
|
||||||
|
(files: File[]) => {
|
||||||
|
const file = files[0];
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
importStylePresets(file)
|
||||||
|
.unwrap()
|
||||||
|
.then(() => {
|
||||||
|
toast({
|
||||||
|
status: 'success',
|
||||||
|
title: t('toast.importSuccessful'),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast({
|
||||||
|
status: 'error',
|
||||||
|
title: t('toast.importFailed'),
|
||||||
|
description: error ? `${error.data.detail}` : undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[importStylePresets, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getInputProps, getRootProps } = useDropzone({
|
||||||
|
accept: { 'text/csv': ['.csv'], 'application/json': ['.json'] },
|
||||||
|
onDropAccepted,
|
||||||
|
noDrag: true,
|
||||||
|
multiple: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
icon={!isLoading ? <PiUploadSimpleBold /> : <PiSpinner />}
|
||||||
|
tooltip={<TooltipContent />}
|
||||||
|
aria-label={t('stylePresets.importTemplates')}
|
||||||
|
size="md"
|
||||||
|
variant="link"
|
||||||
|
w={8}
|
||||||
|
h={8}
|
||||||
|
sx={isLoading ? loadingStyles : undefined}
|
||||||
|
isDisabled={isLoading}
|
||||||
|
{...getRootProps()}
|
||||||
|
/>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TooltipContent = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column">
|
||||||
|
<Text pb={1} fontWeight="semibold">
|
||||||
|
{t('stylePresets.importTemplates')}
|
||||||
|
</Text>
|
||||||
|
<Text>{t('stylePresets.acceptedColumnsKeys')}</Text>
|
||||||
|
<UnorderedList>
|
||||||
|
<ListItem>{t('stylePresets.nameColumn')}</ListItem>
|
||||||
|
<ListItem>{t('stylePresets.positivePromptColumn')}</ListItem>
|
||||||
|
<ListItem>{t('stylePresets.negativePromptColumn')}</ListItem>
|
||||||
|
</UnorderedList>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,39 @@
|
|||||||
|
import { Button, Collapse, Flex, Icon, Text, useDisclosure } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiCaretDownBold } from 'react-icons/pi';
|
||||||
|
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
|
import { StylePresetListItem } from './StylePresetListItem';
|
||||||
|
|
||||||
|
export const StylePresetList = ({ title, data }: { title: string; data: StylePresetRecordWithImage[] }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { onToggle, isOpen } = useDisclosure({ defaultIsOpen: true });
|
||||||
|
const searchTerm = useAppSelector((s) => s.stylePreset.searchTerm);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column">
|
||||||
|
<Button variant="unstyled" onClick={onToggle}>
|
||||||
|
<Flex gap={2} alignItems="center">
|
||||||
|
<Icon boxSize={4} as={PiCaretDownBold} transform={isOpen ? undefined : 'rotate(-90deg)'} fill="base.500" />
|
||||||
|
<Text fontSize="sm" fontWeight="semibold" userSelect="none" color="base.500">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Button>
|
||||||
|
<Collapse in={isOpen}>
|
||||||
|
{data.length ? (
|
||||||
|
data.map((preset) => <StylePresetListItem preset={preset} key={preset.id} />)
|
||||||
|
) : (
|
||||||
|
<IAINoContentFallback
|
||||||
|
fontSize="sm"
|
||||||
|
py={4}
|
||||||
|
label={searchTerm ? t('stylePresets.noMatchingTemplates') : t('stylePresets.noTemplates')}
|
||||||
|
icon={null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Collapse>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,184 @@
|
|||||||
|
import { Badge, ConfirmationAlertDialog, Flex, IconButton, Text, useDisclosure } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { $isMenuOpen } from 'features/stylePresets/store/isMenuOpen';
|
||||||
|
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
|
||||||
|
import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
|
||||||
|
import { toast } from 'features/toast/toast';
|
||||||
|
import type { MouseEvent } from 'react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiCopyBold, PiPencilBold, PiTrashBold } from 'react-icons/pi';
|
||||||
|
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
|
||||||
|
import { useDeleteStylePresetMutation } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
|
import StylePresetImage from './StylePresetImage';
|
||||||
|
|
||||||
|
export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithImage }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [deleteStylePreset] = useDeleteStylePresetMutation();
|
||||||
|
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleClickEdit = useCallback(
|
||||||
|
(e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const { name, preset_data } = preset;
|
||||||
|
const { positive_prompt, negative_prompt } = preset_data;
|
||||||
|
|
||||||
|
$stylePresetModalState.set({
|
||||||
|
prefilledFormData: {
|
||||||
|
name,
|
||||||
|
positivePrompt: positive_prompt || '',
|
||||||
|
negativePrompt: negative_prompt || '',
|
||||||
|
imageUrl: preset.image,
|
||||||
|
type: preset.type,
|
||||||
|
},
|
||||||
|
updatingStylePresetId: preset.id,
|
||||||
|
isModalOpen: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[preset]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClickApply = useCallback(async () => {
|
||||||
|
dispatch(activeStylePresetIdChanged(preset.id));
|
||||||
|
$isMenuOpen.set(false);
|
||||||
|
}, [dispatch, preset.id]);
|
||||||
|
|
||||||
|
const handleClickDelete = useCallback(
|
||||||
|
(e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onOpen();
|
||||||
|
},
|
||||||
|
[onOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClickCopy = useCallback(
|
||||||
|
(e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const { name, preset_data } = preset;
|
||||||
|
const { positive_prompt, negative_prompt } = preset_data;
|
||||||
|
|
||||||
|
$stylePresetModalState.set({
|
||||||
|
prefilledFormData: {
|
||||||
|
name: `${name} (${t('common.copy')})`,
|
||||||
|
positivePrompt: positive_prompt || '',
|
||||||
|
negativePrompt: negative_prompt || '',
|
||||||
|
imageUrl: preset.image,
|
||||||
|
type: 'user',
|
||||||
|
},
|
||||||
|
updatingStylePresetId: null,
|
||||||
|
isModalOpen: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[preset, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeletePreset = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await deleteStylePreset(preset.id);
|
||||||
|
toast({
|
||||||
|
status: 'success',
|
||||||
|
title: t('stylePresets.templateDeleted'),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
status: 'error',
|
||||||
|
title: t('stylePresets.unableToDeleteTemplate'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [preset, t, deleteStylePreset]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex
|
||||||
|
gap={4}
|
||||||
|
onClick={handleClickApply}
|
||||||
|
cursor="pointer"
|
||||||
|
_hover={{ backgroundColor: 'base.750' }}
|
||||||
|
py={3}
|
||||||
|
px={2}
|
||||||
|
borderRadius="base"
|
||||||
|
alignItems="flex-start"
|
||||||
|
w="full"
|
||||||
|
>
|
||||||
|
<StylePresetImage presetImageUrl={preset.image} />
|
||||||
|
<Flex flexDir="column" w="full">
|
||||||
|
<Flex w="full" justifyContent="space-between" alignItems="flex-start">
|
||||||
|
<Flex alignItems="center" gap={2}>
|
||||||
|
<Text fontSize="md" noOfLines={2}>
|
||||||
|
{preset.name}
|
||||||
|
</Text>
|
||||||
|
{activeStylePresetId === preset.id && (
|
||||||
|
<Badge
|
||||||
|
color="invokeBlue.400"
|
||||||
|
borderColor="invokeBlue.700"
|
||||||
|
borderWidth={1}
|
||||||
|
bg="transparent"
|
||||||
|
flexShrink={0}
|
||||||
|
>
|
||||||
|
{t('stylePresets.active')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex alignItems="center" gap={1}>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
aria-label={t('stylePresets.copyTemplate')}
|
||||||
|
onClick={handleClickCopy}
|
||||||
|
icon={<PiCopyBold />}
|
||||||
|
/>
|
||||||
|
{preset.type !== 'default' && (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
aria-label={t('stylePresets.editTemplate')}
|
||||||
|
onClick={handleClickEdit}
|
||||||
|
icon={<PiPencilBold />}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
aria-label={t('stylePresets.deleteTemplate')}
|
||||||
|
onClick={handleClickDelete}
|
||||||
|
colorScheme="error"
|
||||||
|
icon={<PiTrashBold />}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex flexDir="column" gap={1}>
|
||||||
|
<Text fontSize="xs">
|
||||||
|
<Text as="span" fontWeight="semibold">
|
||||||
|
{t('stylePresets.positivePrompt')}:
|
||||||
|
</Text>{' '}
|
||||||
|
{preset.preset_data.positive_prompt}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="xs">
|
||||||
|
<Text as="span" fontWeight="semibold">
|
||||||
|
{t('stylePresets.negativePrompt')}:
|
||||||
|
</Text>{' '}
|
||||||
|
{preset.preset_data.negative_prompt}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<ConfirmationAlertDialog
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
title={t('stylePresets.deleteTemplate')}
|
||||||
|
acceptCallback={handleDeletePreset}
|
||||||
|
acceptButtonText={t('common.delete')}
|
||||||
|
cancelButtonText={t('common.cancel')}
|
||||||
|
>
|
||||||
|
<p>{t('stylePresets.deleteTemplate2')}</p>
|
||||||
|
</ConfirmationAlertDialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,67 @@
|
|||||||
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { StylePresetExportButton } from 'features/stylePresets/components/StylePresetExportButton';
|
||||||
|
import { StylePresetImportButton } from 'features/stylePresets/components/StylePresetImportButton';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
|
||||||
|
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
|
import { StylePresetCreateButton } from './StylePresetCreateButton';
|
||||||
|
import { StylePresetList } from './StylePresetList';
|
||||||
|
import StylePresetSearch from './StylePresetSearch';
|
||||||
|
|
||||||
|
export const StylePresetMenu = () => {
|
||||||
|
const searchTerm = useAppSelector((s) => s.stylePreset.searchTerm);
|
||||||
|
const allowPrivateStylePresets = useAppSelector((s) => s.config.allowPrivateStylePresets);
|
||||||
|
const { data } = useListStylePresetsQuery(undefined, {
|
||||||
|
selectFromResult: ({ data }) => {
|
||||||
|
const filteredData =
|
||||||
|
data?.filter((preset) => preset.name.toLowerCase().includes(searchTerm.toLowerCase())) || EMPTY_ARRAY;
|
||||||
|
|
||||||
|
const groupedData = filteredData.reduce(
|
||||||
|
(
|
||||||
|
acc: {
|
||||||
|
defaultPresets: StylePresetRecordWithImage[];
|
||||||
|
sharedPresets: StylePresetRecordWithImage[];
|
||||||
|
presets: StylePresetRecordWithImage[];
|
||||||
|
},
|
||||||
|
preset
|
||||||
|
) => {
|
||||||
|
if (preset.type === 'default') {
|
||||||
|
acc.defaultPresets.push(preset);
|
||||||
|
} else if (preset.type === 'project') {
|
||||||
|
acc.sharedPresets.push(preset);
|
||||||
|
} else {
|
||||||
|
acc.presets.push(preset);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ defaultPresets: [], sharedPresets: [], presets: [] }
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: groupedData,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column" gap={2} padding={3} layerStyle="second" borderRadius="base">
|
||||||
|
<Flex alignItems="center" gap={2} w="full" justifyContent="space-between">
|
||||||
|
<StylePresetSearch />
|
||||||
|
<StylePresetCreateButton />
|
||||||
|
<StylePresetImportButton />
|
||||||
|
<StylePresetExportButton />
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<StylePresetList title={t('stylePresets.myTemplates')} data={data.presets} />
|
||||||
|
{allowPrivateStylePresets && (
|
||||||
|
<StylePresetList title={t('stylePresets.sharedTemplates')} data={data.sharedPresets} />
|
||||||
|
)}
|
||||||
|
<StylePresetList title={t('stylePresets.defaultTemplates')} data={data.defaultPresets} />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,43 @@
|
|||||||
|
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';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
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();
|
||||||
|
|
||||||
|
const handleToggle = useCallback(() => {
|
||||||
|
$isMenuOpen.set(!isMenuOpen);
|
||||||
|
}, [isMenuOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
onClick={handleToggle}
|
||||||
|
backgroundColor="base.800"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
py={2}
|
||||||
|
px={3}
|
||||||
|
borderRadius="base"
|
||||||
|
gap={1}
|
||||||
|
role="button"
|
||||||
|
_hover={_hover}
|
||||||
|
transitionProperty="background-color"
|
||||||
|
transitionDuration="normal"
|
||||||
|
>
|
||||||
|
<ActiveStylePreset />
|
||||||
|
|
||||||
|
<IconButton aria-label={t('stylePresets.viewList')} variant="ghost" icon={<PiCaretDownBold />} size="sm" />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,65 @@
|
|||||||
|
import { IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { searchTermChanged } from 'features/stylePresets/store/stylePresetSlice';
|
||||||
|
import type { ChangeEvent, KeyboardEvent } from 'react';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiXBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
const StylePresetSearch = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const searchTerm = useAppSelector((s) => s.stylePreset.searchTerm);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handlePresetSearch = useCallback(
|
||||||
|
(newSearchTerm: string) => {
|
||||||
|
dispatch(searchTermChanged(newSearchTerm));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearPresetSearch = useCallback(() => {
|
||||||
|
dispatch(searchTermChanged(''));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleKeydown = useCallback(
|
||||||
|
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
// exit search mode on escape
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
clearPresetSearch();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[clearPresetSearch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
handlePresetSearch(e.target.value);
|
||||||
|
},
|
||||||
|
[handlePresetSearch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputGroup>
|
||||||
|
<Input
|
||||||
|
placeholder={t('stylePresets.searchByName')}
|
||||||
|
value={searchTerm}
|
||||||
|
onKeyDown={handleKeydown}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
{searchTerm && searchTerm.length && (
|
||||||
|
<InputRightElement h="full" pe={2}>
|
||||||
|
<IconButton
|
||||||
|
onClick={clearPresetSearch}
|
||||||
|
size="sm"
|
||||||
|
variant="link"
|
||||||
|
aria-label={t('boards.clearSearch')}
|
||||||
|
icon={<PiXBold />}
|
||||||
|
/>
|
||||||
|
</InputRightElement>
|
||||||
|
)}
|
||||||
|
</InputGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(StylePresetSearch);
|
@ -0,0 +1,39 @@
|
|||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
|
export const PRESET_PLACEHOLDER = '{prompt}';
|
||||||
|
|
||||||
|
export const buildPresetModifiedPrompt = (presetPrompt: string, currentPrompt: string) => {
|
||||||
|
return presetPrompt.includes(PRESET_PLACEHOLDER)
|
||||||
|
? presetPrompt.replace(PRESET_PLACEHOLDER, currentPrompt)
|
||||||
|
: `${currentPrompt} ${presetPrompt}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePresetModifiedPrompts = () => {
|
||||||
|
const { positivePrompt, negativePrompt } = useAppSelector((s) => s.controlLayers.present);
|
||||||
|
|
||||||
|
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);
|
||||||
|
|
||||||
|
const { activeStylePreset } = useListStylePresetsQuery(undefined, {
|
||||||
|
selectFromResult: ({ data }) => {
|
||||||
|
let activeStylePreset = null;
|
||||||
|
if (data) {
|
||||||
|
activeStylePreset = data.find((sp) => sp.id === activeStylePresetId);
|
||||||
|
}
|
||||||
|
return { activeStylePreset };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!activeStylePreset) {
|
||||||
|
return { presetModifiedPositivePrompt: positivePrompt, presetModifiedNegativePrompt: negativePrompt };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { positive_prompt: presetPositivePrompt, negative_prompt: presetNegativePrompt } =
|
||||||
|
activeStylePreset.preset_data;
|
||||||
|
|
||||||
|
const presetModifiedPositivePrompt = buildPresetModifiedPrompt(presetPositivePrompt, positivePrompt);
|
||||||
|
|
||||||
|
const presetModifiedNegativePrompt = buildPresetModifiedPrompt(presetNegativePrompt, negativePrompt);
|
||||||
|
|
||||||
|
return { presetModifiedPositivePrompt, presetModifiedNegativePrompt };
|
||||||
|
};
|
@ -0,0 +1,6 @@
|
|||||||
|
import { atom } from 'nanostores';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks whether or not the style preset menu is open.
|
||||||
|
*/
|
||||||
|
export const $isMenuOpen = atom<boolean>(false);
|
@ -0,0 +1,27 @@
|
|||||||
|
import { atom } from 'nanostores';
|
||||||
|
import type { PresetType } from 'services/api/endpoints/stylePresets';
|
||||||
|
|
||||||
|
const initialState: StylePresetModalState = {
|
||||||
|
isModalOpen: false,
|
||||||
|
updatingStylePresetId: null,
|
||||||
|
prefilledFormData: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks the state for the style preset modal.
|
||||||
|
*/
|
||||||
|
export const $stylePresetModalState = atom<StylePresetModalState>(initialState);
|
||||||
|
|
||||||
|
type StylePresetModalState = {
|
||||||
|
isModalOpen: boolean;
|
||||||
|
updatingStylePresetId: string | null;
|
||||||
|
prefilledFormData: PrefilledFormData | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PrefilledFormData = {
|
||||||
|
name: string;
|
||||||
|
positivePrompt: string;
|
||||||
|
negativePrompt: string;
|
||||||
|
imageUrl: string | null;
|
||||||
|
type: PresetType;
|
||||||
|
};
|
@ -0,0 +1,44 @@
|
|||||||
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
import type { PersistConfig } from 'app/store/store';
|
||||||
|
|
||||||
|
import type { StylePresetState } from './types';
|
||||||
|
|
||||||
|
const initialState: StylePresetState = {
|
||||||
|
activeStylePresetId: null,
|
||||||
|
searchTerm: '',
|
||||||
|
viewMode: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stylePresetSlice = createSlice({
|
||||||
|
name: 'stylePreset',
|
||||||
|
initialState: initialState,
|
||||||
|
reducers: {
|
||||||
|
activeStylePresetIdChanged: (state, action: PayloadAction<string | null>) => {
|
||||||
|
state.activeStylePresetId = action.payload;
|
||||||
|
},
|
||||||
|
searchTermChanged: (state, action: PayloadAction<string>) => {
|
||||||
|
state.searchTerm = action.payload;
|
||||||
|
},
|
||||||
|
viewModeChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.viewMode = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { activeStylePresetIdChanged, searchTermChanged, viewModeChanged } = stylePresetSlice.actions;
|
||||||
|
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
|
const migrateStylePresetState = (state: any): any => {
|
||||||
|
if (!('_version' in state)) {
|
||||||
|
state._version = 1;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stylePresetPersistConfig: PersistConfig<StylePresetState> = {
|
||||||
|
name: stylePresetSlice.name,
|
||||||
|
initialState,
|
||||||
|
migrate: migrateStylePresetState,
|
||||||
|
persistDenylist: [],
|
||||||
|
};
|
@ -0,0 +1,5 @@
|
|||||||
|
export type StylePresetState = {
|
||||||
|
activeStylePresetId: string | null;
|
||||||
|
searchTerm: string;
|
||||||
|
viewMode: boolean;
|
||||||
|
};
|
@ -0,0 +1,15 @@
|
|||||||
|
import { PRESET_PLACEHOLDER } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
|
||||||
|
|
||||||
|
export const getViewModeChunks = (currentPrompt: string, presetPrompt?: string): [string, string, string] => {
|
||||||
|
if (!presetPrompt || !presetPrompt.length) {
|
||||||
|
return ['', currentPrompt, ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [before, after] = presetPrompt.split(PRESET_PLACEHOLDER, 2);
|
||||||
|
|
||||||
|
if (!before || !after) {
|
||||||
|
return ['', `${currentPrompt} `, before || after || ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [before ?? '', currentPrompt, after ?? ''];
|
||||||
|
};
|
@ -19,6 +19,7 @@ const initialConfigState: AppConfig = {
|
|||||||
shouldUpdateImagesOnConnect: false,
|
shouldUpdateImagesOnConnect: false,
|
||||||
shouldFetchMetadataFromApi: false,
|
shouldFetchMetadataFromApi: false,
|
||||||
allowPrivateBoards: false,
|
allowPrivateBoards: false,
|
||||||
|
allowPrivateStylePresets: false,
|
||||||
disabledTabs: [],
|
disabledTabs: [],
|
||||||
disabledFeatures: ['lightbox', 'faceRestore', 'batches'],
|
disabledFeatures: ['lightbox', 'faceRestore', 'batches'],
|
||||||
disabledSDFeatures: ['variation', 'symmetry', 'hires', 'perlinNoise', 'noiseThreshold'],
|
disabledSDFeatures: ['variation', 'symmetry', 'hires', 'perlinNoise', 'noiseThreshold'],
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||||
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
||||||
import QueueControls from 'features/queue/components/QueueControls';
|
import QueueControls from 'features/queue/components/QueueControls';
|
||||||
import { SDXLPrompts } from 'features/sdxl/components/SDXLPrompts/SDXLPrompts';
|
|
||||||
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
|
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
|
||||||
import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion';
|
import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion';
|
||||||
import { ControlSettingsAccordion } from 'features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion';
|
import { ControlSettingsAccordion } from 'features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion';
|
||||||
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
|
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
|
||||||
import { ImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion';
|
import { ImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion';
|
||||||
import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion';
|
import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion';
|
||||||
|
import { StylePresetMenu } from 'features/stylePresets/components/StylePresetMenu';
|
||||||
|
import { StylePresetMenuTrigger } from 'features/stylePresets/components/StylePresetMenuTrigger';
|
||||||
|
import { $isMenuOpen } from 'features/stylePresets/store/isMenuOpen';
|
||||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
@ -21,15 +24,24 @@ const overlayScrollbarsStyles: CSSProperties = {
|
|||||||
|
|
||||||
const ParametersPanelCanvas = () => {
|
const ParametersPanelCanvas = () => {
|
||||||
const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl');
|
const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl');
|
||||||
|
const isMenuOpen = useStore($isMenuOpen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex w="full" h="full" flexDir="column" gap={2}>
|
<Flex w="full" h="full" flexDir="column" gap={2}>
|
||||||
<QueueControls />
|
<QueueControls />
|
||||||
|
<StylePresetMenuTrigger />
|
||||||
<Flex w="full" h="full" position="relative">
|
<Flex w="full" h="full" position="relative">
|
||||||
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
|
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
|
||||||
|
{isMenuOpen && (
|
||||||
|
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
||||||
|
<Flex gap={2} flexDirection="column" h="full" w="full">
|
||||||
|
<StylePresetMenu />
|
||||||
|
</Flex>
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
)}
|
||||||
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
||||||
<Flex gap={2} flexDirection="column" h="full" w="full">
|
<Flex gap={2} flexDirection="column" h="full" w="full">
|
||||||
{isSDXL ? <SDXLPrompts /> : <Prompts />}
|
<Prompts />
|
||||||
<ImageSettingsAccordion />
|
<ImageSettingsAccordion />
|
||||||
<GenerationSettingsAccordion />
|
<GenerationSettingsAccordion />
|
||||||
<ControlSettingsAccordion />
|
<ControlSettingsAccordion />
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import type { ChakraProps } from '@invoke-ai/ui-library';
|
import type { ChakraProps } from '@invoke-ai/ui-library';
|
||||||
import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||||
import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent';
|
import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent';
|
||||||
@ -7,17 +8,19 @@ import { $isPreviewVisible } from 'features/controlLayers/store/controlLayersSli
|
|||||||
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
||||||
import QueueControls from 'features/queue/components/QueueControls';
|
import QueueControls from 'features/queue/components/QueueControls';
|
||||||
import { SDXLPrompts } from 'features/sdxl/components/SDXLPrompts/SDXLPrompts';
|
|
||||||
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
|
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
|
||||||
import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion';
|
import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion';
|
||||||
import { ControlSettingsAccordion } from 'features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion';
|
import { ControlSettingsAccordion } from 'features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion';
|
||||||
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
|
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
|
||||||
import { ImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion';
|
import { ImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion';
|
||||||
import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion';
|
import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion';
|
||||||
|
import { StylePresetMenu } from 'features/stylePresets/components/StylePresetMenu';
|
||||||
|
import { StylePresetMenuTrigger } from 'features/stylePresets/components/StylePresetMenuTrigger';
|
||||||
|
import { $isMenuOpen } from 'features/stylePresets/store/isMenuOpen';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const overlayScrollbarsStyles: CSSProperties = {
|
const overlayScrollbarsStyles: CSSProperties = {
|
||||||
@ -59,14 +62,25 @@ const ParametersPanelTextToImage = () => {
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const isMenuOpen = useStore($isMenuOpen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex w="full" h="full" flexDir="column" gap={2}>
|
<Flex w="full" h="full" flexDir="column" gap={2}>
|
||||||
<QueueControls />
|
<QueueControls />
|
||||||
|
<StylePresetMenuTrigger />
|
||||||
<Flex w="full" h="full" position="relative">
|
<Flex w="full" h="full" position="relative">
|
||||||
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
|
<Box position="absolute" top={0} left={0} right={0} bottom={0} ref={ref}>
|
||||||
|
{isMenuOpen && (
|
||||||
|
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
||||||
|
<Flex gap={2} flexDirection="column" h="full" w="full">
|
||||||
|
<StylePresetMenu />
|
||||||
|
</Flex>
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
)}
|
||||||
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
||||||
<Flex gap={2} flexDirection="column" h="full" w="full">
|
<Flex gap={2} flexDirection="column" h="full" w="full">
|
||||||
{isSDXL ? <SDXLPrompts /> : <Prompts />}
|
<Prompts />
|
||||||
<Tabs
|
<Tabs
|
||||||
defaultIndex={0}
|
defaultIndex={0}
|
||||||
variant="enclosed"
|
variant="enclosed"
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useStore } from '@nanostores/react';
|
||||||
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||||
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
||||||
import QueueControls from 'features/queue/components/QueueControls';
|
import QueueControls from 'features/queue/components/QueueControls';
|
||||||
import { SDXLPrompts } from 'features/sdxl/components/SDXLPrompts/SDXLPrompts';
|
|
||||||
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
|
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
|
||||||
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
|
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
|
||||||
import { UpscaleSettingsAccordion } from 'features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleSettingsAccordion';
|
import { UpscaleSettingsAccordion } from 'features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleSettingsAccordion';
|
||||||
|
import { StylePresetMenu } from 'features/stylePresets/components/StylePresetMenu';
|
||||||
|
import { StylePresetMenuTrigger } from 'features/stylePresets/components/StylePresetMenuTrigger';
|
||||||
|
import { $isMenuOpen } from 'features/stylePresets/store/isMenuOpen';
|
||||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
@ -17,16 +19,23 @@ const overlayScrollbarsStyles: CSSProperties = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ParametersPanelUpscale = () => {
|
const ParametersPanelUpscale = () => {
|
||||||
const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl');
|
const isMenuOpen = useStore($isMenuOpen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex w="full" h="full" flexDir="column" gap={2}>
|
<Flex w="full" h="full" flexDir="column" gap={2}>
|
||||||
<QueueControls />
|
<QueueControls />
|
||||||
|
<StylePresetMenuTrigger />
|
||||||
<Flex w="full" h="full" position="relative">
|
<Flex w="full" h="full" position="relative">
|
||||||
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
|
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
|
||||||
|
{isMenuOpen && (
|
||||||
|
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
||||||
|
<Flex gap={2} flexDirection="column" h="full" w="full">
|
||||||
|
<StylePresetMenu />
|
||||||
|
</Flex>
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
)}
|
||||||
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
||||||
<Flex gap={2} flexDirection="column" h="full" w="full">
|
<Flex gap={2} flexDirection="column" h="full" w="full">
|
||||||
{isSDXL ? <SDXLPrompts /> : <Prompts />}
|
<Prompts />
|
||||||
<UpscaleSettingsAccordion />
|
<UpscaleSettingsAccordion />
|
||||||
<GenerationSettingsAccordion />
|
<GenerationSettingsAccordion />
|
||||||
<AdvancedSettingsAccordion />
|
<AdvancedSettingsAccordion />
|
||||||
|
@ -15,9 +15,9 @@ type UseGetAndLoadLibraryWorkflowReturn = {
|
|||||||
getAndLoadWorkflowResult: ReturnType<typeof useLazyGetWorkflowQuery>[1];
|
getAndLoadWorkflowResult: ReturnType<typeof useLazyGetWorkflowQuery>[1];
|
||||||
};
|
};
|
||||||
|
|
||||||
type UseGetAndLoadLibraryWorkflow = (arg: UseGetAndLoadLibraryWorkflowOptions) => UseGetAndLoadLibraryWorkflowReturn;
|
type UseGetAndLoadLibraryWorkflow = (arg?: UseGetAndLoadLibraryWorkflowOptions) => UseGetAndLoadLibraryWorkflowReturn;
|
||||||
|
|
||||||
export const useGetAndLoadLibraryWorkflow: UseGetAndLoadLibraryWorkflow = ({ onSuccess, onError }) => {
|
export const useGetAndLoadLibraryWorkflow: UseGetAndLoadLibraryWorkflow = (arg) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -29,17 +29,17 @@ export const useGetAndLoadLibraryWorkflow: UseGetAndLoadLibraryWorkflow = ({ onS
|
|||||||
// This action expects a stringified workflow, instead of updating the routes and services we will just stringify it here
|
// This action expects a stringified workflow, instead of updating the routes and services we will just stringify it here
|
||||||
dispatch(workflowLoadRequested({ data: { workflow: JSON.stringify(workflow), graph: null }, asCopy: false }));
|
dispatch(workflowLoadRequested({ data: { workflow: JSON.stringify(workflow), graph: null }, asCopy: false }));
|
||||||
// No toast - the listener for this action does that after the workflow is loaded
|
// No toast - the listener for this action does that after the workflow is loaded
|
||||||
onSuccess && onSuccess();
|
arg?.onSuccess && arg.onSuccess();
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
id: `AUTH_ERROR_TOAST_${workflowsApi.endpoints.getWorkflow.name}`,
|
id: `AUTH_ERROR_TOAST_${workflowsApi.endpoints.getWorkflow.name}`,
|
||||||
title: t('toast.problemRetrievingWorkflow'),
|
title: t('toast.problemRetrievingWorkflow'),
|
||||||
status: 'error',
|
status: 'error',
|
||||||
});
|
});
|
||||||
onError && onError();
|
arg?.onError && arg.onError();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[_getAndLoadWorkflow, dispatch, onSuccess, t, onError, toast]
|
[_getAndLoadWorkflow, dispatch, arg, t, toast]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { getAndLoadWorkflow, getAndLoadWorkflowResult };
|
return { getAndLoadWorkflow, getAndLoadWorkflowResult };
|
||||||
|