Compare commits

..

8 Commits

Author SHA1 Message Date
psychedelicious
affdca4316 infinite grid 2024-10-05 15:22:25 +10:00
psychedelicious
10ca09a5b2 experimenting with image queries 2024-10-05 15:22:25 +10:00
psychedelicious
b88d23e916 feat(ui): remove openDelay on board tooltip
Now that the counts are already available and the tooltip does not make a network request, we can remove the delay (which was added to prevent network thrashing as you moved the mouse over the boards list).
2024-10-05 15:22:25 +10:00
psychedelicious
04460a87ac fix(ui): use correct BoardTooltip component w/ thumbnail image 2024-10-05 15:22:25 +10:00
psychedelicious
da27dcff33 feat(ui): use updated boards data
- Update tooltips to use counts in the DTO
- Remove unused `getBoardImagesTotal` and `getBoardAssetsTotal` queries, which were just abusing the list endpoint to get totals...
- Remove extraneous optimistic update in invocation complete listener
2024-10-05 15:22:04 +10:00
psychedelicious
e3af5d59ed chore(ui): typegen 2024-10-05 15:19:44 +10:00
psychedelicious
bf0fa4be76 feat(app): optimize boards queries
Use SQL instead of python to retrieve image count, asset count and board cover image.

This reduces the number of SQL queries needed to list all boards. Previously, we did `1 + 2 * board_count` queries::
- 1 query to get the list of board records
- 1 query per board to get its total count
- 1 query per board to get its cover image

Then, on the frontend, we made two additional network requests to get each board's counts:
- 1 request (== 1 SQL query) for image count
- 1 request (== 1 SQL query) for asset count

All of this information is now retrieved in a single SQL query, and provided via single network request.

As part of this change, `BoardRecord` now includes `image_count`, `asset_count` and `cover_image_name`. This makes `BoardDTO` redundant, but removing it is a deeper change...
2024-10-05 15:19:44 +10:00
psychedelicious
fe32973e1c feat(app): add method & route to get uncategorized image counts 2024-10-05 15:19:44 +10:00
337 changed files with 3544 additions and 4230 deletions

View File

@@ -105,7 +105,7 @@ Invoke features an organized gallery system for easily storing, accessing, and r
### Other features
- Support for both ckpt and diffusers models
- SD1.5, SD2.0, SDXL, and FLUX support
- SD1.5, SD2.0, and SDXL support
- Upscaling Tools
- Embedding Manager & Support
- Model Manager & Support

View File

@@ -40,7 +40,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
elif [ "$GPU_DRIVER" = "rocm" ]; then \
extra_index_url_arg="--extra-index-url https://download.pytorch.org/whl/rocm5.6"; \
else \
extra_index_url_arg="--extra-index-url https://download.pytorch.org/whl/cu124"; \
extra_index_url_arg="--extra-index-url https://download.pytorch.org/whl/cu121"; \
fi &&\
# xformers + triton fails to install on arm64

View File

@@ -144,7 +144,7 @@ As you might have noticed, we added two new arguments to the `InputField`
definition for `width` and `height`, called `gt` and `le`. They stand for
_greater than or equal to_ and _less than or equal to_.
These impose constraints on those fields, and will raise an exception if the
These impose contraints on those fields, and will raise an exception if the
values do not meet the constraints. Field constraints are provided by
**pydantic**, so anything you see in the **pydantic docs** will work.

View File

@@ -239,7 +239,7 @@ Consult the
get it set up.
Suggest using VSCode's included settings sync so that your remote dev host has
all the same app settings and extensions automatically.
all the same app settings and extensions automagically.
##### One remote dev gotcha

View File

@@ -2,7 +2,7 @@
## **What do I need to know to help?**
If you are looking to help with a code contribution, InvokeAI uses several different technologies under the hood: Python (Pydantic, FastAPI, diffusers) and Typescript (React, Redux Toolkit, ChakraUI, Mantine, Konva). Familiarity with StableDiffusion and image generation concepts is helpful, but not essential.
If you are looking to help to with a code contribution, InvokeAI uses several different technologies under the hood: Python (Pydantic, FastAPI, diffusers) and Typescript (React, Redux Toolkit, ChakraUI, Mantine, Konva). Familiarity with StableDiffusion and image generation concepts is helpful, but not essential.
## **Get Started**

View File

@@ -1,6 +1,6 @@
# Tutorials
Tutorials help new & existing users expand their ability to use InvokeAI to the full extent of our features and services.
Tutorials help new & existing users expand their abilty to use InvokeAI to the full extent of our features and services.
Currently, we have a set of tutorials available on our [YouTube channel](https://www.youtube.com/@invokeai), but as InvokeAI continues to evolve with new updates, we want to ensure that we are giving our users the resources they need to succeed.
@@ -8,4 +8,4 @@ Tutorials can be in the form of videos or article walkthroughs on a subject of y
## Contributing
Please reach out to @imic or @hipsterusername on [Discord](https://discord.gg/ZmtBAhwWhy) to help create tutorials for InvokeAI.
Please reach out to @imic or @hipsterusername on [Discord](https://discord.gg/ZmtBAhwWhy) to help create tutorials for InvokeAI.

View File

@@ -21,7 +21,6 @@ To use a community workflow, download the `.json` node graph file and load it in
+ [Clothing Mask](#clothing-mask)
+ [Contrast Limited Adaptive Histogram Equalization](#contrast-limited-adaptive-histogram-equalization)
+ [Depth Map from Wavefront OBJ](#depth-map-from-wavefront-obj)
+ [Enhance Detail](#enhance-detail)
+ [Film Grain](#film-grain)
+ [Generative Grammar-Based Prompt Nodes](#generative-grammar-based-prompt-nodes)
+ [GPT2RandomPromptMaker](#gpt2randompromptmaker)
@@ -82,7 +81,7 @@ Note: These are inherited from the core nodes so any update to the core nodes sh
**Example Usage:**
</br>
<img src="https://raw.githubusercontent.com/skunkworxdark/autostereogram_nodes/refs/heads/main/images/spider.png" width="200" /> -> <img src="https://raw.githubusercontent.com/skunkworxdark/autostereogram_nodes/refs/heads/main/images/spider-depth.png" width="200" /> -> <img src="https://raw.githubusercontent.com/skunkworxdark/autostereogram_nodes/refs/heads/main/images/spider-dots.png" width="200" /> <img src="https://raw.githubusercontent.com/skunkworxdark/autostereogram_nodes/refs/heads/main/images/spider-pattern.png" width="200" />
<img src="https://github.com/skunkworxdark/autostereogram_nodes/blob/main/images/spider.png" width="200" /> -> <img src="https://github.com/skunkworxdark/autostereogram_nodes/blob/main/images/spider-depth.png" width="200" /> -> <img src="https://github.com/skunkworxdark/autostereogram_nodes/raw/main/images/spider-dots.png" width="200" /> <img src="https://github.com/skunkworxdark/autostereogram_nodes/raw/main/images/spider-pattern.png" width="200" />
--------------------------------
### Average Images
@@ -143,17 +142,6 @@ To be imported, an .obj must use triangulated meshes, so make sure to enable tha
**Example Usage:**
</br><img src="https://raw.githubusercontent.com/dwringer/depth-from-obj-node/main/depth_from_obj_usage.jpg" width="500" />
--------------------------------
### Enhance Detail
**Description:** A single node that can enhance the detail in an image. Increase or decrease details in an image using a guided filter (as opposed to the typical Gaussian blur used by most sharpening filters.) Based on the `Enhance Detail` ComfyUI node from https://github.com/spacepxl/ComfyUI-Image-Filters
**Node Link:** https://github.com/skunkworxdark/enhance-detail-node
**Example Usage:**
</br>
<img src="https://raw.githubusercontent.com/skunkworxdark/enhance-detail-node/refs/heads/main/images/Comparison.png" />
--------------------------------
### Film Grain
@@ -320,7 +308,7 @@ View:
**Node Link:** https://github.com/helix4u/load_video_frame
**Output Example:**
<img src="https://raw.githubusercontent.com/helix4u/load_video_frame/refs/heads/main/_git_assets/dance1736978273.gif" width="500" />
<img src="https://raw.githubusercontent.com/helix4u/load_video_frame/main/_git_assets/testmp4_embed_converted.gif" width="500" />
--------------------------------
### Make 3D
@@ -361,7 +349,7 @@ See full docs here: https://github.com/skunkworxdark/Prompt-tools-nodes/edit/mai
**Output Examples**
<img src="https://github.com/skunkworxdark/match_histogram/assets/21961335/ed12f329-a0ef-444a-9bae-129ed60d6097" />
<img src="https://github.com/skunkworxdark/match_histogram/assets/21961335/ed12f329-a0ef-444a-9bae-129ed60d6097" width="300" />
--------------------------------
### Metadata Linked Nodes
@@ -419,7 +407,7 @@ View:
--------------------------------
### One Button Prompt
<img src="https://raw.githubusercontent.com/AIrjen/OneButtonPrompt_X_InvokeAI/refs/heads/main/images/background.png" width="800" />
<img src="https://github.com/AIrjen/OneButtonPrompt_X_InvokeAI/blob/main/images/background.png" width="800" />
**Description:** an extensive suite of auto prompt generation and prompt helper nodes based on extensive logic. Get creative with the best prompt generator in the world.
@@ -429,7 +417,7 @@ The main node generates interesting prompts based on a set of parameters. There
**Nodes:**
<img src="https://raw.githubusercontent.com/AIrjen/OneButtonPrompt_X_InvokeAI/refs/heads/main/images/OBP_nodes_invokeai.png" width="800" />
<img src="https://github.com/AIrjen/OneButtonPrompt_X_InvokeAI/blob/main/images/OBP_nodes_invokeai.png" width="800" />
--------------------------------
### Oobabooga
@@ -482,7 +470,7 @@ See full docs here: https://github.com/skunkworxdark/Prompt-tools-nodes/edit/mai
**Workflow Examples**
<img src="https://raw.githubusercontent.com/skunkworxdark/prompt-tools/refs/heads/main/images/CSVToIndexStringNode.png"/>
<img src="https://github.com/skunkworxdark/prompt-tools/blob/main/images/CSVToIndexStringNode.png" width="300" />
--------------------------------
### Remote Image
@@ -620,7 +608,7 @@ See full docs here: https://github.com/skunkworxdark/XYGrid_nodes/edit/main/READ
**Output Examples**
<img src="https://raw.githubusercontent.com/skunkworxdark/XYGrid_nodes/refs/heads/main/images/collage.png" />
<img src="https://github.com/skunkworxdark/XYGrid_nodes/blob/main/images/collage.png" width="300" />
--------------------------------

6
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1727955264,
"narHash": "sha256-lrd+7mmb5NauRoMa8+J1jFKYVa+rc8aq2qc9+CxPDKc=",
"lastModified": 1690630721,
"narHash": "sha256-Y04onHyBQT4Erfr2fc82dbJTfXGYrf4V0ysLUYnPOP8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "71cd616696bd199ef18de62524f3df3ffe8b9333",
"rev": "d2b52322f35597c62abf56de91b0236746b2a03d",
"type": "github"
},
"original": {

View File

@@ -34,7 +34,7 @@
cudaPackages.cudnn
cudaPackages.cuda_nvrtc
cudatoolkit
pkg-config
pkgconfig
libconfig
cmake
blas
@@ -66,7 +66,7 @@
black
# Frontend.
pnpm_8
yarn
nodejs
];
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs;

View File

@@ -282,6 +282,12 @@ class InvokeAiInstance:
shutil.copy(src, dest)
os.chmod(dest, 0o0755)
def update(self):
pass
def remove(self):
pass
### Utility functions ###
@@ -396,7 +402,7 @@ def get_torch_source() -> Tuple[str | None, str | None]:
:rtype: list
"""
from messages import GpuType, select_gpu
from messages import select_gpu
# device can be one of: "cuda", "rocm", "cpu", "cuda_and_dml, autodetect"
device = select_gpu()
@@ -406,21 +412,15 @@ def get_torch_source() -> Tuple[str | None, str | None]:
url = None
optional_modules: str | None = None
if OS == "Linux":
if device == GpuType.ROCM:
if device.value == "rocm":
url = "https://download.pytorch.org/whl/rocm5.6"
elif device == GpuType.CPU:
elif device.value == "cpu":
url = "https://download.pytorch.org/whl/cpu"
elif device == GpuType.CUDA:
url = "https://download.pytorch.org/whl/cu124"
optional_modules = "[onnx-cuda]"
elif device == GpuType.CUDA_WITH_XFORMERS:
url = "https://download.pytorch.org/whl/cu124"
elif device.value == "cuda":
# CUDA uses the default PyPi index
optional_modules = "[xformers,onnx-cuda]"
elif OS == "Windows":
if device == GpuType.CUDA:
url = "https://download.pytorch.org/whl/cu124"
optional_modules = "[onnx-cuda]"
elif device == GpuType.CUDA_WITH_XFORMERS:
if device.value == "cuda":
url = "https://download.pytorch.org/whl/cu124"
optional_modules = "[xformers,onnx-cuda]"
elif device.value == "cpu":

View File

@@ -206,7 +206,6 @@ def dest_path(dest: Optional[str | Path] = None) -> Path | None:
class GpuType(Enum):
CUDA_WITH_XFORMERS = "xformers"
CUDA = "cuda"
ROCM = "rocm"
CPU = "cpu"
@@ -222,15 +221,11 @@ def select_gpu() -> GpuType:
return GpuType.CPU
nvidia = (
"an [gold1 b]NVIDIA[/] RTX 3060 or newer GPU using CUDA",
"an [gold1 b]NVIDIA[/] GPU (using CUDA™)",
GpuType.CUDA,
)
vintage_nvidia = (
"an [gold1 b]NVIDIA[/] RTX 20xx or older GPU using CUDA+xFormers",
GpuType.CUDA_WITH_XFORMERS,
)
amd = (
"an [gold1 b]AMD[/] GPU using ROCm",
"an [gold1 b]AMD[/] GPU (using ROCm™)",
GpuType.ROCM,
)
cpu = (
@@ -240,13 +235,14 @@ def select_gpu() -> GpuType:
options = []
if OS == "Windows":
options = [nvidia, vintage_nvidia, cpu]
options = [nvidia, cpu]
if OS == "Linux":
options = [nvidia, vintage_nvidia, amd, cpu]
options = [nvidia, amd, cpu]
elif OS == "Darwin":
options = [cpu]
if len(options) == 1:
print(f'Your platform [gold1]{OS}-{ARCH}[/] only supports the "{options[0][1]}" driver. Proceeding with that.')
return options[0][1]
options = {str(i): opt for i, opt in enumerate(options, 1)}

View File

@@ -5,7 +5,7 @@ from fastapi.routing import APIRouter
from pydantic import BaseModel, Field
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.services.board_records.board_records_common import BoardChanges
from invokeai.app.services.board_records.board_records_common import BoardChanges, UncategorizedImageCounts
from invokeai.app.services.boards.boards_common import BoardDTO
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
@@ -146,3 +146,25 @@ async def list_all_board_image_names(
board_id,
)
return image_names
@boards_router.get(
"/uncategorized/counts",
operation_id="get_uncategorized_image_counts",
response_model=UncategorizedImageCounts,
)
async def get_uncategorized_image_counts() -> UncategorizedImageCounts:
"""Gets count of images and assets for uncategorized images (images with no board assocation)"""
return ApiDependencies.invoker.services.board_records.get_uncategorized_image_counts()
@boards_router.get(
"/uncategorized/names",
operation_id="get_uncategorized_image_names",
response_model=list[str],
)
async def get_uncategorized_image_names() -> list[str]:
"""Gets count of images and assets for uncategorized images (images with no board assocation)"""
return ApiDependencies.invoker.services.board_records.get_uncategorized_image_names()

View File

@@ -0,0 +1,61 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
"import sqlite3\n",
"from uuid import uuid4\n",
"\n",
"# duplicate _all_ images in gallery\n",
"\n",
"def duplicate_images(database_path: Path, num_copies: int):\n",
" conn = sqlite3.connect(database_path)\n",
" cursor = conn.cursor()\n",
"\n",
" cursor.execute(\"SELECT * FROM images\")\n",
" rows = cursor.fetchall()\n",
"\n",
" for _ in range(num_copies):\n",
" for row in rows:\n",
" new_row = list(row)\n",
" new_row[0] = str(uuid4()) # image_name is the first column\n",
" placeholders = \", \".join(\"?\" for _ in new_row)\n",
" cursor.execute(f\"INSERT INTO images VALUES ({placeholders})\", new_row)\n",
"\n",
" conn.commit()\n",
" conn.close()\n",
"\n",
"\n",
"if __name__ == \"__main__\":\n",
" database_path = Path(\"/home/bat/invokeai-4.0.0/databases/invokeai.db\")\n",
" num_copies = 50\n",
" duplicate_images(database_path, num_copies)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.12"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@@ -1,6 +1,6 @@
import io
import traceback
from typing import Optional
from typing import Literal, Optional
from fastapi import BackgroundTasks, Body, HTTPException, Path, Query, Request, Response, UploadFile
from fastapi.responses import FileResponse
@@ -12,10 +12,11 @@ from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.invocations.fields import MetadataField
from invokeai.app.services.image_records.image_records_common import (
ImageCategory,
ImageRecord,
ImageRecordChanges,
ResourceOrigin,
)
from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO
from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO, image_record_to_dto
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
@@ -462,3 +463,76 @@ async def get_bulk_download_item(
return response
except Exception:
raise HTTPException(status_code=404)
@images_router.get(
"/image_names",
operation_id="list_image_names",
response_model=list[str],
)
async def list_image_names(
board_id: str | None = Query(default=None),
category: Literal["images", "assets"] = Query(default="images"),
starred_first: bool = Query(default=True),
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending),
search_term: Optional[str] = Query(default=None),
) -> list[str]:
"""Gets a list of image names"""
return ApiDependencies.invoker.services.image_records.get_image_names(
board_id,
category,
starred_first,
order_dir,
search_term,
)
@images_router.get(
"/images",
operation_id="list_images",
response_model=list[ImageRecord],
)
async def images(
board_id: str | None = Query(default=None),
category: Literal["images", "assets"] = Query(default="images"),
starred_first: bool = Query(default=True),
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending),
search_term: str | None = Query(default=None),
from_image_name: str | None = Query(default=None),
count: int = Query(default=10),
) -> list[ImageRecord]:
"""Gets a list of image names"""
return ApiDependencies.invoker.services.image_records.get_images(
board_id,
category,
starred_first,
order_dir,
search_term,
from_image_name,
count,
)
@images_router.post(
"/images/by_name",
operation_id="get_images_by_name",
response_model=list[ImageDTO],
)
async def get_images_by_name(image_names: list[str] = Body(embed=True)) -> list[ImageDTO]:
"""Gets a list of image names"""
image_records = ApiDependencies.invoker.services.image_records.get_images_by_name(image_names)
image_dtos = [
image_record_to_dto(
image_record=r,
image_url=ApiDependencies.invoker.services.urls.get_image_url(r.image_name),
thumbnail_url=ApiDependencies.invoker.services.urls.get_image_url(r.image_name, True),
board_id=ApiDependencies.invoker.services.board_image_records.get_board_for_image(r.image_name),
)
for r in image_records
]
return image_dtos

View File

@@ -83,15 +83,15 @@ async def create_workflow(
)
async def list_workflows(
page: int = Query(default=0, description="The page to get"),
per_page: Optional[int] = Query(default=None, description="The number of workflows per page"),
per_page: int = Query(default=10, description="The number of workflows per page"),
order_by: WorkflowRecordOrderBy = Query(
default=WorkflowRecordOrderBy.Name, description="The attribute to order by"
),
direction: SQLiteDirection = Query(default=SQLiteDirection.Ascending, description="The direction to order by"),
category: Optional[WorkflowCategory] = Query(default=None, description="The category of workflow to get"),
category: WorkflowCategory = Query(default=WorkflowCategory.User, description="The category of workflow to get"),
query: Optional[str] = Query(default=None, description="The text to query by (matches name and description)"),
) -> PaginatedResults[WorkflowRecordListItemDTO]:
"""Gets a page of workflows"""
return ApiDependencies.invoker.services.workflow_records.get_many(
order_by=order_by, direction=direction, page=page, per_page=per_page, query=query, category=category
page=page, per_page=per_page, order_by=order_by, direction=direction, query=query, category=category
)

View File

@@ -1,6 +1,6 @@
from abc import ABC, abstractmethod
from invokeai.app.services.board_records.board_records_common import BoardChanges, BoardRecord
from invokeai.app.services.board_records.board_records_common import BoardChanges, BoardRecord, UncategorizedImageCounts
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
@@ -48,3 +48,13 @@ class BoardRecordStorageBase(ABC):
def get_all(self, include_archived: bool = False) -> list[BoardRecord]:
"""Gets all board records."""
pass
@abstractmethod
def get_uncategorized_image_counts(self) -> UncategorizedImageCounts:
"""Gets count of images and assets for uncategorized images (images with no board assocation)."""
pass
@abstractmethod
def get_uncategorized_image_names(self) -> list[str]:
"""Gets names of uncategorized images."""
pass

View File

@@ -1,5 +1,5 @@
from datetime import datetime
from typing import Optional, Union
from typing import Any, Optional, Union
from pydantic import BaseModel, Field
@@ -26,21 +26,25 @@ class BoardRecord(BaseModelExcludeNull):
"""Whether or not the board is archived."""
is_private: Optional[bool] = Field(default=None, description="Whether the board is private.")
"""Whether the board is private."""
image_count: int = Field(description="The number of images in the board.")
asset_count: int = Field(description="The number of assets in the board.")
def deserialize_board_record(board_dict: dict) -> BoardRecord:
def deserialize_board_record(board_dict: dict[str, Any]) -> BoardRecord:
"""Deserializes a board record."""
# Retrieve all the values, setting "reasonable" defaults if they are not present.
board_id = board_dict.get("board_id", "unknown")
board_name = board_dict.get("board_name", "unknown")
cover_image_name = board_dict.get("cover_image_name", "unknown")
cover_image_name = board_dict.get("cover_image_name", None)
created_at = board_dict.get("created_at", get_iso_timestamp())
updated_at = board_dict.get("updated_at", get_iso_timestamp())
deleted_at = board_dict.get("deleted_at", get_iso_timestamp())
archived = board_dict.get("archived", False)
is_private = board_dict.get("is_private", False)
image_count = board_dict.get("image_count", 0)
asset_count = board_dict.get("asset_count", 0)
return BoardRecord(
board_id=board_id,
@@ -51,6 +55,8 @@ def deserialize_board_record(board_dict: dict) -> BoardRecord:
deleted_at=deleted_at,
archived=archived,
is_private=is_private,
image_count=image_count,
asset_count=asset_count,
)
@@ -63,19 +69,24 @@ class BoardChanges(BaseModel, extra="forbid"):
class BoardRecordNotFoundException(Exception):
"""Raised when an board record is not found."""
def __init__(self, message="Board record not found"):
def __init__(self, message: str = "Board record not found"):
super().__init__(message)
class BoardRecordSaveException(Exception):
"""Raised when an board record cannot be saved."""
def __init__(self, message="Board record not saved"):
def __init__(self, message: str = "Board record not saved"):
super().__init__(message)
class BoardRecordDeleteException(Exception):
"""Raised when an board record cannot be deleted."""
def __init__(self, message="Board record not deleted"):
def __init__(self, message: str = "Board record not deleted"):
super().__init__(message)
class UncategorizedImageCounts(BaseModel):
image_count: int = Field(description="The number of uncategorized images.")
asset_count: int = Field(description="The number of uncategorized assets.")

View File

@@ -9,12 +9,121 @@ from invokeai.app.services.board_records.board_records_common import (
BoardRecordDeleteException,
BoardRecordNotFoundException,
BoardRecordSaveException,
UncategorizedImageCounts,
deserialize_board_record,
)
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
from invokeai.app.util.misc import uuid_string
_BASE_BOARD_RECORD_QUERY = """
-- This query retrieves board records, joining with the board_images and images tables to get image counts and cover image names.
-- It is not a complete query, as it is missing a GROUP BY or WHERE clause (and is unterminated).
SELECT b.board_id,
b.board_name,
b.created_at,
b.updated_at,
b.archived,
-- Count the number of images in the board, alias image_count
COUNT(
CASE
WHEN i.image_category in ('general') -- "Images" are images in the 'general' category
AND i.is_intermediate = 0 THEN 1 -- Intermediates are not counted
END
) AS image_count,
-- Count the number of assets in the board, alias asset_count
COUNT(
CASE
WHEN i.image_category in ('control', 'mask', 'user', 'other') -- "Assets" are images in any of the other categories ('control', 'mask', 'user', 'other')
AND i.is_intermediate = 0 THEN 1 -- Intermediates are not counted
END
) AS asset_count,
-- Get the name of the the most recent image in the board, alias cover_image_name
(
SELECT bi.image_name
FROM board_images bi
JOIN images i ON bi.image_name = i.image_name
WHERE bi.board_id = b.board_id
AND i.is_intermediate = 0 -- Intermediates cannot be cover images
ORDER BY i.created_at DESC -- Sort by created_at to get the most recent image
LIMIT 1
) AS cover_image_name
FROM boards b
LEFT JOIN board_images bi ON b.board_id = bi.board_id
LEFT JOIN images i ON bi.image_name = i.image_name
"""
def get_paginated_list_board_records_queries(include_archived: bool) -> str:
"""Gets a query to retrieve a paginated list of board records. The query has placeholders for limit and offset.
Args:
include_archived: Whether to include archived board records in the results.
Returns:
A query to retrieve a paginated list of board records.
"""
archived_condition = "WHERE b.archived = 0" if not include_archived else ""
# The GROUP BY must be added _after_ the WHERE clause!
query = f"""
{_BASE_BOARD_RECORD_QUERY}
{archived_condition}
GROUP BY b.board_id,
b.board_name,
b.created_at,
b.updated_at
ORDER BY b.created_at DESC
LIMIT ? OFFSET ?;
"""
return query
def get_total_boards_count_query(include_archived: bool) -> str:
"""Gets a query to retrieve the total count of board records.
Args:
include_archived: Whether to include archived board records in the count.
Returns:
A query to retrieve the total count of board records.
"""
archived_condition = "WHERE b.archived = 0" if not include_archived else ""
return f"SELECT COUNT(*) FROM boards {archived_condition};"
def get_list_all_board_records_query(include_archived: bool) -> str:
"""Gets a query to retrieve all board records.
Args:
include_archived: Whether to include archived board records in the results.
Returns:
A query to retrieve all board records.
"""
archived_condition = "WHERE b.archived = 0" if not include_archived else ""
return f"""
{_BASE_BOARD_RECORD_QUERY}
{archived_condition}
GROUP BY b.board_id,
b.board_name,
b.created_at,
b.updated_at
ORDER BY b.created_at DESC;
"""
def get_board_record_query() -> str:
"""Gets a query to retrieve a board record. The query has a placeholder for the board_id."""
return f"{_BASE_BOARD_RECORD_QUERY} WHERE b.board_id = ?;"
class SqliteBoardRecordStorage(BoardRecordStorageBase):
_conn: sqlite3.Connection
@@ -76,11 +185,7 @@ class SqliteBoardRecordStorage(BoardRecordStorageBase):
try:
self._lock.acquire()
self._cursor.execute(
"""--sql
SELECT *
FROM boards
WHERE board_id = ?;
""",
get_board_record_query(),
(board_id,),
)
@@ -92,7 +197,7 @@ class SqliteBoardRecordStorage(BoardRecordStorageBase):
self._lock.release()
if result is None:
raise BoardRecordNotFoundException
return BoardRecord(**dict(result))
return deserialize_board_record(dict(result))
def update(
self,
@@ -149,45 +254,15 @@ class SqliteBoardRecordStorage(BoardRecordStorageBase):
try:
self._lock.acquire()
# Build base query
base_query = """
SELECT *
FROM boards
{archived_filter}
ORDER BY created_at DESC
LIMIT ? OFFSET ?;
"""
main_query = get_paginated_list_board_records_queries(include_archived=include_archived)
# Determine archived filter condition
if include_archived:
archived_filter = ""
else:
archived_filter = "WHERE archived = 0"
final_query = base_query.format(archived_filter=archived_filter)
# Execute query to fetch boards
self._cursor.execute(final_query, (limit, offset))
self._cursor.execute(main_query, (limit, offset))
result = cast(list[sqlite3.Row], self._cursor.fetchall())
boards = [deserialize_board_record(dict(r)) for r in result]
# Determine count query
if include_archived:
count_query = """
SELECT COUNT(*)
FROM boards;
"""
else:
count_query = """
SELECT COUNT(*)
FROM boards
WHERE archived = 0;
"""
# Execute count query
self._cursor.execute(count_query)
total_query = get_total_boards_count_query(include_archived=include_archived)
self._cursor.execute(total_query)
count = cast(int, self._cursor.fetchone()[0])
return OffsetPaginatedResults[BoardRecord](items=boards, offset=offset, limit=limit, total=count)
@@ -201,26 +276,10 @@ class SqliteBoardRecordStorage(BoardRecordStorageBase):
def get_all(self, include_archived: bool = False) -> list[BoardRecord]:
try:
self._lock.acquire()
base_query = """
SELECT *
FROM boards
{archived_filter}
ORDER BY created_at DESC
"""
if include_archived:
archived_filter = ""
else:
archived_filter = "WHERE archived = 0"
final_query = base_query.format(archived_filter=archived_filter)
self._cursor.execute(final_query)
query = get_list_all_board_records_query(include_archived=include_archived)
self._cursor.execute(query)
result = cast(list[sqlite3.Row], self._cursor.fetchall())
boards = [deserialize_board_record(dict(r)) for r in result]
return boards
except sqlite3.Error as e:
@@ -228,3 +287,47 @@ class SqliteBoardRecordStorage(BoardRecordStorageBase):
raise e
finally:
self._lock.release()
def get_uncategorized_image_counts(self) -> UncategorizedImageCounts:
try:
self._lock.acquire()
query = """
-- Get the count of uncategorized images and assets.
SELECT
CASE
WHEN i.image_category = 'general' THEN 'image_count' -- "Images" are images in the 'general' category
ELSE 'asset_count' -- "Assets" are images in any of the other categories ('control', 'mask', 'user', 'other')
END AS category_type,
COUNT(*) AS unassigned_count
FROM images i
LEFT JOIN board_images bi ON i.image_name = bi.image_name
WHERE bi.board_id IS NULL -- Uncategorized images have no board association
AND i.is_intermediate = 0 -- Omit intermediates from the counts
GROUP BY category_type; -- Group by category_type alias, as derived from the image_category column earlier
"""
self._cursor.execute(query)
results = self._cursor.fetchall()
image_count = dict(results)["image_count"]
asset_count = dict(results)["asset_count"]
return UncategorizedImageCounts(image_count=image_count, asset_count=asset_count)
finally:
self._lock.release()
def get_uncategorized_image_names(self) -> list[str]:
try:
self._lock.acquire()
self._cursor.execute(
"""--sql
SELECT image_name
FROM images
WHERE image_name NOT IN (
SELECT image_name
FROM board_images
);
"""
)
result = cast(list[sqlite3.Row], self._cursor.fetchall())
image_names = [r[0] for r in result]
return image_names
finally:
self._lock.release()

View File

@@ -1,23 +1,8 @@
from typing import Optional
from pydantic import Field
from invokeai.app.services.board_records.board_records_common import BoardRecord
# TODO(psyche): BoardDTO is now identical to BoardRecord. We should consider removing it.
class BoardDTO(BoardRecord):
"""Deserialized board record with cover image URL and image count."""
"""Deserialized board record."""
cover_image_name: Optional[str] = Field(description="The name of the board's cover image.")
"""The URL of the thumbnail of the most recent image in the board."""
image_count: int = Field(description="The number of images in the board.")
"""The number of images in the board."""
def board_record_to_dto(board_record: BoardRecord, cover_image_name: Optional[str], image_count: int) -> BoardDTO:
"""Converts a board record to a board DTO."""
return BoardDTO(
**board_record.model_dump(exclude={"cover_image_name"}),
cover_image_name=cover_image_name,
image_count=image_count,
)
pass

View File

@@ -1,6 +1,6 @@
from invokeai.app.services.board_records.board_records_common import BoardChanges
from invokeai.app.services.boards.boards_base import BoardServiceABC
from invokeai.app.services.boards.boards_common import BoardDTO, board_record_to_dto
from invokeai.app.services.boards.boards_common import BoardDTO
from invokeai.app.services.invoker import Invoker
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
@@ -16,17 +16,11 @@ class BoardService(BoardServiceABC):
board_name: str,
) -> BoardDTO:
board_record = self.__invoker.services.board_records.save(board_name)
return board_record_to_dto(board_record, None, 0)
return BoardDTO.model_validate(board_record.model_dump())
def get_dto(self, board_id: str) -> BoardDTO:
board_record = self.__invoker.services.board_records.get(board_id)
cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(board_record.board_id)
if cover_image:
cover_image_name = cover_image.image_name
else:
cover_image_name = None
image_count = self.__invoker.services.board_image_records.get_image_count_for_board(board_id)
return board_record_to_dto(board_record, cover_image_name, image_count)
return BoardDTO.model_validate(board_record.model_dump())
def update(
self,
@@ -34,14 +28,7 @@ class BoardService(BoardServiceABC):
changes: BoardChanges,
) -> BoardDTO:
board_record = self.__invoker.services.board_records.update(board_id, changes)
cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(board_record.board_id)
if cover_image:
cover_image_name = cover_image.image_name
else:
cover_image_name = None
image_count = self.__invoker.services.board_image_records.get_image_count_for_board(board_id)
return board_record_to_dto(board_record, cover_image_name, image_count)
return BoardDTO.model_validate(board_record.model_dump())
def delete(self, board_id: str) -> None:
self.__invoker.services.board_records.delete(board_id)
@@ -50,30 +37,10 @@ class BoardService(BoardServiceABC):
self, offset: int = 0, limit: int = 10, include_archived: bool = False
) -> OffsetPaginatedResults[BoardDTO]:
board_records = self.__invoker.services.board_records.get_many(offset, limit, include_archived)
board_dtos = []
for r in board_records.items:
cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(r.board_id)
if cover_image:
cover_image_name = cover_image.image_name
else:
cover_image_name = None
image_count = self.__invoker.services.board_image_records.get_image_count_for_board(r.board_id)
board_dtos.append(board_record_to_dto(r, cover_image_name, image_count))
board_dtos = [BoardDTO.model_validate(r.model_dump()) for r in board_records.items]
return OffsetPaginatedResults[BoardDTO](items=board_dtos, offset=offset, limit=limit, total=len(board_dtos))
def get_all(self, include_archived: bool = False) -> list[BoardDTO]:
board_records = self.__invoker.services.board_records.get_all(include_archived)
board_dtos = []
for r in board_records:
cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(r.board_id)
if cover_image:
cover_image_name = cover_image.image_name
else:
cover_image_name = None
image_count = self.__invoker.services.board_image_records.get_image_count_for_board(r.board_id)
board_dtos.append(board_record_to_dto(r, cover_image_name, image_count))
board_dtos = [BoardDTO.model_validate(r.model_dump()) for r in board_records]
return board_dtos

View File

@@ -250,9 +250,9 @@ class InvokeAIAppConfig(BaseSettings):
)
if as_example:
file.write("# This is an example file with default and example settings.\n")
file.write("# You should not copy this whole file into your config.\n")
file.write("# Only add the settings you need to change to your config file.\n\n")
file.write(
"# This is an example file with default and example settings. Use the values here as a baseline.\n\n"
)
file.write("# Internal metadata - do not edit:\n")
file.write(yaml.dump(meta_dict, sort_keys=False))
file.write("\n")

View File

@@ -1,6 +1,6 @@
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Optional
from typing import Literal, Optional
from invokeai.app.invocations.fields import MetadataField
from invokeai.app.services.image_records.image_records_common import (
@@ -97,3 +97,32 @@ class ImageRecordStorageBase(ABC):
def get_most_recent_image_for_board(self, board_id: str) -> Optional[ImageRecord]:
"""Gets the most recent image for a board."""
pass
@abstractmethod
def get_image_names(
self,
board_id: str | None,
category: Literal["images", "assets"],
starred_first: bool = True,
order_dir: SQLiteDirection = SQLiteDirection.Descending,
search_term: Optional[str] = None,
) -> list[str]:
"""Gets image names."""
pass
@abstractmethod
def get_images_by_name(self, image_names: list[str]) -> list[ImageRecord]:
pass
@abstractmethod
def get_images(
self,
board_id: str | None = None,
category: Literal["images", "assets"] = "images",
starred_first: bool = True,
order_dir: SQLiteDirection = SQLiteDirection.Descending,
search_term: str | None = None,
from_image_name: str | None = None, # omit for first page
count: int = 10,
) -> list[ImageRecord]:
pass

View File

@@ -1,7 +1,7 @@
import sqlite3
import threading
from datetime import datetime
from typing import Optional, Union, cast
from typing import Literal, Optional, Union, cast
from invokeai.app.invocations.fields import MetadataField, MetadataFieldValidator
from invokeai.app.services.image_records.image_records_base import ImageRecordStorageBase
@@ -140,6 +140,264 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
finally:
self._lock.release()
# def get_image_names(
# self,
# board_id: str | None = None,
# category: Literal["images", "assets"] = "images",
# starred_first: bool = True,
# order_dir: SQLiteDirection = SQLiteDirection.Descending,
# search_term: Optional[str] = None,
# ) -> list[str]:
# try:
# self._lock.acquire()
# query = """
# SELECT images.image_name
# FROM images
# LEFT JOIN board_images ON board_images.image_name = images.image_name
# WHERE images.is_intermediate = FALSE
# """
# params: list[int | str | bool] = []
# if board_id:
# query += """
# AND board_images.board_id = ?
# """
# params.append(board_id)
# else:
# query += """
# AND board_images.board_id IS NULL
# """
# if category == "images":
# query += """
# AND images.image_category = 'general'
# """
# elif category == "assets":
# query += """
# AND images.image_category IN ('control', 'mask', 'user', 'other')
# """
# else:
# raise ValueError(f"Invalid category: {category}")
# if search_term:
# query += """
# AND images.metadata LIKE ?
# """
# params.append(f"%{search_term.lower()}%")
# if starred_first:
# query += f"""
# ORDER BY images.starred DESC, images.created_at {order_dir.value} -- cannot use parameter substitution here
# """
# else:
# query += f"""
# ORDER BY images.created_at {order_dir.value} -- cannot use parameter substitution here
# """
# query += ";"
# params_tuple = tuple(params)
# self._cursor.execute(query, params_tuple)
# result = cast(list[sqlite3.Row], self._cursor.fetchall())
# image_names = [str(r[0]) for r in result]
# except Exception:
# raise
# finally:
# self._lock.release()
# return image_names
def get_image_names(
self,
board_id: str | None = None,
category: Literal["images", "assets"] = "images",
starred_first: bool = True,
order_dir: SQLiteDirection = SQLiteDirection.Descending,
search_term: str | None = None,
) -> list[str]:
try:
self._lock.acquire()
base_query = """
SELECT images.image_name
FROM images
LEFT JOIN board_images ON board_images.image_name = images.image_name
WHERE images.is_intermediate = FALSE
"""
params: list[int | str | bool] = []
if board_id:
base_query += """
AND board_images.board_id = ?
"""
params.append(board_id)
else:
base_query += """
AND board_images.board_id IS NULL
"""
if category == "images":
base_query += """
AND images.image_category = 'general'
"""
elif category == "assets":
base_query += """
AND images.image_category IN ('control', 'mask', 'user', 'other')
"""
else:
raise ValueError(f"Invalid category: {category}")
if search_term:
base_query += """
AND images.metadata LIKE ?
"""
params.append(f"%{search_term.lower()}%")
if starred_first:
base_query += f"""
ORDER BY images.starred DESC, images.created_at {order_dir.value}, images.image_name {order_dir.value}
"""
else:
base_query += f"""
ORDER BY images.created_at {order_dir.value}, images.image_name {order_dir.value}
"""
final_query = f"{base_query};"
self._cursor.execute(final_query, tuple(params))
result = cast(list[sqlite3.Row], self._cursor.fetchall())
images = [str(r[0]) for r in result]
except Exception:
raise
finally:
self._lock.release()
return images
def get_images_by_name(self, image_names: list[str]) -> list[ImageRecord]:
try:
self._lock.acquire()
query = f"""
SELECT {IMAGE_DTO_COLS}
FROM images
WHERE images.image_name in ({",".join("?" for _ in image_names)});
"""
params = tuple(image_names)
self._cursor.execute(query, tuple(params))
result = cast(list[sqlite3.Row], self._cursor.fetchall())
images = [deserialize_image_record(dict(r)) for r in result]
except Exception:
raise
finally:
self._lock.release()
return images
def get_images(
self,
board_id: str | None = None,
category: Literal["images", "assets"] = "images",
starred_first: bool = True,
order_dir: SQLiteDirection = SQLiteDirection.Descending,
search_term: str | None = None,
from_image_name: str | None = None, # omit for first page
count: int = 10,
) -> list[ImageRecord]:
try:
self._lock.acquire()
base_query = f"""
SELECT {IMAGE_DTO_COLS}
FROM images
LEFT JOIN board_images ON board_images.image_name = images.image_name
WHERE images.is_intermediate = FALSE
"""
params: list[int | str | bool] = []
if board_id:
base_query += """
AND board_images.board_id = ?
"""
params.append(board_id)
else:
base_query += """
AND board_images.board_id IS NULL
"""
if category == "images":
base_query += """
AND images.image_category = 'general'
"""
elif category == "assets":
base_query += """
AND images.image_category IN ('control', 'mask', 'user', 'other')
"""
else:
raise ValueError(f"Invalid category: {category}")
if search_term:
base_query += """
AND images.metadata LIKE ?
"""
params.append(f"%{search_term.lower()}%")
if from_image_name:
# Use keyset pagination to get the next page of results
keyset_query = f"""
WITH image_keyset AS (
SELECT created_at,
image_name
FROM images
WHERE image_name = ?
)
{base_query}
AND (images.created_at, images.image_name) < (
(
SELECT created_at
FROM image_keyset
),
(
SELECT image_name
FROM image_keyset
)
)
"""
base_query = keyset_query
params.append(from_image_name)
if starred_first:
order_by_clause = f"""
ORDER BY images.starred DESC, images.created_at {order_dir.value}, images.image_name {order_dir.value}
"""
else:
order_by_clause = f"""
ORDER BY images.created_at {order_dir.value}, images.image_name {order_dir.value}
"""
final_query = f"""
{base_query}
{order_by_clause}
LIMIT ?;
"""
params.append(count)
self._cursor.execute(final_query, tuple(params))
result = cast(list[sqlite3.Row], self._cursor.fetchall())
images = [deserialize_image_record(dict(r)) for r in result]
except Exception:
raise
finally:
self._lock.release()
return images
def get_many(
self,
offset: int = 0,

View File

@@ -0,0 +1,59 @@
these ideas are trying to figure out the macOS photos UX where you use scroll position instead of page number to go to a specific range of images.
### Brute Force
Two new methods/endpoints:
- `get_image_names`: gets a list of ordered image names for the query params (e.g. board, starred_first)
- `get_images_by_name`: gets the dtos for a list of image names
Broad strokes of client handling:
- Fetch a list of all image names for a board.
- Render a big scroll area, large enough to hold all images. The list of image names is passed to `react-virtuoso` (virtualized list lib).
- As you scroll, we use the rangeChanged callback from `react-virtuoso`, which provides the indices of the currently-visible images in the list of all images. These indices map back to the list of image names from which we can derive the list of image names we need to fetch
- Debounce the rnageChanged callback
- Call the `get_images_by_name` endpoint with hte image names to fetch, use the result to update the `getImageDTO` query cache. De-duplicate the image_names against existing cache before fetching so we aren't requesting the smae data over and over
- Each item/image in the virtualized list fetches its image DTO from the cache _without initiating a network request_. it just kinda waits until the image is in the cache and then displays it
this is roughed out in this branch
#### FATAL FLAW
Once you generate an image, you want to do an optimistic update and insert its name into the big ol' image_names list right? well, where do you insert it? depends on the query parms that can affect the sort order and which images are shown... we only have the image names at this point so we can't easily figure out where to insert
workarounds (?):
- along with the image names, we retrieve `starred_first` and `created_at`. then from the query params we can easily figure out where to insert the new image into the list to match the sort that he backend will be doing. eh
- fetch `starred_first` images separately? so we don't have to worry about inserting the image into the right spot?
ahh but also metadata search... we won't know when to insert the image into the list if the user has a search term...
#### Sub-idea
Ok let's still use pagination but use virtuoso to tell us which page we are on.
virtuoso has an alternate mode where you just tell it how many items you have and it renders each item, passing only an index to it. Maybe we can derive the limit and offset from this information. here's an untested idea:
- pass virtuoso the board count
- Instead of rendering individual images in the list, we render pages (ranges) of images. The list librarys rangeChanged indices now refer to pages or ranges. To the user, it still looks like a bunch of individual images, but internally we group it into pages/ranges of whatever size.
- The page/range size is calculated via DOM, or we can rely on virtuoso to tell us how many items are to be rendered. only thing is it the number can different depending on scroll position, so we'd probably want to like take `endIndex - startIndex` as the limit, add 20% buffer to each end of the limit and round it to the nearest multiple of 5 or 10. that would give us a consistent limit
- then we can derive offset from that value
still has the issue where we aren't sure if we should trigger a image list cache invalidation...
### More Efficient Pagination
sql OFFSET requires a scan thru the whole table upt othe offset. that means the higher the offset, the slower the query. unsure of the practical impact of this, probably negligible for us right now.
I did some quick experiments with cursor/keyset pagination, using an image name as the cursor. this doesn't have the perf issue w/ offset.
Also! This kind of pagination is unaffected by insertions and deletions, which is a problem for limit/offset pagination. When you insert or delete an image, it doesn't shift images at higher pages down. I think this pagination strategy suits our gallery better than limit/offset, given how volatile it is with adding and removing images regularly.
see the `test_keyset` notebook for implementation (also some scattered methods in services as I was fiddling withh it)
may be some way to use this pagination strat in combination with the above ideas to more elegantly handle inserting and deleting images...
### Alternative approach to the whole "how do we know when to insert new images in the list (or invalidate the list cache)" issue
What if we _always_ invalidate the cache when youa re at the top of the list ,but never invalidate it when you have scrolled down?

View File

@@ -0,0 +1,210 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"first query\n",
"36e62fec-5c3a-4b28-867b-9029fb6d2319.png False\n",
"c7f4f4b8-7ce6-4594-abf6-3f5e13fb7fe9.png False\n",
"d8f57fda-5084-4d87-8668-06fb300282e4.png False\n",
"a2fd7b8b-bbe5-4629-9d46-000f99b64931.png False\n",
"c0880bc1-5f7a-452b-acea-53a261f4c0c4.png False\n",
"0ad957df-c341-48e3-b384-f656985c2722.png False\n",
"8c788d82-c81c-4ffe-bf6b-bdad601c5add.png False\n",
"9b1179a0-09a0-4430-918d-60b618ff040c.png False\n",
"c8ad6a32-75db-4d8b-a865-066365fa1563.png False\n",
"e5eb1c19-8c69-4d29-a447-fbc2d649334a.png False\n",
"\n",
"next query, starting from the second image\n",
"36e62fec-5c3a-4b28-867b-9029fb6d2319.png False\n",
"c7f4f4b8-7ce6-4594-abf6-3f5e13fb7fe9.png False\n",
"d8f57fda-5084-4d87-8668-06fb300282e4.png False\n",
"a2fd7b8b-bbe5-4629-9d46-000f99b64931.png False\n",
"c0880bc1-5f7a-452b-acea-53a261f4c0c4.png False\n",
"0ad957df-c341-48e3-b384-f656985c2722.png False\n",
"8c788d82-c81c-4ffe-bf6b-bdad601c5add.png False\n",
"9b1179a0-09a0-4430-918d-60b618ff040c.png False\n",
"c8ad6a32-75db-4d8b-a865-066365fa1563.png False\n",
"e5eb1c19-8c69-4d29-a447-fbc2d649334a.png False\n"
]
}
],
"source": [
"import sqlite3\n",
"from typing import Literal, cast\n",
"from invokeai.app.services.image_records.image_records_common import (\n",
" IMAGE_DTO_COLS,\n",
" ImageRecord,\n",
" deserialize_image_record,\n",
")\n",
"from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection\n",
"\n",
"\n",
"def get_images(\n",
" from_image_name: str | None = None, # omit for first page\n",
" count: int = 10,\n",
" board_id: str | None = None,\n",
" category: Literal[\"images\", \"assets\"] = \"images\",\n",
" starred_first: bool = False,\n",
" order_dir: SQLiteDirection = SQLiteDirection.Descending,\n",
" search_term: str | None = None,\n",
") -> list[ImageRecord]:\n",
" conn = sqlite3.connect(\"/home/bat/invokeai-4.0.0/databases/invokeai.db\")\n",
" conn.row_factory = sqlite3.Row\n",
" cursor = conn.cursor()\n",
"\n",
" base_query = f\"\"\"\n",
" SELECT {IMAGE_DTO_COLS}\n",
" FROM images\n",
" LEFT JOIN board_images ON board_images.image_name = images.image_name\n",
" WHERE images.is_intermediate = FALSE\n",
" \"\"\"\n",
" params: list[int | str | bool] = []\n",
"\n",
" if board_id:\n",
" base_query += \"\"\"\n",
" AND board_images.board_id = ?\n",
" \"\"\"\n",
" params.append(board_id)\n",
" else:\n",
" base_query += \"\"\"\n",
" AND board_images.board_id IS NULL\n",
" \"\"\"\n",
"\n",
" if category == \"images\":\n",
" base_query += \"\"\"\n",
" AND images.image_category = 'general'\n",
" \"\"\"\n",
" elif category == \"assets\":\n",
" base_query += \"\"\"\n",
" AND images.image_category IN ('control', 'mask', 'user', 'other')\n",
" \"\"\"\n",
" else:\n",
" raise ValueError(f\"Invalid category: {category}\")\n",
"\n",
" if search_term:\n",
" base_query += \"\"\"\n",
" AND images.metadata LIKE ?\n",
" \"\"\"\n",
" params.append(f\"%{search_term.lower()}%\")\n",
"\n",
" if from_image_name:\n",
" # Use keyset pagination to get the next page of results\n",
"\n",
" # This uses `<` so that the cursor image is NOT included in the results - only images after it\n",
" if starred_first:\n",
" keyset_query = f\"\"\"\n",
" WITH image_keyset AS (\n",
" SELECT created_at,\n",
" image_name,\n",
" starred\n",
" FROM images\n",
" WHERE image_name = ?\n",
" )\n",
" {base_query}\n",
" AND (images.starred, images.created_at, images.image_name) < ((SELECT starred FROM image_keyset), (SELECT created_at FROM image_keyset), (SELECT image_name FROM image_keyset))\n",
" \"\"\"\n",
" else:\n",
" keyset_query = f\"\"\"\n",
" WITH image_keyset AS (\n",
" SELECT created_at,\n",
" image_name\n",
" FROM images\n",
" WHERE image_name = ?\n",
" )\n",
" {base_query}\n",
" AND (images.created_at, images.image_name) < ((SELECT created_at FROM image_keyset), (SELECT image_name FROM image_keyset))\n",
" \"\"\"\n",
"\n",
" # This uses `<=` so that the cursor image IS included in the results\n",
" # if starred_first:\n",
" # keyset_query = f\"\"\"\n",
" # WITH image_keyset AS (\n",
" # SELECT created_at,\n",
" # image_name,\n",
" # starred\n",
" # FROM images\n",
" # WHERE image_name = ?\n",
" # )\n",
" # {base_query}\n",
" # AND (images.starred, images.created_at, images.image_name) <= ((SELECT starred FROM image_keyset), (SELECT created_at FROM image_keyset), (SELECT image_name FROM image_keyset))\n",
" # \"\"\"\n",
" # else:\n",
" # keyset_query = f\"\"\"\n",
" # WITH image_keyset AS (\n",
" # SELECT created_at,\n",
" # image_name\n",
" # FROM images\n",
" # WHERE image_name = ?\n",
" # )\n",
" # {base_query}\n",
" # AND (images.created_at, images.image_name) <= ((SELECT created_at FROM image_keyset), (SELECT image_name FROM image_keyset))\n",
" # \"\"\"\n",
" base_query = keyset_query\n",
" params.append(from_image_name)\n",
"\n",
" if starred_first:\n",
" order_by_clause = f\"\"\"\n",
" ORDER BY images.starred DESC, images.created_at {order_dir.value}, images.image_name {order_dir.value}\n",
" \"\"\"\n",
" else:\n",
" order_by_clause = f\"\"\"\n",
" ORDER BY images.created_at {order_dir.value}, images.image_name {order_dir.value}\n",
" \"\"\"\n",
"\n",
" final_query = f\"\"\"\n",
" {base_query}\n",
" {order_by_clause}\n",
" LIMIT ?;\n",
" \"\"\"\n",
" params.append(count)\n",
"\n",
" cursor.execute(final_query, tuple(params))\n",
" result = cast(list[sqlite3.Row], cursor.fetchall())\n",
" images = [deserialize_image_record(dict(r)) for r in result]\n",
"\n",
" return images\n",
"\n",
"\n",
"kwargs = {\"starred_first\": False}\n",
"\n",
"images = get_images(**kwargs)\n",
"print(\"first query\")\n",
"for image in images:\n",
" print(image.image_name, image.starred)\n",
"\n",
"print(\"\\nnext query, starting from the second image\")\n",
"images_2 = get_images(from_image_name=images[0].image_name, **kwargs)\n",
"for image in images_2:\n",
" print(image.image_name, image.starred)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.12"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@@ -39,11 +39,11 @@ class WorkflowRecordsStorageBase(ABC):
@abstractmethod
def get_many(
self,
page: int,
per_page: int,
order_by: WorkflowRecordOrderBy,
direction: SQLiteDirection,
page: int,
per_page: Optional[int],
category: Optional[WorkflowCategory],
category: WorkflowCategory,
query: Optional[str],
) -> PaginatedResults[WorkflowRecordListItemDTO]:
"""Gets many workflows."""

View File

@@ -125,11 +125,11 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
def get_many(
self,
page: int,
per_page: int,
order_by: WorkflowRecordOrderBy,
direction: SQLiteDirection,
page: int = 0,
per_page: Optional[int] = None,
category: Optional[WorkflowCategory] = None,
category: WorkflowCategory,
query: Optional[str] = None,
) -> PaginatedResults[WorkflowRecordListItemDTO]:
try:
@@ -137,7 +137,8 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
# sanitize!
assert order_by in WorkflowRecordOrderBy
assert direction in SQLiteDirection
count_query = "SELECT COUNT(*) FROM workflow_library"
assert category in WorkflowCategory
count_query = "SELECT COUNT(*) FROM workflow_library WHERE category = ?"
main_query = """
SELECT
workflow_id,
@@ -148,51 +149,32 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
updated_at,
opened_at
FROM workflow_library
WHERE category = ?
"""
main_params: list[int | str] = []
count_params: list[int | str] = []
if category:
assert category in WorkflowCategory
main_query += " WHERE category = ?"
count_query += " WHERE category = ?"
main_params.append(category.value)
count_params.append(category.value)
main_params: list[int | str] = [category.value]
count_params: list[int | str] = [category.value]
stripped_query = query.strip() if query else None
if stripped_query:
wildcard_query = "%" + stripped_query + "%"
if "WHERE" in main_query:
main_query += " AND (name LIKE ? OR description LIKE ?)"
count_query += " AND (name LIKE ? OR description LIKE ?)"
else:
main_query += " WHERE name LIKE ? OR description LIKE ?"
count_query += " WHERE name LIKE ? OR description LIKE ?"
main_query += " AND name LIKE ? OR description LIKE ? "
count_query += " AND name LIKE ? OR description LIKE ?;"
main_params.extend([wildcard_query, wildcard_query])
count_params.extend([wildcard_query, wildcard_query])
main_query += f" ORDER BY {order_by.value} {direction.value}"
if per_page:
main_query += " LIMIT ? OFFSET ?"
main_params.extend([per_page, page * per_page])
main_query += f" ORDER BY {order_by.value} {direction.value} LIMIT ? OFFSET ?;"
main_params.extend([per_page, page * per_page])
self._cursor.execute(main_query, main_params)
rows = self._cursor.fetchall()
workflows = [WorkflowRecordListItemDTOValidator.validate_python(dict(row)) for row in rows]
self._cursor.execute(count_query, count_params)
total = self._cursor.fetchone()[0]
if per_page:
pages = total // per_page + (total % per_page > 0)
else:
pages = 1 # If no pagination, there is only one page
pages = total // per_page + (total % per_page > 0)
return PaginatedResults(
items=workflows,
page=page,
per_page=per_page if per_page else total,
per_page=per_page,
pages=pages,
total=total,
)

View File

@@ -16,10 +16,7 @@ def attention(q: Tensor, k: Tensor, v: Tensor, pe: Tensor) -> Tensor:
def rope(pos: Tensor, dim: int, theta: int) -> Tensor:
assert dim % 2 == 0
scale = (
torch.arange(0, dim, 2, dtype=torch.float32 if pos.device.type == "mps" else torch.float64, device=pos.device)
/ dim
)
scale = torch.arange(0, dim, 2, dtype=torch.float64, device=pos.device) / dim
omega = 1.0 / (theta**scale)
out = torch.einsum("...n,d->...nd", pos, omega)
out = torch.stack([torch.cos(out), -torch.sin(out), torch.sin(out), torch.cos(out)], dim=-1)

View File

@@ -171,19 +171,8 @@ class StableDiffusionGeneratorPipeline(StableDiffusionPipeline):
"""
if xformers is available, use it, otherwise use sliced attention.
"""
# On 30xx and 40xx series GPUs, `torch-sdp` is faster than `xformers`. This corresponds to a CUDA major
# version of 8 or higher. So, for major version 7 or below, we prefer `xformers`.
# See:
# - https://developer.nvidia.com/cuda-gpus
# - https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#compute-capabilities
try:
prefer_xformers = torch.cuda.is_available() and torch.cuda.get_device_properties("cuda").major <= 7 # type: ignore # Type of "get_device_properties" is partially unknown
except Exception:
prefer_xformers = False
config = get_config()
if config.attention_type == "xformers" and is_xformers_available() and prefer_xformers:
if config.attention_type == "xformers":
self.enable_xformers_memory_efficient_attention()
return
elif config.attention_type == "sliced":
@@ -206,7 +195,7 @@ class StableDiffusionGeneratorPipeline(StableDiffusionPipeline):
# the remainder if this code is called when attention_type=='auto'
if self.unet.device.type == "cuda":
if is_xformers_available() and prefer_xformers:
if is_xformers_available():
self.enable_xformers_memory_efficient_attention()
return
elif hasattr(torch.nn.functional, "scaled_dot_product_attention"):

View File

@@ -58,7 +58,7 @@
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fontsource-variable/inter": "^5.1.0",
"@invoke-ai/ui-library": "^0.0.42",
"@invoke-ai/ui-library": "^0.0.40",
"@nanostores/react": "^0.7.3",
"@reduxjs/toolkit": "2.2.3",
"@roarr/browser-log-writer": "^1.3.0",
@@ -83,7 +83,6 @@
"overlayscrollbars-react": "^0.5.6",
"perfect-freehand": "^1.2.2",
"query-string": "^9.1.0",
"raf-throttle": "^2.0.6",
"react": "^18.3.1",
"react-colorful": "^5.6.1",
"react-dom": "^18.3.1",

View File

@@ -24,8 +24,8 @@ dependencies:
specifier: ^5.1.0
version: 5.1.0
'@invoke-ai/ui-library':
specifier: ^0.0.42
version: 0.0.42(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.1.0)(@types/react@18.3.11)(i18next@23.15.1)(react-dom@18.3.1)(react@18.3.1)
specifier: ^0.0.40
version: 0.0.40(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.1.0)(@types/react@18.3.11)(i18next@23.15.1)(react-dom@18.3.1)(react@18.3.1)
'@nanostores/react':
specifier: ^0.7.3
version: 0.7.3(nanostores@0.11.3)(react@18.3.1)
@@ -98,9 +98,6 @@ dependencies:
query-string:
specifier: ^9.1.0
version: 9.1.0
raf-throttle:
specifier: ^2.0.6
version: 2.0.6
react:
specifier: ^18.3.1
version: 18.3.1
@@ -493,8 +490,8 @@ packages:
resolution: {integrity: sha512-MV6D4VLRIHr4PkW4zMyqfrNS1mPlCTiCXwvYGtDFQYr+xHFfonhAuf9WjsSc0nyp2m0OdkSLnzmVKkZFLo25Tg==}
dev: false
/@chakra-ui/anatomy@2.3.4:
resolution: {integrity: sha512-fFIYN7L276gw0Q7/ikMMlZxP7mvnjRaWJ7f3Jsf9VtDOi6eAYIBRrhQe6+SZ0PGmoOkRaBc7gSE5oeIbgFFyrw==}
/@chakra-ui/anatomy@2.3.3:
resolution: {integrity: sha512-Sy2VAG0WrzkQE40Y0fY406c6AlyqFxAc7j6fDz8Wwotz9veAvm+y5UgFUyhZ6FoYNAjDMPQ7JCcN7OGz74pNlA==}
dev: false
/@chakra-ui/breakpoint-utils@2.0.8:
@@ -551,12 +548,12 @@ packages:
react: 18.3.1
dev: false
/@chakra-ui/hooks@2.4.2(react@18.3.1):
resolution: {integrity: sha512-LRKiVE1oA7afT5tbbSKAy7Uas2xFHE6IkrQdbhWCHmkHBUtPvjQQDgwtnd4IRZPmoEfNGwoJ/MQpwOM/NRTTwA==}
/@chakra-ui/hooks@2.3.3(react@18.3.1):
resolution: {integrity: sha512-nvqQfR+u0qAJ2/mdGF1XTrnfW9WahSsOc62E/xtRm5hPClfkxPCIXDuw0C17lZ2RYfg/hxsYKLJCGnQWZcC/7w==}
peerDependencies:
react: '>=18'
dependencies:
'@chakra-ui/utils': 2.2.2(react@18.3.1)
'@chakra-ui/utils': 2.1.3(react@18.3.1)
'@zag-js/element-size': 0.31.1
copy-to-clipboard: 3.3.3
framesync: 6.1.2
@@ -574,13 +571,13 @@ packages:
react: 18.3.1
dev: false
/@chakra-ui/icons@2.2.4(@chakra-ui/react@2.10.2)(react@18.3.1):
resolution: {integrity: sha512-l5QdBgwrAg3Sc2BRqtNkJpfuLw/pWRDwwT58J6c4PqQT6wzXxyNa8Q0PForu1ltB5qEiFb1kxr/F/HO1EwNa6g==}
/@chakra-ui/icons@2.2.3(@chakra-ui/react@2.9.4)(react@18.3.1):
resolution: {integrity: sha512-BihIvFvAKq+9/U3sI47Vdo3Mmr9VxTvWcFBl3qZsbJSBpqK7GYaakNWADyPvgsCRGo2be72AZgcOAYaAqWDThQ==}
peerDependencies:
'@chakra-ui/react': '>=2.0.0'
react: '>=18'
dependencies:
'@chakra-ui/react': 2.10.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1)
'@chakra-ui/react': 2.9.4(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1)
react: 18.3.1
dev: false
@@ -803,8 +800,8 @@ packages:
react: 18.3.1
dev: false
/@chakra-ui/react@2.10.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-TfIHTqTlxTHYJZBtpiR5EZasPUrLYKJxdbHkdOJb5G1OQ+2c5kKl5XA7c2pMtsEptzb7KxAAIB62t3hxdfWp1w==}
/@chakra-ui/react@2.9.4(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-e7fMItdoUjZsQuVsq4DSvrX/dpmYHEwJD2UM5dkHvR2Vzsrili0EWfXrT9R+4kCDHtc6vlECbHA13RHru7XUUg==}
peerDependencies:
'@emotion/react': '>=11'
'@emotion/styled': '>=11'
@@ -812,10 +809,10 @@ packages:
react: '>=18'
react-dom: '>=18'
dependencies:
'@chakra-ui/hooks': 2.4.2(react@18.3.1)
'@chakra-ui/styled-system': 2.11.2(react@18.3.1)
'@chakra-ui/theme': 3.4.6(@chakra-ui/styled-system@2.11.2)(react@18.3.1)
'@chakra-ui/utils': 2.2.2(react@18.3.1)
'@chakra-ui/hooks': 2.3.3(react@18.3.1)
'@chakra-ui/styled-system': 2.10.3(react@18.3.1)
'@chakra-ui/theme': 3.4.3(@chakra-ui/styled-system@2.10.3)(react@18.3.1)
'@chakra-ui/utils': 2.1.3(react@18.3.1)
'@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1)
'@emotion/styled': 11.13.0(@emotion/react@11.13.3)(@types/react@18.3.11)(react@18.3.1)
'@popperjs/core': 2.11.8
@@ -826,6 +823,7 @@ packages:
react-dom: 18.3.1(react@18.3.1)
react-fast-compare: 3.2.2
react-focus-lock: 2.13.2(@types/react@18.3.11)(react@18.3.1)
react-lorem-component: 0.13.0(react@18.3.1)
react-remove-scroll: 2.6.0(@types/react@18.3.11)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
@@ -846,10 +844,10 @@ packages:
react: 18.3.1
dev: false
/@chakra-ui/styled-system@2.11.2(react@18.3.1):
resolution: {integrity: sha512-y++z2Uop+hjfZX9mbH88F1ikazPv32asD2er56zMJBemUAzweXnHTpiCQbluEDSUDhqmghVZAdb+5L4XLbsRxA==}
/@chakra-ui/styled-system@2.10.3(react@18.3.1):
resolution: {integrity: sha512-rU4sG712pnp3Qrc8XT5AKcMhZjByXp1IrErLJ8wmiez2v8hAl/Dv8roK2BTqd4GfkJOrtkyfq2e2ZcDWjbd9Dw==}
dependencies:
'@chakra-ui/utils': 2.2.2(react@18.3.1)
'@chakra-ui/utils': 2.1.3(react@18.3.1)
csstype: 3.1.3
transitivePeerDependencies:
- react
@@ -893,14 +891,14 @@ packages:
color2k: 2.0.3
dev: false
/@chakra-ui/theme-tools@2.2.6(@chakra-ui/styled-system@2.11.2)(react@18.3.1):
resolution: {integrity: sha512-3UhKPyzKbV3l/bg1iQN9PBvffYp+EBOoYMUaeTUdieQRPFzo2jbYR0lNCxqv8h5aGM/k54nCHU2M/GStyi9F2A==}
/@chakra-ui/theme-tools@2.2.3(@chakra-ui/styled-system@2.10.3)(react@18.3.1):
resolution: {integrity: sha512-9fbBh4YaF8k1puovMnvdZtoVxQd1IKlRvWQBmIzXoae3KSJi9p1znRLzEX+Qjvph15dFCa2Q4h1gynI+HOh8oQ==}
peerDependencies:
'@chakra-ui/styled-system': '>=2.0.0'
dependencies:
'@chakra-ui/anatomy': 2.3.4
'@chakra-ui/styled-system': 2.11.2(react@18.3.1)
'@chakra-ui/utils': 2.2.2(react@18.3.1)
'@chakra-ui/anatomy': 2.3.3
'@chakra-ui/styled-system': 2.10.3(react@18.3.1)
'@chakra-ui/utils': 2.1.3(react@18.3.1)
color2k: 2.0.3
transitivePeerDependencies:
- react
@@ -926,15 +924,15 @@ packages:
'@chakra-ui/theme-tools': 2.1.2(@chakra-ui/styled-system@2.9.2)
dev: false
/@chakra-ui/theme@3.4.6(@chakra-ui/styled-system@2.11.2)(react@18.3.1):
resolution: {integrity: sha512-ZwFBLfiMC3URwaO31ONXoKH9k0TX0OW3UjdPF3EQkQpYyrk/fm36GkkzajjtdpWEd7rzDLRsQjPmvwNaSoNDtg==}
/@chakra-ui/theme@3.4.3(@chakra-ui/styled-system@2.10.3)(react@18.3.1):
resolution: {integrity: sha512-WxGk5wEMr8x/YmR99TfVcnt+qsHt9qy5FJycPgcKoL8blQiZ+v/rLhdWhXvu8K03DyfAoLkQDh2guVl+wKFfHA==}
peerDependencies:
'@chakra-ui/styled-system': '>=2.8.0'
dependencies:
'@chakra-ui/anatomy': 2.3.4
'@chakra-ui/styled-system': 2.11.2(react@18.3.1)
'@chakra-ui/theme-tools': 2.2.6(@chakra-ui/styled-system@2.11.2)(react@18.3.1)
'@chakra-ui/utils': 2.2.2(react@18.3.1)
'@chakra-ui/anatomy': 2.3.3
'@chakra-ui/styled-system': 2.10.3(react@18.3.1)
'@chakra-ui/theme-tools': 2.2.3(@chakra-ui/styled-system@2.10.3)(react@18.3.1)
'@chakra-ui/utils': 2.1.3(react@18.3.1)
transitivePeerDependencies:
- react
dev: false
@@ -959,8 +957,8 @@ packages:
lodash.mergewith: 4.6.2
dev: false
/@chakra-ui/utils@2.2.2(react@18.3.1):
resolution: {integrity: sha512-jUPLT0JzRMWxpdzH6c+t0YMJYrvc5CLericgITV3zDSXblkfx3DsYXqU11DJTSGZI9dUKzM1Wd0Wswn4eJwvFQ==}
/@chakra-ui/utils@2.1.3(react@18.3.1):
resolution: {integrity: sha512-qIuyEg1ThVrUAnkV5nOngMDxUVCKavC04LfuraOCS1PHU4zhU4urJC2FURriALIQSgy6LpegASjvRzi7CIDDQQ==}
peerDependencies:
react: '>=16.8.0'
dependencies:
@@ -1696,18 +1694,18 @@ packages:
prettier: 3.3.3
dev: true
/@invoke-ai/ui-library@0.0.42(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.1.0)(@types/react@18.3.11)(i18next@23.15.1)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-OuDXRipBO5mu+Nv4qN8cd8MiwiGBdq6h4PirVgPI9/ltbdcIzePgUJ0dJns26lflHSTRWW38I16wl4YTw3mNWA==}
/@invoke-ai/ui-library@0.0.40(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.1.0)(@types/react@18.3.11)(i18next@23.15.1)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-GoqihMV1uaHPRgJ/GAmtt5+0ES1S3YpWUAkXAdRFqRWBoMs7i6mWddAY+qB9r5dWUR+LTESrGLKADHJBYjtVEQ==}
peerDependencies:
'@fontsource-variable/inter': ^5.0.16
react: ^18.2.0
react-dom: ^18.2.0
dependencies:
'@chakra-ui/anatomy': 2.2.2
'@chakra-ui/icons': 2.2.4(@chakra-ui/react@2.10.2)(react@18.3.1)
'@chakra-ui/icons': 2.2.3(@chakra-ui/react@2.9.4)(react@18.3.1)
'@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1)
'@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1)
'@chakra-ui/react': 2.10.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1)
'@chakra-ui/react': 2.9.4(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1)
'@chakra-ui/styled-system': 2.9.2
'@chakra-ui/theme-tools': 2.1.2(@chakra-ui/styled-system@2.9.2)
'@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1)
@@ -4690,6 +4688,13 @@ packages:
yaml: 1.10.2
dev: false
/create-react-class@15.7.0:
resolution: {integrity: sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==}
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
dev: false
/cross-fetch@4.0.0:
resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
dependencies:
@@ -6808,6 +6813,13 @@ packages:
dependencies:
js-tokens: 4.0.0
/lorem-ipsum@1.0.6:
resolution: {integrity: sha512-Rx4XH8X4KSDCKAVvWGYlhAfNqdUP5ZdT4rRyf0jjrvWgtViZimDIlopWNfn/y3lGM5K4uuiAoY28TaD+7YKFrQ==}
hasBin: true
dependencies:
minimist: 1.2.8
dev: false
/loupe@2.3.7:
resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
dependencies:
@@ -7001,7 +7013,6 @@ packages:
/minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
dev: true
/minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
@@ -7557,10 +7568,6 @@ packages:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true
/raf-throttle@2.0.6:
resolution: {integrity: sha512-C7W6hy78A+vMmk5a/B6C5szjBHrUzWJkVyakjKCK59Uy2CcA7KhO1JUvvH32IXYFIcyJ3FMKP3ZzCc2/71I6Vg==}
dev: false
/railroad-diagrams@1.0.0:
resolution: {integrity: sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==}
dev: false
@@ -7760,6 +7767,18 @@ packages:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
dev: true
/react-lorem-component@0.13.0(react@18.3.1):
resolution: {integrity: sha512-4mWjxmcG/DJJwdxdKwXWyP2N9zohbJg/yYaC+7JffQNrKj3LYDpA/A4u/Dju1v1ZF6Jew2gbFKGb5Z6CL+UNTw==}
peerDependencies:
react: 16.x
dependencies:
create-react-class: 15.7.0
lorem-ipsum: 1.0.6
object-assign: 4.1.1
react: 18.3.1
seedable-random: 0.0.1
dev: false
/react-redux@9.1.2(@types/react@18.3.11)(react@18.3.1)(redux@5.0.1):
resolution: {integrity: sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==}
peerDependencies:
@@ -8274,6 +8293,10 @@ packages:
engines: {node: '>=0.10.0'}
dev: false
/seedable-random@0.0.1:
resolution: {integrity: sha512-uZWbEfz3BQdBl4QlUPELPqhInGEO1Q6zjzqrTDkd3j7mHaWWJo7h4ydr2g24a2WtTLk3imTLc8mPbBdQqdsbGw==}
dev: false
/semver-compare@1.0.0:
resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==}
dev: false

View File

@@ -651,8 +651,7 @@
"imageCopied": "Bild kopiert",
"parametersNotSet": "Parameter nicht festgelegt",
"addedToBoard": "Dem Board hinzugefügt",
"loadedWithWarnings": "Workflow mit Warnungen geladen",
"imageSaved": "Bild gespeichert"
"loadedWithWarnings": "Workflow mit Warnungen geladen"
},
"accessibility": {
"uploadImage": "Bild hochladen",
@@ -665,9 +664,7 @@
"resetUI": "$t(accessibility.reset) von UI",
"createIssue": "Ticket erstellen",
"about": "Über",
"submitSupportTicket": "Support-Ticket senden",
"toggleRightPanel": "Rechtes Bedienfeld umschalten (G)",
"toggleLeftPanel": "Linkes Bedienfeld umschalten (T)"
"submitSupportTicket": "Support-Ticket senden"
},
"boards": {
"autoAddBoard": "Board automatisch erstellen",
@@ -1065,22 +1062,7 @@
"missingFieldTemplate": "Fehlende Feldvorlage",
"missingNode": "Fehlender Aufrufknoten",
"missingInvocationTemplate": "Fehlende Aufrufvorlage",
"edit": "Bearbeiten",
"workflowAuthor": "Autor",
"graph": "Graph",
"workflowDescription": "Kurze Beschreibung",
"versionUnknown": " Version unbekannt",
"workflow": "Arbeitsablauf",
"noGraph": "Kein Graph",
"version": "Version",
"zoomInNodes": "Hineinzoomen",
"zoomOutNodes": "Herauszoomen",
"workflowName": "Name",
"unknownNode": "Unbekannter Knoten",
"workflowContact": "Kontaktdaten",
"workflowNotes": "Notizen",
"workflowTags": "Tags",
"workflowVersion": "Version"
"edit": "Bearbeiten"
},
"hrf": {
"enableHrf": "Korrektur für hohe Auflösungen",

View File

@@ -90,7 +90,6 @@
"batch": "Batch Manager",
"beta": "Beta",
"cancel": "Cancel",
"close": "Close",
"copy": "Copy",
"copyError": "$t(gallery.copy) Error",
"on": "On",
@@ -282,7 +281,6 @@
"gallery": "Gallery",
"alwaysShowImageSizeBadge": "Always Show Image Size Badge",
"assets": "Assets",
"assetsTab": "Files youve uploaded for use in your projects.",
"autoAssignBoardOnClick": "Auto-Assign Board on Click",
"autoSwitchNewImages": "Auto-Switch to New Images",
"copy": "Copy",
@@ -303,7 +301,6 @@
"gallerySettings": "Gallery Settings",
"go": "Go",
"image": "image",
"imagesTab": "Images youve created and saved within Invoke.",
"jump": "Jump",
"loading": "Loading",
"newestFirst": "Newest First",
@@ -702,7 +699,7 @@
"convert": "Convert",
"convertingModelBegin": "Converting Model. Please wait.",
"convertToDiffusers": "Convert To Diffusers",
"convertToDiffusersHelpText1": "This model will be converted to the 🧨 Diffusers format.",
"convertToDiffusersHelpText1": "This model will be converted to the \ud83e\udde8 Diffusers format.",
"convertToDiffusersHelpText2": "This process will replace your Model Manager entry with the Diffusers version of the same model.",
"convertToDiffusersHelpText3": "Your checkpoint file on disk WILL be deleted if it is in InvokeAI root folder. If it is in a custom location, then it WILL NOT be deleted.",
"convertToDiffusersHelpText4": "This is a one time process only. It might take around 30s-60s depending on the specifications of your computer.",
@@ -857,8 +854,6 @@
"ipAdapter": "IP-Adapter",
"loadingNodes": "Loading Nodes...",
"loadWorkflow": "Load Workflow",
"noWorkflows": "No Workflows",
"noMatchingWorkflows": "No Matching Workflows",
"noWorkflow": "No Workflow",
"mismatchedVersion": "Invalid node: node {{node}} of type {{type}} has mismatched version (try updating?)",
"missingTemplate": "Invalid node: node {{node}} of type {{type}} missing template (not installed?)",
@@ -875,7 +870,6 @@
"nodeType": "Node Type",
"noFieldsLinearview": "No fields added to Linear View",
"noFieldsViewMode": "This workflow has no selected fields to display. View the full workflow to configure values.",
"workflowHelpText": "Need Help? Check out our guide to <LinkComponent>Getting Started with Workflows</LinkComponent>.",
"noNodeSelected": "No node selected",
"nodeOpacity": "Node Opacity",
"nodeVersion": "Node Version",
@@ -1126,7 +1120,6 @@
"canceled": "Processing Canceled",
"connected": "Connected to Server",
"imageCopied": "Image Copied",
"linkCopied": "Link Copied",
"unableToLoadImage": "Unable to Load Image",
"unableToLoadImageMetadata": "Unable to Load Image Metadata",
"unableToLoadStylePreset": "Unable to Load Style Preset",
@@ -1523,7 +1516,6 @@
}
},
"workflows": {
"chooseWorkflowFromLibrary": "Choose Workflow from Library",
"defaultWorkflows": "Default Workflows",
"userWorkflows": "User Workflows",
"projectWorkflows": "Project Workflows",
@@ -1536,9 +1528,7 @@
"openWorkflow": "Open Workflow",
"updated": "Updated",
"uploadWorkflow": "Load from File",
"uploadAndSaveWorkflow": "Upload to Library",
"deleteWorkflow": "Delete Workflow",
"deleteWorkflow2": "Are you sure you want to delete this workflow? This cannot be undone.",
"unnamedWorkflow": "Unnamed Workflow",
"downloadWorkflow": "Save to File",
"saveWorkflow": "Save Workflow",
@@ -1561,13 +1551,9 @@
"loadFromGraph": "Load Workflow from Graph",
"convertGraph": "Convert Graph",
"loadWorkflow": "$t(common.load) Workflow",
"autoLayout": "Auto Layout",
"edit": "Edit",
"download": "Download",
"copyShareLink": "Copy Share Link",
"copyShareLinkForWorkflow": "Copy Share Link for Workflow",
"delete": "Delete"
"autoLayout": "Auto Layout"
},
"app": {},
"controlLayers": {
"regional": "Regional",
"global": "Global",
@@ -1646,14 +1632,14 @@
"viewProgressInViewer": "View progress and outputs in the <Btn>Image Viewer</Btn>.",
"viewProgressOnCanvas": "View progress and stage outputs on the <Btn>Canvas</Btn>.",
"rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)",
"rasterLayer_withCount_other": "Raster Layers",
"controlLayer_withCount_one": "$t(controlLayers.controlLayer)",
"controlLayer_withCount_other": "Control Layers",
"inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)",
"inpaintMask_withCount_other": "Inpaint Masks",
"regionalGuidance_withCount_one": "$t(controlLayers.regionalGuidance)",
"regionalGuidance_withCount_other": "Regional Guidance",
"globalReferenceImage_withCount_one": "$t(controlLayers.globalReferenceImage)",
"rasterLayer_withCount_other": "Raster Layers",
"controlLayer_withCount_other": "Control Layers",
"inpaintMask_withCount_other": "Inpaint Masks",
"regionalGuidance_withCount_other": "Regional Guidance",
"globalReferenceImage_withCount_other": "Global Reference Images",
"opacity": "Opacity",
"regionalGuidance_withCount_hidden": "Regional Guidance ({{count}} hidden)",
@@ -1666,6 +1652,7 @@
"rasterLayers_withCount_visible": "Raster Layers ({{count}})",
"globalReferenceImages_withCount_visible": "Global Reference Images ({{count}})",
"inpaintMasks_withCount_visible": "Inpaint Masks ({{count}})",
"layer": "Layer",
"layer_one": "Layer",
"layer_other": "Layers",
"layer_withCount_one": "Layer ({{count}})",
@@ -1816,10 +1803,6 @@
"transform": {
"transform": "Transform",
"fitToBbox": "Fit to Bbox",
"fitMode": "Fit Mode",
"fitModeContain": "Contain",
"fitModeCover": "Cover",
"fitModeFill": "Fill",
"reset": "Reset",
"apply": "Apply",
"cancel": "Cancel"
@@ -1986,7 +1969,7 @@
}
},
"newUserExperience": {
"toGetStarted": "To get started, enter a prompt in the box and click <StrongComponent>Invoke</StrongComponent> to generate your first image. Select a prompt template to improve results. You can choose to save your images directly to the <StrongComponent>Gallery</StrongComponent> or edit them to the <StrongComponent>Canvas</StrongComponent>.",
"toGetStarted": "To get started, enter a prompt in the box and click <StrongComponent>Invoke</StrongComponent> to generate your first image. You can choose to save your images directly to the <StrongComponent>Gallery</StrongComponent> or edit them to the <StrongComponent>Canvas</StrongComponent>.",
"gettingStartedSeries": "Want more guidance? Check out our <LinkComponent>Getting Started Series</LinkComponent> for tips on unlocking the full potential of the Invoke Studio."
},
"whatsNew": {

View File

@@ -667,8 +667,7 @@
"paramVAEPrecision": {
"heading": "Précision du VAE",
"paragraphs": [
"La précision utilisée lors de l'encodage et du décodage VAE.",
"La pr'ecision Fp16/Half est plus efficace, au détriment de légères variations d'image."
"La précision utilisée lors de l'encodage et du décodage VAE."
]
},
"controlNetWeight": {
@@ -839,15 +838,11 @@
"paramCFGScale": {
"heading": "Échelle CFG",
"paragraphs": [
"Contrôle de l'influence du prompt sur le processus de génération.",
"Des valeurs élevées de l'échelle CFG peuvent entraîner une saturation excessive et des distortions. "
"Contrôle de l'influence du prompt sur le processus de génération."
]
},
"loraWeight": {
"heading": "Poids",
"paragraphs": [
"Poids du LoRA. Un poids plus élevé aura un impact plus important sur l'image finale."
]
"heading": "Poids"
},
"imageFit": {
"heading": "Ajuster l'image initiale à la taille de sortie",
@@ -858,8 +853,7 @@
"paramCFGRescaleMultiplier": {
"heading": "Multiplicateur de mise à l'échelle CFG",
"paragraphs": [
"Multiplicateur de mise à l'échelle pour le guidage CFG, utilisé pour les modèles entraînés en utilisant le zero-terminal SNR (ztsnr).",
"Une valeur de 0.7 est suggérée pour ces modèles."
"Multiplicateur de mise à l'échelle pour le guidage CFG, utilisé pour les modèles entraînés en utilisant le zero-terminal SNR (ztsnr)."
]
},
"controlNetProcessor": {
@@ -867,66 +861,6 @@
"paragraphs": [
"Méthode de traitement de l'image d'entrée pour guider le processus de génération. Différents processeurs fourniront différents effets ou styles dans vos images générées."
]
},
"paramUpscaleMethod": {
"paragraphs": [
"Méthode utilisée pour améliorer l'image pour la correction de haute résolution."
],
"heading": "Méthode d'agrandissement"
},
"refinerModel": {
"heading": "Modèle de Raffinage",
"paragraphs": [
"Modèle utilisé pendant la partie raffinage du processus de génération.",
"Similaire au Modèle de Génération."
]
},
"paramWidth": {
"paragraphs": [
"Largeur de l'image générée. Doit être un multiple de 8."
],
"heading": "Largeur"
},
"paramHeight": {
"heading": "Hauteur",
"paragraphs": [
"Hauteur de l'image générée. Doit être un multiple de 8."
]
},
"paramHrf": {
"heading": "Activer la correction haute résolution",
"paragraphs": [
"Générez des images de haute qualité à une résolution plus grande que celle qui est optimale pour le modèle. Cela est généralement utilisé pour prévenir la duplication dans l'image générée."
]
},
"patchmatchDownScaleSize": {
"paragraphs": [
"Intensité du sous-échantillonage qui se produit avant le remplissage?",
"Un sous-échantillonage plus élevé améliorera les performances et réduira la qualité."
],
"heading": "Sous-échantillonage"
},
"paramAspect": {
"paragraphs": [
"Rapport hauteur/largeur de l'image générée. Changer le rapport mettra à jour la largeur et la hauteur en conséquence.",
"\"Optimiser\" définira la largeur et la hauteur aux dimensions optimales pour le modèle choisi."
],
"heading": "Aspect"
},
"refinerScheduler": {
"heading": "Planificateur"
},
"refinerPositiveAestheticScore": {
"paragraphs": [
"Ajoute un biais envers les générations pour qu'elles soient plus similaires aux images ayant un score esthétique élevé, en fonction des données d'entraînement."
],
"heading": "Score Esthétique Positif"
},
"refinerNegativeAestheticScore": {
"heading": "Score Esthétique Négatif",
"paragraphs": [
"Ajoute un biais envers les générations pour qu'elles soient plus similaires aux images ayant un faible score esthétique, en fonction des données d'entraînement."
]
}
},
"dynamicPrompts": {

View File

@@ -65,7 +65,7 @@
"blue": "Blu",
"alpha": "Alfa",
"copy": "Copia",
"on": "Acceso",
"on": "Attivato",
"checkpoint": "Checkpoint",
"safetensors": "Safetensors",
"ai": "ia",
@@ -85,7 +85,7 @@
"openInViewer": "Apri nel visualizzatore",
"apply": "Applica",
"loadingImage": "Caricamento immagine",
"off": "Spento",
"off": "Disattivato",
"edit": "Modifica",
"placeholderSelectAModel": "Seleziona un modello",
"reset": "Reimposta",
@@ -155,9 +155,7 @@
"move": "Sposta",
"gallery": "Galleria",
"openViewer": "Apri visualizzatore",
"closeViewer": "Chiudi visualizzatore",
"imagesTab": "Immagini create e salvate in Invoke.",
"assetsTab": "File che hai caricato per usarli nei tuoi progetti."
"closeViewer": "Chiudi visualizzatore"
},
"hotkeys": {
"searchHotkeys": "Cerca tasti di scelta rapida",
@@ -323,22 +321,6 @@
"selectViewTool": {
"title": "Strumento Visualizza",
"desc": "Seleziona lo strumento Visualizza."
},
"applyFilter": {
"title": "Applica filtro",
"desc": "Applica il filtro in sospeso al livello selezionato."
},
"cancelFilter": {
"title": "Annulla filtro",
"desc": "Annulla il filtro in sospeso."
},
"cancelTransform": {
"desc": "Annulla la trasformazione in sospeso.",
"title": "Annulla Trasforma"
},
"applyTransform": {
"title": "Applica trasformazione",
"desc": "Applica la trasformazione in sospeso al livello selezionato."
}
},
"workflows": {
@@ -592,8 +574,8 @@
"scale": "Scala",
"imageFit": "Adatta l'immagine iniziale alle dimensioni di output",
"scaleBeforeProcessing": "Scala prima dell'elaborazione",
"scaledWidth": "Larghezza scalata",
"scaledHeight": "Altezza scalata",
"scaledWidth": "Larghezza ridimensionata",
"scaledHeight": "Altezza ridimensionata",
"infillMethod": "Metodo di riempimento",
"tileSize": "Dimensione piastrella",
"downloadImage": "Scarica l'immagine",
@@ -635,11 +617,7 @@
"ipAdapterIncompatibleBaseModel": "Il modello base dell'adattatore IP non è compatibile",
"ipAdapterNoImageSelected": "Nessuna immagine dell'adattatore IP selezionata",
"rgNoPromptsOrIPAdapters": "Nessun prompt o adattatore IP",
"rgNoRegion": "Nessuna regione selezionata",
"t2iAdapterIncompatibleBboxWidth": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, larghezza riquadro è {{width}}",
"t2iAdapterIncompatibleBboxHeight": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, altezza riquadro è {{height}}",
"t2iAdapterIncompatibleScaledBboxWidth": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, larghezza del riquadro scalato {{width}}",
"t2iAdapterIncompatibleScaledBboxHeight": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, altezza del riquadro scalato {{height}}"
"rgNoRegion": "Nessuna regione selezionata"
},
"fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), altezza riquadro è {{height}}",
"fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), larghezza riquadro è {{width}}",
@@ -647,11 +625,7 @@
"fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), altezza del riquadro scalato è {{height}}",
"noT5EncoderModelSelected": "Nessun modello di encoder T5 selezionato per la generazione con FLUX",
"noCLIPEmbedModelSelected": "Nessun modello CLIP Embed selezionato per la generazione con FLUX",
"noFLUXVAEModelSelected": "Nessun modello VAE selezionato per la generazione con FLUX",
"canvasIsTransforming": "La tela sta trasformando",
"canvasIsRasterizing": "La tela sta rasterizzando",
"canvasIsCompositing": "La tela è in fase di composizione",
"canvasIsFiltering": "La tela sta filtrando"
"noFLUXVAEModelSelected": "Nessun modello VAE selezionato per la generazione con FLUX"
},
"useCpuNoise": "Usa la CPU per generare rumore",
"iterations": "Iterazioni",
@@ -670,12 +644,7 @@
"processImage": "Elabora Immagine",
"sendToUpscale": "Invia a Amplia",
"postProcessing": "Post-elaborazione (Shift + U)",
"guidance": "Guida",
"gaussianBlur": "Sfocatura Gaussiana",
"boxBlur": "Sfocatura Box",
"staged": "Maschera espansa",
"optimizedImageToImage": "Immagine-a-immagine ottimizzata",
"sendToCanvas": "Invia alla Tela"
"guidance": "Guida"
},
"settings": {
"models": "Modelli",
@@ -709,8 +678,7 @@
"enableInformationalPopovers": "Abilita testo informativo a comparsa",
"reloadingIn": "Ricaricando in",
"informationalPopoversDisabled": "Testo informativo a comparsa disabilitato",
"informationalPopoversDisabledDesc": "I testi informativi a comparsa sono disabilitati. Attivali nelle impostazioni.",
"confirmOnNewSession": "Conferma su nuova sessione"
"informationalPopoversDisabledDesc": "I testi informativi a comparsa sono disabilitati. Attivali nelle impostazioni."
},
"toast": {
"uploadFailed": "Caricamento fallito",
@@ -753,20 +721,7 @@
"somethingWentWrong": "Qualcosa è andato storto",
"outOfMemoryErrorDesc": "Le impostazioni della generazione attuale superano la capacità del sistema. Modifica le impostazioni e riprova.",
"importFailed": "Importazione non riuscita",
"importSuccessful": "Importazione riuscita",
"layerSavedToAssets": "Livello salvato nelle risorse",
"problemSavingLayer": "Impossibile salvare il livello",
"unableToLoadImage": "Impossibile caricare l'immagine",
"problemCopyingLayer": "Impossibile copiare il livello",
"sentToCanvas": "Inviato alla Tela",
"sentToUpscale": "Inviato a Amplia",
"unableToLoadStylePreset": "Impossibile caricare lo stile predefinito",
"stylePresetLoaded": "Stile predefinito caricato",
"unableToLoadImageMetadata": "Impossibile caricare i metadati dell'immagine",
"imageSaved": "Immagine salvata",
"imageSavingFailed": "Salvataggio dell'immagine non riuscito",
"layerCopiedToClipboard": "Livello copiato negli appunti",
"imageNotLoadedDesc": "Impossibile trovare l'immagine"
"importSuccessful": "Importazione riuscita"
},
"accessibility": {
"invokeProgressBar": "Barra di avanzamento generazione",
@@ -779,9 +734,7 @@
"resetUI": "$t(accessibility.reset) l'Interfaccia Utente",
"createIssue": "Segnala un problema",
"about": "Informazioni",
"submitSupportTicket": "Invia ticket di supporto",
"toggleLeftPanel": "Attiva/disattiva il pannello sinistro (T)",
"toggleRightPanel": "Attiva/disattiva il pannello destro (G)"
"submitSupportTicket": "Invia ticket di supporto"
},
"nodes": {
"zoomOutNodes": "Rimpicciolire",
@@ -901,7 +854,7 @@
"clearWorkflowDesc": "Cancellare questo flusso di lavoro e avviarne uno nuovo?",
"clearWorkflow": "Cancella il flusso di lavoro",
"clearWorkflowDesc2": "Il tuo flusso di lavoro attuale presenta modifiche non salvate.",
"viewMode": "Usa la vista lineare",
"viewMode": "Utilizzare nella vista lineare",
"reorderLinearView": "Riordina la vista lineare",
"editMode": "Modifica nell'editor del flusso di lavoro",
"resetToDefaultValue": "Ripristina il valore predefinito",
@@ -919,10 +872,7 @@
"imageAccessError": "Impossibile trovare l'immagine {{image_name}}, ripristino ai valori predefiniti",
"boardAccessError": "Impossibile trovare la bacheca {{board_id}}, ripristino ai valori predefiniti",
"modelAccessError": "Impossibile trovare il modello {{key}}, ripristino ai valori predefiniti",
"saveToGallery": "Salva nella Galleria",
"noMatchingWorkflows": "Nessun flusso di lavoro corrispondente",
"noWorkflows": "Nessun flusso di lavoro",
"workflowHelpText": "Hai bisogno di aiuto? Consulta la nostra guida <LinkComponent>Introduzione ai flussi di lavoro</LinkComponent>"
"saveToGallery": "Salva nella Galleria"
},
"boards": {
"autoAddBoard": "Aggiungi automaticamente bacheca",
@@ -966,8 +916,7 @@
"noBoards": "Nessuna bacheca {{boardType}}",
"hideBoards": "Nascondi bacheche",
"viewBoards": "Visualizza bacheche",
"deletedPrivateBoardsCannotbeRestored": "Le bacheche cancellate non possono essere ripristinate. Selezionando 'Cancella solo bacheca', le immagini verranno spostate nella bacheca \"Non categorizzato\" privata dell'autore dell'immagine.",
"updateBoardError": "Errore durante l'aggiornamento della bacheca"
"deletedPrivateBoardsCannotbeRestored": "Le bacheche cancellate non possono essere ripristinate. Selezionando 'Cancella solo bacheca', le immagini verranno spostate nella bacheca \"Non categorizzato\" privata dell'autore dell'immagine."
},
"queue": {
"queueFront": "Aggiungi all'inizio della coda",
@@ -1452,25 +1401,6 @@
"paragraphs": [
"La struttura determina quanto l'immagine finale rispecchierà il layout dell'originale. Una struttura bassa permette cambiamenti significativi, mentre una struttura alta conserva la composizione e il layout originali."
]
},
"fluxDevLicense": {
"heading": "Licenza non commerciale",
"paragraphs": [
"I modelli FLUX.1 [dev] sono concessi in licenza con la licenza non commerciale FLUX [dev]. Per utilizzare questo tipo di modello per scopi commerciali in Invoke, visita il nostro sito Web per saperne di più."
]
},
"optimizedDenoising": {
"heading": "Immagine-a-immagine ottimizzata",
"paragraphs": [
"Abilita 'Immagine-a-immagine ottimizzata' per una scala di riduzione del rumore più graduale per le trasformazioni da immagine a immagine e di inpainting con modelli Flux. Questa impostazione migliora la capacità di controllare la quantità di modifica applicata a un'immagine, ma può essere disattivata se preferisci usare la scala di riduzione rumore standard. Questa impostazione è ancora in fase di messa a punto ed è in stato beta."
]
},
"paramGuidance": {
"heading": "Guida",
"paragraphs": [
"Controlla quanto il prompt influenza il processo di generazione.",
"Valori di guida elevati possono causare sovrasaturazione e una guida elevata o bassa può causare risultati di generazione distorti. La guida si applica solo ai modelli FLUX DEV."
]
}
},
"sdxl": {
@@ -1564,13 +1494,7 @@
"convertGraph": "Converti grafico",
"loadWorkflow": "$t(common.load) Flusso di lavoro",
"autoLayout": "Disposizione automatica",
"loadFromGraph": "Carica il flusso di lavoro dal grafico",
"userWorkflows": "Flussi di lavoro utente",
"projectWorkflows": "Flussi di lavoro del progetto",
"defaultWorkflows": "Flussi di lavoro predefiniti",
"uploadAndSaveWorkflow": "Carica nella libreria",
"chooseWorkflowFromLibrary": "Scegli il flusso di lavoro dalla libreria",
"deleteWorkflow2": "Vuoi davvero eliminare questo flusso di lavoro? Questa operazione non può essere annullata."
"loadFromGraph": "Carica il flusso di lavoro dal grafico"
},
"accordions": {
"compositing": {
@@ -1609,302 +1533,7 @@
"addPositivePrompt": "Aggiungi $t(controlLayers.prompt)",
"addNegativePrompt": "Aggiungi $t(controlLayers.negativePrompt)",
"regionalGuidance": "Guida regionale",
"opacity": "Opacità",
"mergeVisible": "Fondi il visibile",
"mergeVisibleOk": "Livelli visibili uniti",
"deleteReferenceImage": "Elimina l'immagine di riferimento",
"referenceImage": "Immagine di riferimento",
"fitBboxToLayers": "Adatta il riquadro di delimitazione ai livelli",
"mergeVisibleError": "Errore durante l'unione dei livelli visibili",
"regionalReferenceImage": "Immagine di riferimento Regionale",
"newLayerFromImage": "Nuovo livello da immagine",
"newCanvasFromImage": "Nuova tela da immagine",
"globalReferenceImage": "Immagine di riferimento Globale",
"copyToClipboard": "Copia negli appunti",
"sendingToCanvas": "Effettua le generazioni nella Tela",
"clearHistory": "Cancella la cronologia",
"inpaintMask": "Maschera Inpaint",
"sendToGallery": "Invia alla Galleria",
"controlLayer": "Livello di Controllo",
"rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)",
"rasterLayer_withCount_many": "Livelli Raster",
"rasterLayer_withCount_other": "Livelli Raster",
"controlLayer_withCount_one": "$t(controlLayers.controlLayer)",
"controlLayer_withCount_many": "Livelli di controllo",
"controlLayer_withCount_other": "Livelli di controllo",
"clipToBbox": "Ritaglia i tratti al riquadro",
"duplicate": "Duplica",
"width": "Larghezza",
"addControlLayer": "Aggiungi $t(controlLayers.controlLayer)",
"addInpaintMask": "Aggiungi $t(controlLayers.inpaintMask)",
"addRegionalGuidance": "Aggiungi $t(controlLayers.regionalGuidance)",
"sendToCanvasDesc": "Premendo Invoke il lavoro in corso viene visualizzato sulla tela.",
"addRasterLayer": "Aggiungi $t(controlLayers.rasterLayer)",
"clearCaches": "Svuota le cache",
"regionIsEmpty": "La regione selezionata è vuota",
"recalculateRects": "Ricalcola rettangoli",
"removeBookmark": "Rimuovi segnalibro",
"saveCanvasToGallery": "Salva la tela nella Galleria",
"regional": "Regionale",
"global": "Globale",
"canvas": "Tela",
"bookmark": "Segnalibro per cambio rapido",
"newRegionalReferenceImageOk": "Immagine di riferimento regionale creata",
"newRegionalReferenceImageError": "Problema nella creazione dell'immagine di riferimento regionale",
"newControlLayerOk": "Livello di controllo creato",
"bboxOverlay": "Mostra sovrapposizione riquadro",
"resetCanvas": "Reimposta la tela",
"outputOnlyMaskedRegions": "Solo regioni mascherate in uscita",
"enableAutoNegative": "Abilita Auto Negativo",
"disableAutoNegative": "Disabilita Auto Negativo",
"showHUD": "Mostra HUD",
"maskFill": "Riempimento maschera",
"addReferenceImage": "Aggiungi $t(controlLayers.referenceImage)",
"addGlobalReferenceImage": "Aggiungi $t(controlLayers.globalReferenceImage)",
"sendingToGallery": "Inviare generazioni alla Galleria",
"sendToGalleryDesc": "Premendo Invoke viene generata e salvata un'immagine unica nella tua galleria.",
"sendToCanvas": "Invia alla Tela",
"viewProgressInViewer": "Visualizza i progressi e i risultati nel <Btn>Visualizzatore immagini</Btn>.",
"viewProgressOnCanvas": "Visualizza i progressi e i risultati nella <Btn>Tela</Btn>.",
"saveBboxToGallery": "Salva il riquadro di delimitazione nella Galleria",
"cropLayerToBbox": "Ritaglia il livello al riquadro di delimitazione",
"savedToGalleryError": "Errore durante il salvataggio nella galleria",
"rasterLayer": "Livello Raster",
"regionalGuidance_withCount_one": "$t(controlLayers.regionalGuidance)",
"regionalGuidance_withCount_many": "Guide regionali",
"regionalGuidance_withCount_other": "Guide regionali",
"inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)",
"inpaintMask_withCount_many": "Maschere Inpaint",
"inpaintMask_withCount_other": "Maschere Inpaint",
"savedToGalleryOk": "Salvato nella Galleria",
"newGlobalReferenceImageOk": "Immagine di riferimento globale creata",
"newGlobalReferenceImageError": "Problema nella creazione dell'immagine di riferimento globale",
"newControlLayerError": "Problema nella creazione del livello di controllo",
"newRasterLayerOk": "Livello raster creato",
"newRasterLayerError": "Problema nella creazione del livello raster",
"saveLayerToAssets": "Salva il livello nelle Risorse",
"pullBboxIntoLayerError": "Problema nel caricare il riquadro nel livello",
"pullBboxIntoReferenceImageOk": "Contenuto del riquadro inserito nell'immagine di riferimento",
"pullBboxIntoLayerOk": "Riquadro caricato nel livello",
"pullBboxIntoReferenceImageError": "Problema nell'inserimento del contenuto del riquadro nell'immagine di riferimento",
"globalReferenceImage_withCount_one": "$t(controlLayers.globalReferenceImage)",
"globalReferenceImage_withCount_many": "Immagini di riferimento Globali",
"globalReferenceImage_withCount_other": "Immagini di riferimento Globali",
"controlMode": {
"balanced": "Bilanciato",
"controlMode": "Modalità di controllo",
"prompt": "Prompt",
"control": "Controllo",
"megaControl": "Mega Controllo"
},
"negativePrompt": "Prompt Negativo",
"prompt": "Prompt Positivo",
"beginEndStepPercentShort": "Inizio/Fine %",
"stagingOnCanvas": "Genera immagini nella",
"ipAdapterMethod": {
"full": "Completo",
"style": "Solo Stile",
"composition": "Solo Composizione",
"ipAdapterMethod": "Metodo Adattatore IP"
},
"showingType": "Mostrare {{type}}",
"dynamicGrid": "Griglia dinamica",
"tool": {
"view": "Muovi",
"colorPicker": "Selettore Colore",
"rectangle": "Rettangolo",
"bbox": "Riquadro di delimitazione",
"move": "Sposta",
"brush": "Pennello",
"eraser": "Cancellino"
},
"filter": {
"apply": "Applica",
"reset": "Reimposta",
"process": "Elabora",
"cancel": "Annulla",
"autoProcess": "Processo automatico",
"filterType": "Tipo Filtro",
"filter": "Filtro",
"filters": "Filtri",
"mlsd_detection": {
"score_threshold": "Soglia di punteggio",
"distance_threshold": "Soglia di distanza",
"description": "Genera una mappa dei segmenti di linea dal livello selezionato utilizzando il modello di rilevamento dei segmenti di linea MLSD."
},
"content_shuffle": {
"label": "Mescola contenuto",
"scale_factor": "Fattore di scala",
"description": "Mescola il contenuto del livello selezionato, in modo simile all'effetto \"liquefa\"."
},
"mediapipe_face_detection": {
"min_confidence": "Confidenza minima",
"label": "Rilevamento del volto MediaPipe",
"max_faces": "Max volti",
"description": "Rileva i volti nel livello selezionato utilizzando il modello di rilevamento dei volti MediaPipe."
},
"dw_openpose_detection": {
"draw_face": "Disegna il volto",
"description": "Rileva le pose umane nel livello selezionato utilizzando il modello DW Openpose.",
"label": "Rilevamento DW Openpose",
"draw_hands": "Disegna le mani",
"draw_body": "Disegna il corpo"
},
"normal_map": {
"description": "Genera una mappa delle normali dal livello selezionato.",
"label": "Mappa delle normali"
},
"lineart_edge_detection": {
"label": "Rilevamento bordi Lineart",
"coarse": "Grossolano",
"description": "Genera una mappa dei bordi dal livello selezionato utilizzando il modello di rilevamento dei bordi Lineart."
},
"depth_anything_depth_estimation": {
"model_size_small": "Piccolo",
"model_size_small_v2": "Piccolo v2",
"model_size": "Dimensioni modello",
"model_size_large": "Grande",
"model_size_base": "Base",
"description": "Genera una mappa di profondità dal livello selezionato utilizzando un modello Depth Anything."
},
"color_map": {
"label": "Mappa colore",
"description": "Crea una mappa dei colori dal livello selezionato.",
"tile_size": "Dimens. Piastrella"
},
"canny_edge_detection": {
"high_threshold": "Soglia superiore",
"low_threshold": "Soglia inferiore",
"description": "Genera una mappa dei bordi dal livello selezionato utilizzando l'algoritmo di rilevamento dei bordi Canny.",
"label": "Rilevamento bordi Canny"
},
"spandrel_filter": {
"scale": "Scala di destinazione",
"autoScaleDesc": "Il modello selezionato verrà eseguito fino al raggiungimento della scala di destinazione.",
"description": "Esegue un modello immagine-a-immagine sul livello selezionato.",
"label": "Modello Immagine-a-Immagine",
"model": "Modello",
"autoScale": "Auto Scala"
},
"pidi_edge_detection": {
"quantize_edges": "Quantizza i bordi",
"scribble": "Scarabocchio",
"description": "Genera una mappa dei bordi dal livello selezionato utilizzando il modello di rilevamento dei bordi PiDiNet.",
"label": "Rilevamento bordi PiDiNet"
},
"hed_edge_detection": {
"label": "Rilevamento bordi HED",
"description": "Genera una mappa dei bordi dal livello selezionato utilizzando il modello di rilevamento dei bordi HED.",
"scribble": "Scarabocchio"
},
"lineart_anime_edge_detection": {
"description": "Genera una mappa dei bordi dal livello selezionato utilizzando il modello di rilevamento dei bordi Lineart Anime.",
"label": "Rilevamento bordi Lineart Anime"
}
},
"controlLayers_withCount_hidden": "Livelli di controllo ({{count}} nascosti)",
"regionalGuidance_withCount_hidden": "Guida regionale ({{count}} nascosti)",
"fill": {
"grid": "Griglia",
"crosshatch": "Tratteggio incrociato",
"fillColor": "Colore di riempimento",
"fillStyle": "Stile riempimento",
"solid": "Solido",
"vertical": "Verticale",
"horizontal": "Orizzontale",
"diagonal": "Diagonale"
},
"rasterLayers_withCount_hidden": "Livelli raster ({{count}} nascosti)",
"inpaintMasks_withCount_hidden": "Maschere Inpaint ({{count}} nascoste)",
"regionalGuidance_withCount_visible": "Guide regionali ({{count}})",
"locked": "Bloccato",
"hidingType": "Nascondere {{type}}",
"logDebugInfo": "Registro Info Debug",
"inpaintMasks_withCount_visible": "Maschere Inpaint ({{count}})",
"layer_one": "Livello",
"layer_many": "Livelli",
"layer_other": "Livelli",
"disableTransparencyEffect": "Disabilita l'effetto trasparenza",
"controlLayers_withCount_visible": "Livelli di controllo ({{count}})",
"transparency": "Trasparenza",
"newCanvasSessionDesc": "Questo cancellerà la tela e tutte le impostazioni, eccetto la selezione del modello. Le generazioni saranno effettuate sulla tela.",
"rasterLayers_withCount_visible": "Livelli raster ({{count}})",
"globalReferenceImages_withCount_visible": "Immagini di riferimento Globali ({{count}})",
"globalReferenceImages_withCount_hidden": "Immagini di riferimento globali ({{count}} nascoste)",
"layer_withCount_one": "Livello ({{count}})",
"layer_withCount_many": "Livelli ({{count}})",
"layer_withCount_other": "Livelli ({{count}})",
"convertToControlLayer": "Converti in livello di controllo",
"convertToRasterLayer": "Converti in livello raster",
"unlocked": "Sbloccato",
"enableTransparencyEffect": "Abilita l'effetto trasparenza",
"replaceLayer": "Sostituisci livello",
"pullBboxIntoLayer": "Carica l'immagine delimitata nel riquadro",
"pullBboxIntoReferenceImage": "Carica l'immagine delimitata nel riquadro",
"showProgressOnCanvas": "Mostra i progressi sulla Tela",
"weight": "Peso",
"newGallerySession": "Nuova sessione Galleria",
"newGallerySessionDesc": "Questo cancellerà la tela e tutte le impostazioni, eccetto la selezione del modello. Le generazioni saranno inviate alla galleria.",
"newCanvasSession": "Nuova sessione Tela",
"deleteSelected": "Elimina selezione",
"settings": {
"isolatedFilteringPreview": "Anteprima del filtraggio isolata",
"isolatedStagingPreview": "Anteprima di generazione isolata",
"isolatedTransformingPreview": "Anteprima di trasformazione isolata",
"isolatedPreview": "Anteprima isolata",
"invertBrushSizeScrollDirection": "Inverti scorrimento per dimensione pennello",
"snapToGrid": {
"label": "Aggancia alla griglia",
"on": "Acceso",
"off": "Spento"
},
"pressureSensitivity": "Sensibilità alla pressione",
"preserveMask": {
"alert": "Preservare la regione mascherata",
"label": "Preserva la regione mascherata"
}
},
"transform": {
"reset": "Reimposta",
"fitToBbox": "Adatta al Riquadro",
"transform": "Trasforma",
"apply": "Applica",
"cancel": "Annulla"
},
"stagingArea": {
"next": "Successiva",
"discard": "Scarta",
"discardAll": "Scarta tutto",
"accept": "Accetta",
"saveToGallery": "Salva nella Galleria",
"previous": "Precedente",
"showResultsOn": "Risultati visualizzati",
"showResultsOff": "Risultati nascosti"
},
"HUD": {
"bbox": "Riquadro di delimitazione",
"entityStatus": {
"isHidden": "{{title}} è nascosto",
"isLocked": "{{title}} è bloccato",
"isTransforming": "{{title}} sta trasformando",
"isFiltering": "{{title}} sta filtrando",
"isEmpty": "{{title}} è vuoto",
"isDisabled": "{{title}} è disabilitato"
},
"scaledBbox": "Riquadro scalato"
},
"canvasContextMenu": {
"newControlLayer": "Nuovo Livello di Controllo",
"newRegionalReferenceImage": "Nuova immagine di riferimento Regionale",
"newGlobalReferenceImage": "Nuova immagine di riferimento Globale",
"bboxGroup": "Crea dal riquadro di delimitazione",
"saveBboxToGallery": "Salva il riquadro nella Galleria",
"cropCanvasToBbox": "Ritaglia la Tela al riquadro",
"canvasGroup": "Tela",
"newRasterLayer": "Nuovo Livello Raster",
"saveCanvasToGallery": "Salva la Tela nella Galleria",
"saveToGalleryGroup": "Salva nella Galleria"
}
"opacity": "Opacità"
},
"ui": {
"tabs": {
@@ -1916,8 +1545,7 @@
"modelsTab": "$t(ui.tabs.models) $t(common.tab)",
"queue": "Coda",
"upscaling": "Amplia",
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)",
"gallery": "Galleria"
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)"
}
},
"upscaling": {
@@ -1987,45 +1615,5 @@
"noTemplates": "Nessun modello",
"acceptedColumnsKeys": "Colonne/chiavi accettate:",
"promptTemplateCleared": "Modello di prompt cancellato"
},
"newUserExperience": {
"gettingStartedSeries": "Desideri maggiori informazioni? Consulta la nostra <LinkComponent>Getting Started Series</LinkComponent> per suggerimenti su come sfruttare appieno il potenziale di Invoke Studio.",
"toGetStarted": "Per iniziare, inserisci un prompt nella casella e fai clic su <StrongComponent>Invoke</StrongComponent> per generare la tua prima immagine. Seleziona un modello di prompt per migliorare i risultati. Puoi scegliere di salvare le tue immagini direttamente nella <StrongComponent>Galleria</StrongComponent> o modificarle nella <StrongComponent>Tela</StrongComponent>."
},
"whatsNew": {
"canvasV2Announcement": {
"readReleaseNotes": "Leggi le Note di Rilascio",
"fluxSupport": "Supporto per la famiglia di modelli Flux",
"newCanvas": "Una nuova potente tela di controllo",
"watchReleaseVideo": "Guarda il video di rilascio",
"watchUiUpdatesOverview": "Guarda le novità dell'interfaccia",
"newLayerTypes": "Nuovi tipi di livello per un miglior controllo"
},
"whatsNewInInvoke": "Novità in Invoke"
},
"system": {
"logLevel": {
"info": "Info",
"warn": "Avviso",
"fatal": "Fatale",
"error": "Errore",
"debug": "Debug",
"trace": "Traccia",
"logLevel": "Livello di registro"
},
"logNamespaces": {
"workflows": "Flussi di lavoro",
"generation": "Generazione",
"canvas": "Tela",
"config": "Configurazione",
"models": "Modelli",
"gallery": "Galleria",
"queue": "Coda",
"events": "Eventi",
"system": "Sistema",
"metadata": "Metadati",
"logNamespaces": "Elementi del registro"
},
"enableLogging": "Abilita la registrazione"
}
}

View File

@@ -93,8 +93,7 @@
"placeholderSelectAModel": "Выбрать модель",
"reset": "Сброс",
"none": "Ничего",
"new": "Новый",
"ok": "Ok"
"new": "Новый"
},
"gallery": {
"galleryImageSize": "Размер изображений",
@@ -228,118 +227,6 @@
"selectBrushTool": {
"title": "Инструмент кисть",
"desc": "Выбирает кисть."
},
"selectBboxTool": {
"title": "Инструмент рамка",
"desc": "Выбрать инструмент «Ограничительная рамка»."
},
"incrementToolWidth": {
"desc": "Increment the brush or eraser tool width, whichever is selected.",
"title": "Increment Tool Width"
},
"selectColorPickerTool": {
"title": "Color Picker Tool",
"desc": "Select the color picker tool."
},
"prevEntity": {
"title": "Prev Layer",
"desc": "Select the previous layer in the list."
},
"filterSelected": {
"title": "Filter",
"desc": "Filter the selected layer. Only applies to Raster and Control layers."
},
"undo": {
"desc": "Отменяет последнее действие на холсте.",
"title": "Отменить"
},
"transformSelected": {
"title": "Transform",
"desc": "Transform the selected layer."
},
"setZoomTo400Percent": {
"title": "Zoom to 400%",
"desc": "Set the canvas zoom to 400%."
},
"setZoomTo200Percent": {
"title": "Zoom to 200%",
"desc": "Set the canvas zoom to 200%."
},
"deleteSelected": {
"desc": "Delete the selected layer.",
"title": "Delete Layer"
},
"resetSelected": {
"title": "Reset Layer",
"desc": "Reset the selected layer. Only applies to Inpaint Mask and Regional Guidance."
},
"redo": {
"desc": "Возвращает последнее отмененное действие.",
"title": "Вернуть"
},
"nextEntity": {
"title": "Next Layer",
"desc": "Select the next layer in the list."
},
"setFillToWhite": {
"title": "Set Color to White",
"desc": "Set the current tool color to white."
},
"applyFilter": {
"title": "Apply Filter",
"desc": "Apply the pending filter to the selected layer."
},
"cancelFilter": {
"title": "Cancel Filter",
"desc": "Cancel the pending filter."
},
"applyTransform": {
"desc": "Apply the pending transform to the selected layer.",
"title": "Apply Transform"
},
"cancelTransform": {
"title": "Cancel Transform",
"desc": "Cancel the pending transform."
},
"selectEraserTool": {
"title": "Eraser Tool",
"desc": "Select the eraser tool."
},
"fitLayersToCanvas": {
"desc": "Scale and position the view to fit all visible layers.",
"title": "Fit Layers to Canvas"
},
"decrementToolWidth": {
"title": "Decrement Tool Width",
"desc": "Decrement the brush or eraser tool width, whichever is selected."
},
"setZoomTo800Percent": {
"title": "Zoom to 800%",
"desc": "Set the canvas zoom to 800%."
},
"quickSwitch": {
"title": "Layer Quick Switch",
"desc": "Switch between the last two selected layers. If a layer is bookmarked, always switch between it and the last non-bookmarked layer."
},
"fitBboxToCanvas": {
"title": "Fit Bbox to Canvas",
"desc": "Scale and position the view to fit the bbox."
},
"setZoomTo100Percent": {
"title": "Zoom to 100%",
"desc": "Set the canvas zoom to 100%."
},
"selectMoveTool": {
"desc": "Select the move tool.",
"title": "Move Tool"
},
"selectRectTool": {
"title": "Rect Tool",
"desc": "Select the rect tool."
},
"selectViewTool": {
"title": "View Tool",
"desc": "Select the view tool."
}
},
"hotkeys": "Горячие клавиши",
@@ -349,33 +236,11 @@
"desc": "Отменить последнее действие в рабочем процессе."
},
"deleteSelection": {
"desc": "Удалить выделенные узлы и ребра.",
"title": "Delete"
"desc": "Удалить выделенные узлы и ребра."
},
"redo": {
"title": "Вернуть",
"desc": "Вернуть последнее действие в рабочем процессе."
},
"copySelection": {
"title": "Copy",
"desc": "Copy selected nodes and edges."
},
"pasteSelection": {
"title": "Paste",
"desc": "Paste copied nodes and edges."
},
"addNode": {
"desc": "Open the add node menu.",
"title": "Add Node"
},
"title": "Workflows",
"pasteSelectionWithEdges": {
"title": "Paste with Edges",
"desc": "Paste copied nodes, edges, and all edges connected to copied nodes."
},
"selectAll": {
"desc": "Select all nodes and edges.",
"title": "Select All"
}
},
"viewer": {
@@ -392,84 +257,12 @@
"title": "Восстановить все метаданные"
},
"swapImages": {
"desc": "Поменять местами сравниваемые изображения.",
"title": "Swap Comparison Images"
"desc": "Поменять местами сравниваемые изображения."
},
"title": "Просмотрщик изображений",
"toggleViewer": {
"title": "Открыть/закрыть просмотрщик",
"desc": "Показать или скрыть просмотрщик изображений. Доступно только на вкладке «Холст»."
},
"recallSeed": {
"title": "Recall Seed",
"desc": "Recall the seed for the current image."
},
"recallPrompts": {
"desc": "Recall the positive and negative prompts for the current image.",
"title": "Recall Prompts"
},
"remix": {
"title": "Remix",
"desc": "Recall all metadata except for the seed for the current image."
},
"useSize": {
"desc": "Use the current image's size as the bbox size.",
"title": "Use Size"
},
"runPostprocessing": {
"title": "Run Postprocessing",
"desc": "Run the selected postprocessing on the current image."
},
"toggleMetadata": {
"title": "Show/Hide Metadata",
"desc": "Show or hide the current image's metadata overlay."
}
},
"gallery": {
"galleryNavRightAlt": {
"desc": "Same as Navigate Right, but selects the compare image, opening compare mode if it isn't already open.",
"title": "Navigate Right (Compare Image)"
},
"galleryNavRight": {
"desc": "Navigate right in the gallery grid, selecting that image. If at the last image of the row, go to the next row. If at the last image of the page, go to the next page.",
"title": "Navigate Right"
},
"galleryNavUp": {
"desc": "Navigate up in the gallery grid, selecting that image. If at the top of the page, go to the previous page.",
"title": "Navigate Up"
},
"galleryNavDown": {
"title": "Navigate Down",
"desc": "Navigate down in the gallery grid, selecting that image. If at the bottom of the page, go to the next page."
},
"galleryNavLeft": {
"title": "Navigate Left",
"desc": "Navigate left in the gallery grid, selecting that image. If at the first image of the row, go to the previous row. If at the first image of the page, go to the previous page."
},
"galleryNavDownAlt": {
"title": "Navigate Down (Compare Image)",
"desc": "Same as Navigate Down, but selects the compare image, opening compare mode if it isn't already open."
},
"galleryNavLeftAlt": {
"desc": "Same as Navigate Left, but selects the compare image, opening compare mode if it isn't already open.",
"title": "Navigate Left (Compare Image)"
},
"clearSelection": {
"desc": "Clear the current selection, if any.",
"title": "Clear Selection"
},
"deleteSelection": {
"title": "Delete",
"desc": "Delete all selected images. By default, you will be prompted to confirm deletion. If the images are currently in use in the app, you will be warned."
},
"galleryNavUpAlt": {
"title": "Navigate Up (Compare Image)",
"desc": "Same as Navigate Up, but selects the compare image, opening compare mode if it isn't already open."
},
"title": "Gallery",
"selectAllOnPage": {
"title": "Select All On Page",
"desc": "Select all images on the current page."
}
}
},
@@ -579,9 +372,7 @@
"ipAdapters": "IP адаптеры",
"starterModelsInModelManager": "Стартовые модели можно найти в Менеджере моделей",
"learnMoreAboutSupportedModels": "Подробнее о поддерживаемых моделях",
"t5Encoder": "T5 энкодер",
"spandrelImageToImage": "Image to Image (Spandrel)",
"clipEmbed": "CLIP Embed"
"t5Encoder": "T5 энкодер"
},
"parameters": {
"images": "Изображения",
@@ -641,16 +432,12 @@
"rgNoRegion": "регион не выбран",
"rgNoPromptsOrIPAdapters": "нет текстовых запросов или IP-адаптеров",
"ipAdapterIncompatibleBaseModel": "несовместимая базовая модель IP-адаптера",
"ipAdapterNoImageSelected": "изображение IP-адаптера не выбрано",
"t2iAdapterIncompatibleScaledBboxWidth": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, масштабированная ширина рамки {{width}}",
"t2iAdapterIncompatibleBboxHeight": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, высота рамки {{height}}",
"t2iAdapterIncompatibleBboxWidth": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, ширина рамки {{width}}",
"t2iAdapterIncompatibleScaledBboxHeight": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, масштабированная высота рамки {{height}}"
"ipAdapterNoImageSelected": "изображение IP-адаптера не выбрано"
},
"fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), ширина рамки {{width}}",
"fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), высота рамки {{height}}",
"fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), масштабированная высота рамки {{height}}",
"fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16) масштабированная ширина рамки {{width}}",
"fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), ширина bbox {{width}}",
"fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), высота bbox {{height}}",
"fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), масштабированная высота bbox {{height}}",
"fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16) масштабированная ширина bbox {{width}}",
"noFLUXVAEModelSelected": "Для генерации FLUX не выбрана модель VAE",
"noT5EncoderModelSelected": "Для генерации FLUX не выбрана модель T5 энкодера",
"canvasIsFiltering": "Холст фильтруется",
@@ -683,8 +470,7 @@
"staged": "Инсценировка",
"optimizedImageToImage": "Оптимизированное img2img",
"sendToCanvas": "Отправить на холст",
"guidance": "Точность",
"boxBlur": "Box Blur"
"guidance": "Точность"
},
"settings": {
"models": "Модели",
@@ -718,8 +504,7 @@
"intermediatesClearedFailed": "Проблема очистки промежуточных",
"reloadingIn": "Перезагрузка через",
"informationalPopoversDisabled": "Информационные всплывающие окна отключены",
"informationalPopoversDisabledDesc": "Информационные всплывающие окна были отключены. Включите их в Настройках.",
"confirmOnNewSession": "Подтверждение нового сеанса"
"informationalPopoversDisabledDesc": "Информационные всплывающие окна были отключены. Включите их в Настройках."
},
"toast": {
"uploadFailed": "Загрузка не удалась",
@@ -733,8 +518,8 @@
"parameterSet": "Параметр задан",
"problemCopyingImage": "Не удается скопировать изображение",
"baseModelChangedCleared_one": "Очищена или отключена {{count}} несовместимая подмодель",
"baseModelChangedCleared_few": "Очищено или отключено {{count}} несовместимых подмодели",
"baseModelChangedCleared_many": "Очищено или отключено {{count}} несовместимых подмоделей",
"baseModelChangedCleared_few": "Очищены или отключены {{count}} несовместимые подмодели",
"baseModelChangedCleared_many": "Очищены или отключены {{count}} несовместимых подмоделей",
"loadedWithWarnings": "Рабочий процесс загружен с предупреждениями",
"setControlImage": "Установить как контрольное изображение",
"setNodeField": "Установить как поле узла",
@@ -788,9 +573,7 @@
"resetUI": "$t(accessibility.reset) интерфейс",
"createIssue": "Сообщить о проблеме",
"about": "Об этом",
"submitSupportTicket": "Отправить тикет в службу поддержки",
"toggleRightPanel": "Переключить правую панель (G)",
"toggleLeftPanel": "Переключить левую панель (T)"
"submitSupportTicket": "Отправить тикет в службу поддержки"
},
"nodes": {
"zoomInNodes": "Увеличьте масштаб",
@@ -947,16 +730,16 @@
"loading": "Загрузка...",
"clearSearch": "Очистить поиск",
"deleteBoardOnly": "Удалить только доску",
"movingImagesToBoard_one": "Перемещение {{count}} изображения на доску:",
"movingImagesToBoard_few": "Перемещение {{count}} изображений на доску:",
"movingImagesToBoard_many": "Перемещение {{count}} изображений на доску:",
"movingImagesToBoard_one": "Перемещаем {{count}} изображение на доску:",
"movingImagesToBoard_few": "Перемещаем {{count}} изображения на доску:",
"movingImagesToBoard_many": "Перемещаем {{count}} изображений на доску:",
"downloadBoard": "Скачать доску",
"deleteBoard": "Удалить доску",
"deleteBoardAndImages": "Удалить доску и изображения",
"deletedBoardsCannotbeRestored": "Удаленные доски не могут быть восстановлены. Выбор «Удалить только доску» переведет изображения в состояние без категории.",
"assetsWithCount_one": "{{count}} актив",
"assetsWithCount_few": "{{count}} актива",
"assetsWithCount_many": "{{count}} активов",
"assetsWithCount_one": "{{count}} ассет",
"assetsWithCount_few": "{{count}} ассета",
"assetsWithCount_many": "{{count}} ассетов",
"imagesWithCount_one": "{{count}} изображение",
"imagesWithCount_few": "{{count}} изображения",
"imagesWithCount_many": "{{count}} изображений",
@@ -972,8 +755,7 @@
"hideBoards": "Скрыть доски",
"viewBoards": "Просмотреть доски",
"noBoards": "Нет досок {{boardType}}",
"deletedPrivateBoardsCannotbeRestored": "Удаленные доски не могут быть восстановлены. Выбор «Удалить только доску» переведет изображения в приватное состояние без категории для создателя изображения.",
"updateBoardError": "Ошибка обновления доски"
"deletedPrivateBoardsCannotbeRestored": "Удаленные доски не могут быть восстановлены. Выбор «Удалить только доску» переведет изображения в приватное состояние без категории для создателя изображения."
},
"dynamicPrompts": {
"seedBehaviour": {
@@ -1610,15 +1392,15 @@
"autoNegative": "Авто негатив",
"deletePrompt": "Удалить запрос",
"rectangle": "Прямоугольник",
"addNegativePrompt": "Добавить $t(controlLayers.negativePrompt)",
"addNegativePrompt": "Добавить $t(common.negativePrompt)",
"regionalGuidance": "Региональная точность",
"opacity": "Непрозрачность",
"addLayer": "Добавить слой",
"moveToFront": "На передний план",
"addPositivePrompt": "Добавить $t(controlLayers.prompt)",
"addPositivePrompt": "Добавить $t(common.positivePrompt)",
"regional": "Региональный",
"bookmark": "Закладка для быстрого переключения",
"fitBboxToLayers": "Подогнать рамку к слоям",
"fitBboxToLayers": "Подогнать Bbox к слоям",
"mergeVisibleOk": "Объединенные видимые слои",
"mergeVisibleError": "Ошибка объединения видимых слоев",
"clearHistory": "Очистить историю",
@@ -1627,7 +1409,7 @@
"saveLayerToAssets": "Сохранить слой в активы",
"clearCaches": "Очистить кэши",
"recalculateRects": "Пересчитать прямоугольники",
"saveBboxToGallery": "Сохранить рамку в галерею",
"saveBboxToGallery": "Сохранить Bbox в галерею",
"resetCanvas": "Сбросить холст",
"canvas": "Холст",
"global": "Глобальный",
@@ -1639,280 +1421,15 @@
"newRasterLayerOk": "Создан растровый слой",
"newRasterLayerError": "Ошибка создания растрового слоя",
"newGlobalReferenceImageOk": "Создано глобальное эталонное изображение",
"bboxOverlay": "Показать наложение ограничительной рамки",
"bboxOverlay": "Показать наложение Bbox",
"saveCanvasToGallery": "Сохранить холст в галерею",
"pullBboxIntoReferenceImageOk": "рамка перенесена в эталонное изображение",
"pullBboxIntoReferenceImageError": "Ошибка переноса рамки в эталонное изображение",
"pullBboxIntoReferenceImageOk": "Bbox перенесен в эталонное изображение",
"pullBboxIntoReferenceImageError": "Ошибка переноса BBox в эталонное изображение",
"regionIsEmpty": "Выбранный регион пуст",
"savedToGalleryOk": "Сохранено в галерею",
"savedToGalleryError": "Ошибка сохранения в галерею",
"pullBboxIntoLayerOk": "Рамка перенесена в слой",
"pullBboxIntoLayerError": "Проблема с переносом рамки в слой",
"newLayerFromImage": "Новый слой из изображения",
"filter": {
"lineart_anime_edge_detection": {
"label": "Обнаружение краев Lineart Anime",
"description": "Создает карту краев выбранного слоя с помощью модели обнаружения краев Lineart Anime."
},
"hed_edge_detection": {
"scribble": "Штрих",
"label": "обнаружение границ HED",
"description": "Создает карту границ из выбранного слоя с использованием модели обнаружения границ HED."
},
"mlsd_detection": {
"description": "Генерирует карту сегментов линий из выбранного слоя с помощью модели обнаружения сегментов линий MLSD.",
"score_threshold": "Пороговый балл",
"distance_threshold": "Порог расстояния",
"label": "Обнаружение сегментов линии"
},
"canny_edge_detection": {
"low_threshold": "Низкий порог",
"high_threshold": "Высокий порог",
"label": "Обнаружение краев",
"description": "Создает карту краев выбранного слоя с помощью алгоритма обнаружения краев Canny."
},
"color_map": {
"description": "Создайте цветовую карту из выбранного слоя.",
"label": "Цветная карта",
"tile_size": "Размер плитки"
},
"depth_anything_depth_estimation": {
"model_size_base": "Базовая",
"model_size_large": "Большая",
"label": "Анализ глубины",
"model_size_small": "Маленькая",
"model_size_small_v2": "Маленькая v2",
"description": "Создает карту глубины из выбранного слоя с использованием модели Depth Anything.",
"model_size": "Размер модели"
},
"mediapipe_face_detection": {
"min_confidence": "Минимальная уверенность",
"label": "Распознавание лиц MediaPipe",
"description": "Обнаруживает лица в выбранном слое с помощью модели обнаружения лиц MediaPipe.",
"max_faces": "Максимум лиц"
},
"lineart_edge_detection": {
"label": "Обнаружение краев Lineart",
"description": "Создает карту краев выбранного слоя с помощью модели обнаружения краев Lineart.",
"coarse": "Грубый"
},
"filterType": "Тип фильтра",
"autoProcess": "Автообработка",
"reset": "Сбросить",
"content_shuffle": {
"scale_factor": "Коэффициент",
"label": "Перетасовка контента",
"description": "Перемешивает содержимое выбранного слоя, аналогично эффекту «сжижения»."
},
"dw_openpose_detection": {
"label": "Обнаружение DW Openpose",
"draw_hands": "Рисовать руки",
"description": "Обнаруживает позы человека в выбранном слое с помощью модели DW Openpose.",
"draw_face": "Рисовать лицо",
"draw_body": "Рисовать тело"
},
"normal_map": {
"label": "Карта нормалей",
"description": "Создает карту нормалей для выбранного слоя."
},
"spandrel_filter": {
"model": "Модель",
"label": "Модель img2img",
"autoScale": "Авто масштабирование",
"scale": "Целевой масштаб",
"description": "Запустить модель изображения к изображению на выбранном слое.",
"autoScaleDesc": "Выбранная модель будет работать до тех пор, пока не будет достигнут целевой масштаб."
},
"pidi_edge_detection": {
"scribble": "Штрих",
"description": "Генерирует карту краев из выбранного слоя с помощью модели обнаружения краев PiDiNet.",
"label": "Обнаружение краев PiDiNet",
"quantize_edges": "Квантизация краев"
},
"process": "Обработать",
"apply": "Применить",
"cancel": "Отменить",
"filter": "Фильтр",
"filters": "Фильтры"
},
"HUD": {
"entityStatus": {
"isHidden": "{{title}} скрыт",
"isLocked": "{{title}} заблокирован",
"isDisabled": "{{title}} отключен",
"isEmpty": "{{title}} пуст",
"isFiltering": "{{title}} фильтруется",
"isTransforming": "{{title}} трансформируется"
},
"scaledBbox": "Масштабированная рамка",
"bbox": "Ограничительная рамка"
},
"canvasContextMenu": {
"saveBboxToGallery": "Сохранить рамку в галерею",
"newGlobalReferenceImage": "Новое глобальное эталонное изображение",
"bboxGroup": "Сохдать из рамки",
"canvasGroup": "Холст",
"newControlLayer": "Новый контрольный слой",
"newRasterLayer": "Новый растровый слой",
"saveToGalleryGroup": "Сохранить в галерею",
"saveCanvasToGallery": "Сохранить холст в галерею",
"cropCanvasToBbox": "Обрезать холст по рамке",
"newRegionalReferenceImage": "Новое региональное эталонное изображение"
},
"fill": {
"solid": "Сплошной",
"fillStyle": "Стиль заполнения",
"fillColor": "Цвет заполнения",
"grid": "Сетка",
"horizontal": "Горизонтальная",
"diagonal": "Диагональная",
"crosshatch": "Штриховка",
"vertical": "Вертикальная"
},
"showHUD": "Показать HUD",
"copyToClipboard": "Копировать в буфер обмена",
"ipAdapterMethod": {
"composition": "Только композиция",
"style": "Только стиль",
"ipAdapterMethod": "Метод IP адаптера",
"full": "Полный"
},
"addReferenceImage": "Добавить $t(controlLayers.referenceImage)",
"inpaintMask": "Маска перерисовки",
"sendToGalleryDesc": "При нажатии кнопки Invoke создается изображение и сохраняется в вашей галерее.",
"sendToCanvas": "Отправить на холст",
"regionalGuidance_withCount_one": "$t(controlLayers.regionalGuidance)",
"regionalGuidance_withCount_few": "Региональных точности",
"regionalGuidance_withCount_many": "Региональных точностей",
"controlLayer_withCount_one": "$t(controlLayers.controlLayer)",
"controlLayer_withCount_few": "Контрольных слоя",
"controlLayer_withCount_many": "Контрольных слоев",
"newCanvasFromImage": "Новый холст из изображения",
"inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)",
"inpaintMask_withCount_few": "Маски перерисовки",
"inpaintMask_withCount_many": "Масок перерисовки",
"globalReferenceImages_withCount_visible": "Глобальные эталонные изображения ({{count}})",
"controlMode": {
"prompt": "Запрос",
"controlMode": "Режим контроля",
"megaControl": "Мега контроль",
"balanced": "Сбалансированный",
"control": "Контроль"
},
"settings": {
"isolatedPreview": "Изолированный предпросмотр",
"isolatedTransformingPreview": "Изолированный предпросмотр преобразования",
"invertBrushSizeScrollDirection": "Инвертировать прокрутку для размера кисти",
"snapToGrid": {
"label": "Привязка к сетке",
"on": "Вкл",
"off": "Выкл"
},
"isolatedFilteringPreview": "Изолированный предпросмотр фильтрации",
"pressureSensitivity": "Чувствительность к давлению",
"isolatedStagingPreview": "Изолированный предпросмотр на промежуточной стадии",
"preserveMask": {
"label": "Сохранить замаскированную область",
"alert": "Сохранение замаскированной области"
}
},
"stagingArea": {
"discardAll": "Отбросить все",
"discard": "Отбросить",
"accept": "Принять",
"previous": "Предыдущий",
"next": "Следующий",
"saveToGallery": "Сохранить в галерею",
"showResultsOn": "Показать результаты",
"showResultsOff": "Скрыть результаты"
},
"pullBboxIntoReferenceImage": "Поместить рамку в эталонное изображение",
"enableAutoNegative": "Включить авто негатив",
"maskFill": "Заполнение маски",
"viewProgressInViewer": "Просматривайте прогресс и результаты в <Btn>Просмотрщике изображений</Btn>.",
"convertToRasterLayer": "Конвертировать в растровый слой",
"tool": {
"move": "Двигать",
"bbox": "Ограничительная рамка",
"view": "Смотреть",
"brush": "Кисть",
"eraser": "Ластик",
"rectangle": "Прямоугольник",
"colorPicker": "Подборщик цветов"
},
"rasterLayer": "Растровый слой",
"sendingToCanvas": "Постановка генераций на холст",
"rasterLayers_withCount_visible": "Растровые слои ({{count}})",
"regionalGuidance_withCount_hidden": "Региональная точность ({{count}} скрыто)",
"enableTransparencyEffect": "Включить эффект прозрачности",
"hidingType": "Скрыть {{type}}",
"addRegionalGuidance": "Добавить $t(controlLayers.regionalGuidance)",
"sendingToGallery": "Отправка генераций в галерею",
"viewProgressOnCanvas": "Просматривайте прогресс и результаты этапов на <Btn>Холсте</Btn>.",
"controlLayers_withCount_hidden": "Контрольные слои ({{count}} скрыто)",
"rasterLayers_withCount_hidden": "Растровые слои ({{count}} скрыто)",
"deleteSelected": "Удалить выбранное",
"stagingOnCanvas": "Постановка изображений на",
"pullBboxIntoLayer": "Поместить рамку в слой",
"locked": "Заблокировано",
"replaceLayer": "Заменить слой",
"width": "Ширина",
"controlLayer": "Слой управления",
"addRasterLayer": "Добавить $t(controlLayers.rasterLayer)",
"addControlLayer": "Добавить $t(controlLayers.controlLayer)",
"addInpaintMask": "Добавить $t(controlLayers.inpaintMask)",
"inpaintMasks_withCount_hidden": "Маски перерисовки ({{count}} скрыто)",
"regionalGuidance_withCount_visible": "Региональная точность ({{count}})",
"newGallerySessionDesc": "Это очистит холст и все настройки, кроме выбранной модели. Генерации будут отправлены в галерею.",
"newCanvasSession": "Новая сессия холста",
"newCanvasSessionDesc": "Это очистит холст и все настройки, кроме выбора модели. Генерации будут размещены на холсте.",
"cropLayerToBbox": "Обрезать слой по ограничительной рамке",
"clipToBbox": "Обрезка штрихов в рамке",
"outputOnlyMaskedRegions": "Вывод только маскированных областей",
"duplicate": "Дублировать",
"inpaintMasks_withCount_visible": "Маски перерисовки ({{count}})",
"layer_one": "Слой",
"layer_few": "",
"layer_many": "",
"prompt": "Запрос",
"negativePrompt": "Исключающий запрос",
"beginEndStepPercentShort": "Начало/конец %",
"transform": {
"transform": "Трансформировать",
"fitToBbox": "Вместить в рамку",
"reset": "Сбросить",
"apply": "Применить",
"cancel": "Отменить"
},
"disableAutoNegative": "Отключить авто негатив",
"deleteReferenceImage": "Удалить эталонное изображение",
"controlLayers_withCount_visible": "Контрольные слои ({{count}})",
"rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)",
"rasterLayer_withCount_few": "Растровых слоя",
"rasterLayer_withCount_many": "Растровых слоев",
"transparency": "Прозрачность",
"weight": "Вес",
"newGallerySession": "Новая сессия галереи",
"sendToCanvasDesc": "Нажатие кнопки Invoke отображает вашу текущую работу на холсте.",
"globalReferenceImages_withCount_hidden": "Глобальные эталонные изображения ({{count}} скрыто)",
"convertToControlLayer": "Конвертировать в контрольный слой",
"layer_withCount_one": "Слой ({{count}})",
"layer_withCount_few": "Слои ({{count}})",
"layer_withCount_many": "Слои ({{count}})",
"disableTransparencyEffect": "Отключить эффект прозрачности",
"showingType": "Показать {{type}}",
"dynamicGrid": "Динамическая сетка",
"logDebugInfo": "Писать отладочную информацию",
"unlocked": "Разблокировано",
"showProgressOnCanvas": "Показать прогресс на холсте",
"globalReferenceImage_withCount_one": "$t(controlLayers.globalReferenceImage)",
"globalReferenceImage_withCount_few": "Глобальных эталонных изображения",
"globalReferenceImage_withCount_many": "Глобальных эталонных изображений",
"regionalReferenceImage": "Региональное эталонное изображение",
"globalReferenceImage": "Глобальное эталонное изображение",
"sendToGallery": "Отправить в галерею",
"referenceImage": "Эталонное изображение",
"addGlobalReferenceImage": "Добавить $t(controlLayers.globalReferenceImage)"
"pullBboxIntoLayerOk": "Bbox перенесен в слой",
"pullBboxIntoLayerError": "Проблема с переносом BBox в слой"
},
"ui": {
"tabs": {
@@ -1924,8 +1441,7 @@
"modelsTab": "$t(ui.tabs.models) $t(common.tab)",
"queue": "Очередь",
"upscaling": "Увеличение",
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)",
"gallery": "Галерея"
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)"
}
},
"upscaling": {
@@ -1997,45 +1513,5 @@
"professional": "Профессионал",
"professionalUpsell": "Доступно в профессиональной версии Invoke. Нажмите здесь или посетите invoke.com/pricing для получения более подробной информации.",
"shareAccess": "Поделиться доступом"
},
"system": {
"logNamespaces": {
"canvas": "Холст",
"config": "Конфигурация",
"generation": "Генерация",
"workflows": "Рабочие процессы",
"gallery": "Галерея",
"models": "Модели",
"logNamespaces": "Пространства имен логов",
"events": "События",
"system": "Система",
"queue": "Очередь",
"metadata": "Метаданные"
},
"enableLogging": "Включить логи",
"logLevel": {
"logLevel": "Уровень логов",
"fatal": "Фатальное",
"debug": "Отладка",
"info": "Инфо",
"warn": "Предупреждение",
"error": "Ошибки",
"trace": "Трассировка"
}
},
"whatsNew": {
"canvasV2Announcement": {
"newLayerTypes": "Новые типы слоев для еще большего контроля",
"readReleaseNotes": "Прочитать информацию о выпуске",
"watchReleaseVideo": "Смотреть видео о выпуске",
"fluxSupport": "Поддержка семейства моделей Flux",
"newCanvas": "Новый мощный холст управления",
"watchUiUpdatesOverview": "Обзор обновлений пользовательского интерфейса"
},
"whatsNewInInvoke": "Что нового в Invoke"
},
"newUserExperience": {
"toGetStarted": "Чтобы начать работу, введите в поле запрос и нажмите <StrongComponent>Invoke</StrongComponent>, чтобы сгенерировать первое изображение. Вы можете сохранить изображения непосредственно в <StrongComponent>Галерею</StrongComponent> или отредактировать их на <StrongComponent>Холсте</StrongComponent>.",
"gettingStartedSeries": "Хотите получить больше рекомендаций? Ознакомьтесь с нашей серией <LinkComponent>Getting Started Series</LinkComponent> для получения советов по раскрытию всего потенциала Invoke Studio."
}
}

View File

@@ -415,8 +415,7 @@
"resetUI": "$t(accessibility.reset) UI",
"createIssue": "创建问题",
"about": "关于",
"submitSupportTicket": "提交支持工单",
"toggleRightPanel": "切换右侧面板(G)"
"submitSupportTicket": "提交支持工单"
},
"nodes": {
"zoomInNodes": "放大",

View File

@@ -21,16 +21,12 @@ import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageMo
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/WorkflowListMenu/ShareWorkflowModal';
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { DeleteStylePresetDialog } from 'features/stylePresets/components/DeleteStylePresetDialog';
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal';
import { configChanged } from 'features/system/store/configSlice';
import { selectLanguage } from 'features/system/store/systemSelectors';
import { AppContent } from 'features/ui/components/AppContent';
import { DeleteWorkflowDialog } from 'features/workflowLibrary/components/DeleteLibraryWorkflowConfirmationAlertDialog';
import { NewWorkflowConfirmationAlertDialog } from 'features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog';
import { AnimatePresence } from 'framer-motion';
import i18n from 'i18n';
import { size } from 'lodash-es';
@@ -111,10 +107,6 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
<DynamicPromptsModal />
<StylePresetModal />
<ClearQueueConfirmationsAlertDialog />
<NewWorkflowConfirmationAlertDialog />
<DeleteStylePresetDialog />
<DeleteWorkflowDialog />
<ShareWorkflowModal />
<RefreshAfterResetModal />
<DeleteBoardModal />
<GlobalImageHotkeys />

View File

@@ -65,10 +65,10 @@ const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => {
</Text>
</Flex>
<Flex gap={4}>
<Button leftIcon={<PiArrowCounterClockwiseBold />} onClick={resetErrorBoundary}>
<Button leftIcon={<PiArrowCounterClockwiseBold />} onPointerUp={resetErrorBoundary}>
{t('accessibility.resetUI')}
</Button>
<Button leftIcon={<PiCopyBold />} onClick={handleCopy}>
<Button leftIcon={<PiCopyBold />} onPointerUp={handleCopy}>
{t('common.copyError')}
</Button>
<Link href={url} isExternal>

View File

@@ -1,7 +1,6 @@
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppSelector } from 'app/store/storeHooks';
import { useIsRegionFocused } from 'common/hooks/focus';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useImageActions } from 'features/gallery/hooks/useImageActions';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
@@ -12,7 +11,6 @@ import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
export const GlobalImageHotkeys = memo(() => {
useAssertSingleton('GlobalImageHotkeys');
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken);

View File

@@ -9,11 +9,11 @@ import { imageDTOToImageObject } from 'features/controlLayers/store/util';
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { sentImageToCanvas } from 'features/gallery/store/actions';
import { parseAndRecallAllMetadata } from 'features/metadata/util/handlers';
import { $isWorkflowListMenuIsOpen } from 'features/nodes/store/workflowListMenu';
import { $isStylePresetsMenuOpen, activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
import { toast } from 'features/toast/toast';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { useGetAndLoadLibraryWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow';
import { $workflowLibraryModal } from 'features/workflowLibrary/store/isWorkflowLibraryModalOpen';
import { useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { getImageDTO, getImageMetadata } from 'services/api/endpoints/images';
@@ -160,7 +160,7 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
case 'viewAllWorkflows':
// Go to the workflows tab and open the workflow library modal
store.dispatch(setActiveTab('workflows'));
$isWorkflowListMenuIsOpen.set(true);
$workflowLibraryModal.set(true);
break;
case 'viewAllStylePresets':
// Go to the canvas tab and open the style presets menu

View File

@@ -1,4 +1,3 @@
export const STORAGE_PREFIX = '@@invokeai-';
export const EMPTY_ARRAY = [];
/** @knipignore */
export const EMPTY_OBJECT = {};

View File

@@ -166,8 +166,8 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
reducer: rememberedRootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: import.meta.env.MODE === 'development',
immutableCheck: import.meta.env.MODE === 'development',
serializableCheck: false, // import.meta.env.MODE === 'development',
immutableCheck: false, // import.meta.env.MODE === 'development',
})
.concat(api.middleware)
.concat(dynamicMiddlewares)

View File

@@ -98,8 +98,8 @@ const RgbColorPicker = (props: Props) => {
export default memo(RgbColorPicker);
const ColorSwatch = ({ color, onChange }: { color: RgbColor; onChange: (color: RgbColor) => void }) => {
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
onChange(color);
}, [color, onChange]);
return <Box role="button" onClick={onClick} h={8} w={8} bg={rgbColorToString(color)} borderRadius="base" />;
return <Box role="button" onPointerUp={onPointerUp} h={8} w={8} bg={rgbColorToString(color)} borderRadius="base" />;
};

View File

@@ -109,8 +109,8 @@ const RgbaColorPicker = (props: Props) => {
export default memo(RgbaColorPicker);
const ColorSwatch = ({ color, onChange }: { color: RgbaColor; onChange: (color: RgbaColor) => void }) => {
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
onChange(color);
}, [color, onChange]);
return <Box role="button" onClick={onClick} h={8} w={8} bg={rgbaColorToString(color)} borderRadius="base" />;
return <Box role="button" onPointerUp={onPointerUp} h={8} w={8} bg={rgbaColorToString(color)} borderRadius="base" />;
};

View File

@@ -4,7 +4,6 @@ import { IAILoadingImageFallback, IAINoContentFallback } from 'common/components
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
import type { MouseEvent, ReactElement, ReactNode, SyntheticEvent } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { PiImageBold, PiUploadSimpleBold } from 'react-icons/pi';
@@ -55,7 +54,7 @@ type IAIDndImageProps = FlexProps & {
imageDTO: ImageDTO | undefined;
onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
onLoad?: (event: SyntheticEvent<HTMLImageElement>) => void;
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
onPointerUp?: (event: MouseEvent<HTMLDivElement>) => void;
withMetadataOverlay?: boolean;
isDragDisabled?: boolean;
isDropDisabled?: boolean;
@@ -82,7 +81,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
const {
imageDTO,
onError,
onClick,
onPointerUp,
withMetadataOverlay = false,
isDropDisabled = false,
isDragDisabled = false,
@@ -125,36 +124,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
[onMouseOut]
);
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
postUploadAction,
isDisabled: isUploadDisabled,
});
const uploadButtonStyles = useMemo<SystemStyleObject>(() => {
const styles: SystemStyleObject = {
minH: minSize,
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'base',
transitionProperty: 'common',
transitionDuration: '0.1s',
color: 'base.500',
};
if (!isUploadDisabled) {
Object.assign(styles, {
cursor: 'pointer',
bg: 'base.700',
_hover: {
bg: 'base.650',
color: 'base.300',
},
});
}
return styles;
}, [isUploadDisabled, minSize]);
const openInNewTab = useCallback(
(e: MouseEvent) => {
if (!imageDTO) {
@@ -169,10 +138,10 @@ const IAIDndImage = (props: IAIDndImageProps) => {
);
return (
<ImageContextMenu imageDTO={imageDTO}>
{(ref) => (
// <ImageContextMenu imageDTO={imageDTO}>
// {(ref) => (
<Flex
ref={ref}
// ref={ref}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
width="full"
@@ -216,28 +185,77 @@ const IAIDndImage = (props: IAIDndImageProps) => {
</Flex>
)}
{!imageDTO && !isUploadDisabled && (
<>
<Flex sx={uploadButtonStyles} {...getUploadButtonProps()}>
<input {...getUploadInputProps()} />
{uploadElement}
</Flex>
</>
<UploadButton
isUploadDisabled={isUploadDisabled}
postUploadAction={postUploadAction}
uploadElement={uploadElement}
minSize={minSize}
/>
)}
{!imageDTO && isUploadDisabled && noContentFallback}
{imageDTO && !isDragDisabled && (
<IAIDraggable
data={draggableData}
disabled={isDragDisabled || !imageDTO}
onClick={onClick}
onPointerUp={onPointerUp}
onAuxClick={openInNewTab}
/>
)}
{children}
{!isDropDisabled && <IAIDroppable data={droppableData} disabled={isDropDisabled} dropLabel={dropLabel} />}
</Flex>
)}
</ImageContextMenu>
// )}
// </ImageContextMenu>
);
};
export default memo(IAIDndImage);
const UploadButton = ({
isUploadDisabled,
postUploadAction,
uploadElement,
minSize,
}: {
isUploadDisabled: boolean;
postUploadAction?: PostUploadAction;
uploadElement: ReactNode;
minSize: number;
}) => {
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
postUploadAction,
isDisabled: isUploadDisabled,
});
const uploadButtonStyles = useMemo<SystemStyleObject>(() => {
const styles: SystemStyleObject = {
minH: minSize,
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'base',
transitionProperty: 'common',
transitionDuration: '0.1s',
color: 'base.500',
};
if (!isUploadDisabled) {
Object.assign(styles, {
cursor: 'pointer',
bg: 'base.700',
_hover: {
bg: 'base.650',
color: 'base.300',
},
});
}
return styles;
}, [isUploadDisabled, minSize]);
return (
<Flex sx={uploadButtonStyles} {...getUploadButtonProps()}>
<input {...getUploadInputProps()} />
{uploadElement}
</Flex>
);
};

View File

@@ -16,17 +16,17 @@ const sx: SystemStyleObject = {
},
};
type Props = Omit<IconButtonProps, 'aria-label' | 'onClick' | 'tooltip'> & {
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
type Props = Omit<IconButtonProps, 'aria-label' | 'onPointerUp' | 'tooltip'> & {
onPointerUp: (event: MouseEvent<HTMLButtonElement>) => void;
tooltip: string;
};
const IAIDndImageIcon = (props: Props) => {
const { onClick, tooltip, icon, ...rest } = props;
const { onPointerUp, tooltip, icon, ...rest } = props;
return (
<IconButton
onClick={onClick}
onPointerUp={onPointerUp}
aria-label={tooltip}
icon={icon}
variant="link"

View File

@@ -1,30 +0,0 @@
import type { MenuItemProps } from '@invoke-ai/ui-library';
import { Flex, MenuItem, Tooltip } from '@invoke-ai/ui-library';
import type { ReactNode } from 'react';
type Props = MenuItemProps & {
tooltip?: ReactNode;
icon: ReactNode;
};
export const IconMenuItem = ({ tooltip, icon, ...props }: Props) => {
return (
<Tooltip label={tooltip} placement="top" gutter={12}>
<MenuItem
display="flex"
alignItems="center"
justifyContent="center"
w="min-content"
aspectRatio="1"
borderRadius="base"
{...props}
>
{icon}
</MenuItem>
</Tooltip>
);
};
export const IconMenuItemGroup = ({ children }: { children: ReactNode }) => {
return <Flex gap={2}>{children}</Flex>;
};

View File

@@ -95,14 +95,14 @@ const Content = ({ data, feature, hideDisable }: ContentProps) => {
[feature, t]
);
const onClickLearnMore = useCallback(() => {
const onPointerUpLearnMore = useCallback(() => {
if (!data?.href) {
return;
}
window.open(data.href);
}, [data?.href]);
const onClickDontShowMeThese = useCallback(() => {
const onPointerUpDontShowMeThese = useCallback(() => {
dispatch(setShouldEnableInformationalPopovers(false));
toast({
title: t('settings.informationalPopoversDisabled'),
@@ -135,13 +135,13 @@ const Content = ({ data, feature, hideDisable }: ContentProps) => {
<Divider />
<Flex alignItems="center" justifyContent="space-between" w="full">
{!hideDisable && (
<Button onClick={onClickDontShowMeThese} variant="link" size="sm">
<Button onPointerUp={onPointerUpDontShowMeThese} variant="link" size="sm">
{t('common.dontShowMeThese')}
</Button>
)}
<Spacer />
{data?.href && (
<Button onClick={onClickLearnMore} leftIcon={<PiArrowSquareOutBold />} variant="link" size="sm">
<Button onPointerUp={onPointerUpLearnMore} leftIcon={<PiArrowSquareOutBold />} variant="link" size="sm">
{t('common.learnMore') ?? heading}
</Button>
)}

View File

@@ -51,7 +51,7 @@ export const buildUseBoolean = (initialValue: boolean): [() => UseBoolean, Writa
* Hook to manage a boolean state. Use this for a local boolean state.
* @param initialValue Initial value of the boolean
*/
export const useBoolean = (initialValue: boolean): UseBoolean => {
export const useBoolean = (initialValue: boolean) => {
const [isTrue, set] = useState(initialValue);
const setTrue = useCallback(() => {
@@ -72,82 +72,3 @@ export const useBoolean = (initialValue: boolean): UseBoolean => {
toggle,
};
};
type UseDisclosure = {
isOpen: boolean;
open: () => void;
close: () => void;
set: (isOpen: boolean) => void;
toggle: () => void;
};
/**
* This is the same as `buildUseBoolean`, but the method names are more descriptive,
* serving the semantics of a disclosure state.
*
* Creates a hook to manage a boolean state. The boolean is stored in a nanostores atom.
* Returns a tuple containing the hook and the atom. Use this for global boolean state.
*
* @param defaultIsOpen Initial state of the disclosure
*/
export const buildUseDisclosure = (defaultIsOpen: boolean): [() => UseDisclosure, WritableAtom<boolean>] => {
const $isOpen = atom(defaultIsOpen);
const open = () => {
$isOpen.set(true);
};
const close = () => {
$isOpen.set(false);
};
const set = (isOpen: boolean) => {
$isOpen.set(isOpen);
};
const toggle = () => {
$isOpen.set(!$isOpen.get());
};
const useDisclosure = () => {
const isOpen = useStore($isOpen);
return {
isOpen,
open,
close,
set,
toggle,
};
};
return [useDisclosure, $isOpen] as const;
};
/**
* This is the same as `useBoolean`, but the method names are more descriptive,
* serving the semantics of a disclosure state.
*
* Hook to manage a boolean state. Use this for a local boolean state.
* @param defaultIsOpen Initial state of the disclosure
*
* @knipignore
*/
export const useDisclosure = (defaultIsOpen: boolean): UseDisclosure => {
const [isOpen, set] = useState(defaultIsOpen);
const open = useCallback(() => {
set(true);
}, [set]);
const close = useCallback(() => {
set(false);
}, [set]);
const toggle = useCallback(() => {
set((val) => !val);
}, [set]);
return {
isOpen,
open,
close,
set,
toggle,
};
};

View File

@@ -3,7 +3,6 @@ import { Combobox, ConfirmationAlertDialog, Flex, FormControl, Text } from '@inv
import { createSelector } from '@reduxjs/toolkit';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import {
changeBoardReset,
isModalOpenChanged,
@@ -26,7 +25,6 @@ const selectIsModalOpen = createSelector(
);
const ChangeBoardModal = () => {
useAssertSingleton('ChangeBoardModal');
const dispatch = useAppDispatch();
const [selectedBoard, setSelectedBoard] = useState<string | null>();
const queryArgs = useAppSelector(selectListBoardsQueryArgs);

View File

@@ -33,7 +33,7 @@ export const CanvasAddEntityButtons = memo(() => {
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addGlobalReferenceImage}
onPointerUp={addGlobalReferenceImage}
isDisabled={isFLUX}
>
{t('controlLayers.globalReferenceImage')}
@@ -46,7 +46,7 @@ export const CanvasAddEntityButtons = memo(() => {
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addInpaintMask}
onPointerUp={addInpaintMask}
>
{t('controlLayers.inpaintMask')}
</Button>
@@ -55,7 +55,7 @@ export const CanvasAddEntityButtons = memo(() => {
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addRegionalGuidance}
onPointerUp={addRegionalGuidance}
isDisabled={isFLUX}
>
{t('controlLayers.regionalGuidance')}
@@ -65,7 +65,7 @@ export const CanvasAddEntityButtons = memo(() => {
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addRegionalReferenceImage}
onPointerUp={addRegionalReferenceImage}
isDisabled={isFLUX}
>
{t('controlLayers.regionalReferenceImage')}
@@ -79,7 +79,7 @@ export const CanvasAddEntityButtons = memo(() => {
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addControlLayer}
onPointerUp={addControlLayer}
isDisabled={isFLUX}
>
{t('controlLayers.controlLayer')}
@@ -89,7 +89,7 @@ export const CanvasAddEntityButtons = memo(() => {
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addRasterLayer}
onPointerUp={addRasterLayer}
>
{t('controlLayers.rasterLayer')}
</Button>

View File

@@ -17,12 +17,12 @@ import { Trans, useTranslation } from 'react-i18next';
const ActivateImageViewerButton = (props: PropsWithChildren) => {
const imageViewer = useImageViewer();
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
imageViewer.open();
selectCanvasRightPanelGalleryTab();
}, [imageViewer]);
return (
<Button onClick={onClick} size="sm" variant="link" color="base.50">
<Button onPointerUp={onPointerUp} size="sm" variant="link" color="base.50">
{props.children}
</Button>
);
@@ -58,13 +58,13 @@ export const CanvasAlertsSendingToGallery = () => {
const ActivateCanvasButton = (props: PropsWithChildren) => {
const dispatch = useAppDispatch();
const imageViewer = useImageViewer();
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
dispatch(setActiveTab('canvas'));
selectCanvasRightPanelLayersTab();
imageViewer.close();
}, [dispatch, imageViewer]);
return (
<Button onClick={onClick} size="sm" variant="link" color="base.50">
<Button onPointerUp={onPointerUp} size="sm" variant="link" color="base.50">
{props.children}
</Button>
);

View File

@@ -30,24 +30,24 @@ export const CanvasContextMenuGlobalMenuItems = memo(() => {
<CanvasContextMenuItemsCropCanvasToBbox />
</MenuGroup>
<MenuGroup title={t('controlLayers.canvasContextMenu.saveToGalleryGroup')}>
<MenuItem icon={<PiFloppyDiskBold />} isDisabled={isBusy} onClick={saveCanvasToGallery}>
<MenuItem icon={<PiFloppyDiskBold />} isDisabled={isBusy} onPointerUp={saveCanvasToGallery}>
{t('controlLayers.canvasContextMenu.saveCanvasToGallery')}
</MenuItem>
<MenuItem icon={<PiFloppyDiskBold />} isDisabled={isBusy} onClick={saveBboxToGallery}>
<MenuItem icon={<PiFloppyDiskBold />} isDisabled={isBusy} onPointerUp={saveBboxToGallery}>
{t('controlLayers.canvasContextMenu.saveBboxToGallery')}
</MenuItem>
</MenuGroup>
<MenuGroup title={t('controlLayers.canvasContextMenu.bboxGroup')}>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onClick={newGlobalReferenceImageFromBbox}>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onPointerUp={newGlobalReferenceImageFromBbox}>
{t('controlLayers.canvasContextMenu.newGlobalReferenceImage')}
</MenuItem>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onClick={newRegionalReferenceImageFromBbox}>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onPointerUp={newRegionalReferenceImageFromBbox}>
{t('controlLayers.canvasContextMenu.newRegionalReferenceImage')}
</MenuItem>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onClick={newControlLayerFromBbox}>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onPointerUp={newControlLayerFromBbox}>
{t('controlLayers.canvasContextMenu.newControlLayer')}
</MenuItem>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onClick={newRasterLayerFromBbox}>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onPointerUp={newRasterLayerFromBbox}>
{t('controlLayers.canvasContextMenu.newRasterLayer')}
</MenuItem>
</MenuGroup>

View File

@@ -17,7 +17,7 @@ export const CanvasContextMenuItemsCropCanvasToBbox = memo(() => {
}, [canvasManager]);
return (
<MenuItem icon={<PiCropBold />} isDisabled={isBusy} onClick={cropCanvasToBbox}>
<MenuItem icon={<PiCropBold />} isDisabled={isBusy} onPointerUp={cropCanvasToBbox}>
{t('controlLayers.canvasContextMenu.cropCanvasToBbox')}
</MenuItem>
);

View File

@@ -8,7 +8,6 @@ import type {
} from 'features/dnd/types';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const addRasterLayerFromImageDropData: AddRasterLayerFromImageDropData = {
id: 'add-raster-layer-from-image-drop-data',
@@ -31,7 +30,6 @@ const addGlobalReferenceImageFromImageDropData: AddGlobalReferenceImageFromImage
};
export const CanvasDropArea = memo(() => {
const { t } = useTranslation();
const imageViewer = useImageViewer();
if (imageViewer.isOpen) {
@@ -51,28 +49,16 @@ export const CanvasDropArea = memo(() => {
pointerEvents="none"
>
<GridItem position="relative">
<IAIDroppable
dropLabel={t('controlLayers.canvasContextMenu.newRasterLayer')}
data={addRasterLayerFromImageDropData}
/>
<IAIDroppable dropLabel="New Raster Layer" data={addRasterLayerFromImageDropData} />
</GridItem>
<GridItem position="relative">
<IAIDroppable
dropLabel={t('controlLayers.canvasContextMenu.newControlLayer')}
data={addControlLayerFromImageDropData}
/>
<IAIDroppable dropLabel="New Control Layer" data={addControlLayerFromImageDropData} />
</GridItem>
<GridItem position="relative">
<IAIDroppable
dropLabel={t('controlLayers.canvasContextMenu.newRegionalReferenceImage')}
data={addRegionalReferenceImageFromImageDropData}
/>
<IAIDroppable dropLabel="New Regional Reference Image" data={addRegionalReferenceImageFromImageDropData} />
</GridItem>
<GridItem position="relative">
<IAIDroppable
dropLabel={t('controlLayers.canvasContextMenu.newGlobalReferenceImage')}
data={addGlobalReferenceImageFromImageDropData}
/>
<IAIDroppable dropLabel="New Global Reference Image" data={addGlobalReferenceImageFromImageDropData} />
</GridItem>
</Grid>
</>

View File

@@ -40,26 +40,26 @@ export const EntityListGlobalActionBarAddLayerMenu = memo(() => {
/>
<MenuList>
<MenuGroup title={t('controlLayers.global')}>
<MenuItem icon={<PiPlusBold />} onClick={addGlobalReferenceImage} isDisabled={isFLUX}>
<MenuItem icon={<PiPlusBold />} onPointerUp={addGlobalReferenceImage} isDisabled={isFLUX}>
{t('controlLayers.globalReferenceImage')}
</MenuItem>
</MenuGroup>
<MenuGroup title={t('controlLayers.regional')}>
<MenuItem icon={<PiPlusBold />} onClick={addInpaintMask}>
<MenuItem icon={<PiPlusBold />} onPointerUp={addInpaintMask}>
{t('controlLayers.inpaintMask')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidance} isDisabled={isFLUX}>
<MenuItem icon={<PiPlusBold />} onPointerUp={addRegionalGuidance} isDisabled={isFLUX}>
{t('controlLayers.regionalGuidance')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRegionalReferenceImage} isDisabled={isFLUX}>
<MenuItem icon={<PiPlusBold />} onPointerUp={addRegionalReferenceImage} isDisabled={isFLUX}>
{t('controlLayers.regionalReferenceImage')}
</MenuItem>
</MenuGroup>
<MenuGroup title={t('controlLayers.layer_other')}>
<MenuItem icon={<PiPlusBold />} onClick={addControlLayer} isDisabled={isFLUX}>
<MenuItem icon={<PiPlusBold />} onPointerUp={addControlLayer} isDisabled={isFLUX}>
{t('controlLayers.controlLayer')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRasterLayer}>
<MenuItem icon={<PiPlusBold />} onPointerUp={addRasterLayer}>
{t('controlLayers.rasterLayer')}
</MenuItem>
</MenuGroup>

View File

@@ -12,7 +12,7 @@ export const EntityListSelectedEntityActionBarDuplicateButton = memo(() => {
const dispatch = useAppDispatch();
const isBusy = useCanvasIsBusy();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
if (!selectedEntityIdentifier) {
return;
}
@@ -21,7 +21,7 @@ export const EntityListSelectedEntityActionBarDuplicateButton = memo(() => {
return (
<IconButton
onClick={onClick}
onPointerUp={onPointerUp}
isDisabled={!selectedEntityIdentifier || isBusy}
size="sm"
variant="link"

View File

@@ -22,7 +22,7 @@ export const EntityListSelectedEntityActionBarFilterButton = memo(() => {
return (
<IconButton
onClick={filter.start}
onPointerUp={filter.start}
isDisabled={filter.isDisabled}
size="sm"
variant="link"

View File

@@ -157,7 +157,7 @@ export const EntityListSelectedEntityActionBarOpacity = memo(() => {
clampValueOnBlur={false}
variant="outline"
>
<NumberInputField paddingInlineEnd={7} _focusVisible={{ zIndex: 0 }} title="" />
<NumberInputField paddingInlineEnd={7} _focusVisible={{ zIndex: 0 }} />
<PopoverTrigger>
<IconButton
aria-label="open-slider"

View File

@@ -15,7 +15,7 @@ export const EntityListSelectedEntityActionBarSaveToAssetsButton = memo(() => {
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const adapter = useEntityAdapterSafe(selectedEntityIdentifier);
const saveLayerToAssets = useSaveLayerToAssets();
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
saveLayerToAssets(adapter);
}, [saveLayerToAssets, adapter]);
@@ -29,7 +29,7 @@ export const EntityListSelectedEntityActionBarSaveToAssetsButton = memo(() => {
return (
<IconButton
onClick={onClick}
onPointerUp={onPointerUp}
isDisabled={!selectedEntityIdentifier || isBusy}
size="sm"
variant="link"

View File

@@ -22,7 +22,7 @@ export const EntityListSelectedEntityActionBarTransformButton = memo(() => {
return (
<IconButton
onClick={transform.start}
onPointerUp={transform.start}
isDisabled={transform.isDisabled}
size="sm"
variant="link"

View File

@@ -20,7 +20,7 @@ export const CanvasRightPanel = memo(() => {
const { t } = useTranslation();
const tabIndex = useStore($canvasRightPanelTabIndex);
const imageViewer = useImageViewer();
const onClickViewerToggleButton = useCallback(() => {
const onPointerUpViewerToggleButton = useCallback(() => {
if ($canvasRightPanelTabIndex.get() !== 1) {
$canvasRightPanelTabIndex.set(1);
}
@@ -38,7 +38,7 @@ export const CanvasRightPanel = memo(() => {
<TabList alignItems="center">
<PanelTabs />
<Spacer />
<Button size="sm" variant="ghost" onClick={onClickViewerToggleButton}>
<Button size="sm" variant="ghost" onPointerUp={onPointerUpViewerToggleButton}>
{imageViewer.isOpen ? t('gallery.closeViewer') : t('gallery.openViewer')}
</Button>
</TabList>
@@ -99,7 +99,6 @@ const PanelTabs = memo(() => {
<Box as="span" w="full">
{layersTabLabel}
</Box>
{dndCtx.active && <Box position="absolute" top={0} left={0} right={0} bottom={0} border="2px solid red" />}
</Tab>
<Tab position="relative" onMouseOver={onOnMouseOverGalleryTab} onMouseOut={onMouseOut}>
{t('gallery.gallery')}

View File

@@ -84,7 +84,7 @@ export const ControlLayerControlAdapter = memo(() => {
<Flex w="full" gap={2}>
<ControlLayerControlAdapterModel modelKey={controlAdapter.model?.key ?? null} onChange={onChangeModel} />
<IconButton
onClick={filter.start}
onPointerUp={filter.start}
isDisabled={filter.isDisabled}
size="sm"
alignSelf="stretch"
@@ -94,7 +94,7 @@ export const ControlLayerControlAdapter = memo(() => {
icon={<PiShootingStarBold />}
/>
<IconButton
onClick={pullBboxIntoLayer}
onPointerUp={pullBboxIntoLayer}
isDisabled={isBusy}
size="sm"
alignSelf="stretch"

View File

@@ -1,5 +1,4 @@
import { MenuDivider } from '@invoke-ai/ui-library';
import { IconMenuItemGroup } from 'common/components/IconMenuItem';
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
@@ -15,20 +14,18 @@ import { memo } from 'react';
export const ControlLayerMenuItems = memo(() => {
return (
<>
<IconMenuItemGroup>
<CanvasEntityMenuItemsArrange />
<CanvasEntityMenuItemsDuplicate />
<CanvasEntityMenuItemsDelete />
</IconMenuItemGroup>
<MenuDivider />
<CanvasEntityMenuItemsTransform />
<CanvasEntityMenuItemsFilter />
<ControlLayerMenuItemsConvertControlToRaster />
<ControlLayerMenuItemsTransparencyEffect />
<MenuDivider />
<CanvasEntityMenuItemsArrange />
<MenuDivider />
<CanvasEntityMenuItemsCropToBbox />
<CanvasEntityMenuItemsDuplicate />
<CanvasEntityMenuItemsCopyToClipboard />
<CanvasEntityMenuItemsSave />
<CanvasEntityMenuItemsDelete />
</>
);
});

View File

@@ -18,7 +18,7 @@ export const ControlLayerMenuItemsConvertControlToRaster = memo(() => {
}, [dispatch, entityIdentifier]);
return (
<MenuItem onClick={convertControlLayerToRasterLayer} icon={<PiLightningBold />} isDisabled={!isInteractable}>
<MenuItem onPointerUp={convertControlLayerToRasterLayer} icon={<PiLightningBold />} isDisabled={!isInteractable}>
{t('controlLayers.convertToRasterLayer')}
</MenuItem>
);

View File

@@ -28,7 +28,7 @@ export const ControlLayerMenuItemsTransparencyEffect = memo(() => {
}, [dispatch, entityIdentifier]);
return (
<MenuItem onClick={onToggle} icon={<PiDropHalfBold />} isDisabled={!isInteractable}>
<MenuItem onPointerUp={onToggle} icon={<PiDropHalfBold />} isDisabled={!isInteractable}>
{withTransparencyEffect
? t('controlLayers.disableTransparencyEffect')
: t('controlLayers.enableTransparencyEffect')}

View File

@@ -109,7 +109,7 @@ const FilterContent = memo(
<Button
variant="ghost"
leftIcon={<PiShootingStarBold />}
onClick={adapter.filterer.processImmediate}
onPointerUp={adapter.filterer.processImmediate}
isLoading={isProcessing}
loadingText={t('controlLayers.filter.process')}
isDisabled={!isValid || autoProcessFilter}
@@ -119,7 +119,7 @@ const FilterContent = memo(
<Spacer />
<Button
leftIcon={<PiArrowsCounterClockwiseBold />}
onClick={adapter.filterer.reset}
onPointerUp={adapter.filterer.reset}
isLoading={isProcessing}
loadingText={t('controlLayers.filter.reset')}
variant="ghost"
@@ -129,7 +129,7 @@ const FilterContent = memo(
<Button
variant="ghost"
leftIcon={<PiCheckBold />}
onClick={adapter.filterer.apply}
onPointerUp={adapter.filterer.apply}
isLoading={isProcessing}
loadingText={t('controlLayers.filter.apply')}
isDisabled={!isValid || !hasProcessed}
@@ -139,7 +139,7 @@ const FilterContent = memo(
<Button
variant="ghost"
leftIcon={<PiXBold />}
onClick={adapter.filterer.cancel}
onPointerUp={adapter.filterer.cancel}
loadingText={t('controlLayers.filter.cancel')}
>
{t('controlLayers.filter.cancel')}

View File

@@ -69,7 +69,7 @@ export const IPAdapterImagePreview = memo(({ image, onChangeImage, droppableData
{controlImage && (
<Flex position="absolute" flexDir="column" top={2} insetInlineEnd={2} gap={1}>
<IAIDndImageIcon
onClick={handleResetControlImage}
onPointerUp={handleResetControlImage}
icon={<PiArrowCounterClockwiseBold size={16} />}
tooltip={t('common.reset')}
/>

View File

@@ -1,4 +1,4 @@
import { IconMenuItemGroup } from 'common/components/IconMenuItem';
import { MenuDivider } from '@invoke-ai/ui-library';
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
@@ -6,11 +6,12 @@ import { memo } from 'react';
export const IPAdapterMenuItems = memo(() => {
return (
<IconMenuItemGroup>
<>
<CanvasEntityMenuItemsArrange />
<MenuDivider />
<CanvasEntityMenuItemsDuplicate />
<CanvasEntityMenuItemsDelete />
</IconMenuItemGroup>
</>
);
});

View File

@@ -103,7 +103,7 @@ export const IPAdapterSettings = memo(() => {
/>
</Box>
<IconButton
onClick={pullBboxIntoIPAdapter}
onPointerUp={pullBboxIntoIPAdapter}
isDisabled={isBusy}
variant="ghost"
aria-label={t('controlLayers.pullBboxIntoReferenceImage')}

View File

@@ -1,5 +1,4 @@
import { MenuDivider } from '@invoke-ai/ui-library';
import { IconMenuItemGroup } from 'common/components/IconMenuItem';
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
@@ -10,15 +9,13 @@ import { memo } from 'react';
export const InpaintMaskMenuItems = memo(() => {
return (
<>
<IconMenuItemGroup>
<CanvasEntityMenuItemsArrange />
<CanvasEntityMenuItemsDuplicate />
<CanvasEntityMenuItemsDelete />
</IconMenuItemGroup>
<MenuDivider />
<CanvasEntityMenuItemsTransform />
<MenuDivider />
<CanvasEntityMenuItemsArrange />
<MenuDivider />
<CanvasEntityMenuItemsCropToBbox />
<CanvasEntityMenuItemsDuplicate />
<CanvasEntityMenuItemsDelete />
</>
);
});

View File

@@ -1,6 +1,5 @@
import { Checkbox, ConfirmationAlertDialog, Flex, FormControl, FormLabel, Text } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { buildUseBoolean } from 'common/hooks/useBoolean';
import { newCanvasSessionRequested, newGallerySessionRequested } from 'features/controlLayers/store/actions';
import {
@@ -66,7 +65,6 @@ export const useNewCanvasSession = () => {
};
export const NewGallerySessionDialog = memo(() => {
useAssertSingleton('NewGallerySessionDialog');
const { t } = useTranslation();
const dispatch = useAppDispatch();
@@ -102,7 +100,6 @@ export const NewGallerySessionDialog = memo(() => {
NewGallerySessionDialog.displayName = 'NewGallerySessionDialog';
export const NewCanvasSessionDialog = memo(() => {
useAssertSingleton('NewCanvasSessionDialog');
const { t } = useTranslation();
const dispatch = useAppDispatch();

View File

@@ -1,5 +1,4 @@
import { MenuDivider } from '@invoke-ai/ui-library';
import { IconMenuItemGroup } from 'common/components/IconMenuItem';
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
@@ -14,19 +13,17 @@ import { memo } from 'react';
export const RasterLayerMenuItems = memo(() => {
return (
<>
<IconMenuItemGroup>
<CanvasEntityMenuItemsArrange />
<CanvasEntityMenuItemsDuplicate />
<CanvasEntityMenuItemsDelete />
</IconMenuItemGroup>
<MenuDivider />
<CanvasEntityMenuItemsTransform />
<CanvasEntityMenuItemsFilter />
<RasterLayerMenuItemsConvertRasterToControl />
<MenuDivider />
<CanvasEntityMenuItemsArrange />
<MenuDivider />
<CanvasEntityMenuItemsCropToBbox />
<CanvasEntityMenuItemsDuplicate />
<CanvasEntityMenuItemsCopyToClipboard />
<CanvasEntityMenuItemsSave />
<CanvasEntityMenuItemsDelete />
</>
);
});

View File

@@ -15,7 +15,7 @@ export const RasterLayerMenuItemsConvertRasterToControl = memo(() => {
const defaultControlAdapter = useAppSelector(selectDefaultControlAdapter);
const isInteractable = useIsEntityInteractable(entityIdentifier);
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
dispatch(
rasterLayerConvertedToControlLayer({
entityIdentifier,
@@ -27,7 +27,7 @@ export const RasterLayerMenuItemsConvertRasterToControl = memo(() => {
}, [defaultControlAdapter, dispatch, entityIdentifier]);
return (
<MenuItem onClick={onClick} icon={<PiLightningBold />} isDisabled={!isInteractable}>
<MenuItem onPointerUp={onPointerUp} icon={<PiLightningBold />} isDisabled={!isInteractable}>
{t('controlLayers.convertToControlLayer')}
</MenuItem>
);

View File

@@ -30,7 +30,7 @@ export const RegionalGuidanceAddPromptsIPAdapterButtons = () => {
size="sm"
variant="ghost"
leftIcon={<PiPlusBold />}
onClick={addRegionalGuidancePositivePrompt}
onPointerUp={addRegionalGuidancePositivePrompt}
isDisabled={!validActions.canAddPositivePrompt}
>
{t('controlLayers.prompt')}
@@ -39,12 +39,12 @@ export const RegionalGuidanceAddPromptsIPAdapterButtons = () => {
size="sm"
variant="ghost"
leftIcon={<PiPlusBold />}
onClick={addRegionalGuidanceNegativePrompt}
onPointerUp={addRegionalGuidanceNegativePrompt}
isDisabled={!validActions.canAddNegativePrompt}
>
{t('controlLayers.negativePrompt')}
</Button>
<Button size="sm" variant="ghost" leftIcon={<PiPlusBold />} onClick={addRegionalGuidanceIPAdapter}>
<Button size="sm" variant="ghost" leftIcon={<PiPlusBold />} onPointerUp={addRegionalGuidanceIPAdapter}>
{t('controlLayers.referenceImage')}
</Button>
</Flex>

View File

@@ -15,7 +15,7 @@ export const RegionalGuidanceDeletePromptButton = memo(({ onDelete }: Props) =>
variant="link"
aria-label={t('controlLayers.deletePrompt')}
icon={<PiTrashSimpleFill />}
onClick={onDelete}
onPointerUp={onDelete}
flexGrow={0}
size="sm"
p={0}

View File

@@ -120,7 +120,7 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Pro
icon={<PiTrashSimpleFill />}
tooltip={t('controlLayers.deleteReferenceImage')}
aria-label={t('controlLayers.deleteReferenceImage')}
onClick={onDeleteIPAdapter}
onPointerUp={onDeleteIPAdapter}
colorScheme="error"
/>
</Flex>
@@ -135,7 +135,7 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Pro
/>
</Box>
<IconButton
onClick={pullBboxIntoIPAdapter}
onPointerUp={pullBboxIntoIPAdapter}
isDisabled={isBusy}
variant="ghost"
aria-label={t('controlLayers.pullBboxIntoReferenceImage')}

View File

@@ -1,4 +1,4 @@
import { Flex, MenuDivider } from '@invoke-ai/ui-library';
import { MenuDivider } from '@invoke-ai/ui-library';
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
@@ -11,18 +11,16 @@ import { memo } from 'react';
export const RegionalGuidanceMenuItems = memo(() => {
return (
<>
<Flex gap={2}>
<CanvasEntityMenuItemsArrange />
<CanvasEntityMenuItemsDuplicate />
<CanvasEntityMenuItemsDelete />
</Flex>
<MenuDivider />
<RegionalGuidanceMenuItemsAddPromptsAndIPAdapter />
<MenuDivider />
<CanvasEntityMenuItemsTransform />
<RegionalGuidanceMenuItemsAutoNegative />
<MenuDivider />
<CanvasEntityMenuItemsArrange />
<MenuDivider />
<CanvasEntityMenuItemsCropToBbox />
<CanvasEntityMenuItemsDuplicate />
<CanvasEntityMenuItemsDelete />
</>
);
});

View File

@@ -26,13 +26,19 @@ export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => {
return (
<>
<MenuItem onClick={addRegionalGuidancePositivePrompt} isDisabled={!validActions.canAddPositivePrompt || isBusy}>
<MenuItem
onPointerUp={addRegionalGuidancePositivePrompt}
isDisabled={!validActions.canAddPositivePrompt || isBusy}
>
{t('controlLayers.addPositivePrompt')}
</MenuItem>
<MenuItem onClick={addRegionalGuidanceNegativePrompt} isDisabled={!validActions.canAddNegativePrompt || isBusy}>
<MenuItem
onPointerUp={addRegionalGuidanceNegativePrompt}
isDisabled={!validActions.canAddNegativePrompt || isBusy}
>
{t('controlLayers.addNegativePrompt')}
</MenuItem>
<MenuItem onClick={addRegionalGuidanceIPAdapter} isDisabled={isBusy}>
<MenuItem onPointerUp={addRegionalGuidanceIPAdapter} isDisabled={isBusy}>
{t('controlLayers.addReferenceImage')}
</MenuItem>
</>

View File

@@ -17,12 +17,12 @@ export const RegionalGuidanceMenuItemsAutoNegative = memo(() => {
[entityIdentifier]
);
const autoNegative = useAppSelector(selectAutoNegative);
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
dispatch(rgAutoNegativeToggled({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem icon={<PiSelectionInverseBold />} onClick={onClick}>
<MenuItem icon={<PiSelectionInverseBold />} onPointerUp={onPointerUp}>
{autoNegative ? t('controlLayers.disableAutoNegative') : t('controlLayers.enableAutoNegative')}
</MenuItem>
);

View File

@@ -10,7 +10,7 @@ export const CanvasSettingsClearCachesButton = memo(() => {
canvasManager.cache.clearAll();
}, [canvasManager]);
return (
<Button onClick={clearCaches} size="sm" colorScheme="warning">
<Button onPointerUp={clearCaches} size="sm" colorScheme="warning">
{t('controlLayers.clearCaches')}
</Button>
);

View File

@@ -7,11 +7,11 @@ import { useTranslation } from 'react-i18next';
export const CanvasSettingsClearHistoryButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
dispatch(canvasClearHistory());
}, [dispatch]);
return (
<Button onClick={onClick} size="sm">
<Button onPointerUp={onPointerUp} size="sm">
{t('controlLayers.clearHistory')}
</Button>
);

View File

@@ -6,11 +6,11 @@ import { useTranslation } from 'react-i18next';
export const CanvasSettingsLogDebugInfoButton = memo(() => {
const { t } = useTranslation();
const canvasManager = useCanvasManager();
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
canvasManager.logDebugInfo();
}, [canvasManager]);
return (
<Button onClick={onClick} size="sm">
<Button onPointerUp={onPointerUp} size="sm">
{t('controlLayers.logDebugInfo')}
</Button>
);

View File

@@ -6,14 +6,14 @@ import { useTranslation } from 'react-i18next';
export const CanvasSettingsRecalculateRectsButton = memo(() => {
const { t } = useTranslation();
const canvasManager = useCanvasManager();
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
for (const adapter of canvasManager.getAllAdapters()) {
adapter.transformer.requestRectCalculation();
}
}, [canvasManager]);
return (
<Button onClick={onClick} size="sm" colorScheme="warning">
<Button onPointerUp={onPointerUp} size="sm" colorScheme="warning">
{t('controlLayers.recalculateRects')}
</Button>
);

View File

@@ -60,7 +60,7 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
tooltip={`${t('common.accept')} (Enter)`}
aria-label={`${t('common.accept')} (Enter)`}
icon={<PiCheckBold />}
onClick={acceptSelected}
onPointerUp={acceptSelected}
colorScheme="invokeBlue"
isDisabled={!selectedImage}
/>

View File

@@ -18,7 +18,7 @@ export const StagingAreaToolbarDiscardAllButton = memo(() => {
tooltip={`${t('controlLayers.stagingArea.discardAll')} (Esc)`}
aria-label={t('controlLayers.stagingArea.discardAll')}
icon={<PiTrashSimpleBold />}
onClick={discardAll}
onPointerUp={discardAll}
colorScheme="error"
fontSize={16}
/>

View File

@@ -35,7 +35,7 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(() => {
tooltip={t('controlLayers.stagingArea.discard')}
aria-label={t('controlLayers.stagingArea.discard')}
icon={<PiXBold />}
onClick={discardSelected}
onPointerUp={discardSelected}
colorScheme="invokeBlue"
fontSize={16}
isDisabled={!selectedImage}

View File

@@ -40,7 +40,7 @@ export const StagingAreaToolbarNextButton = memo(() => {
tooltip={`${t('controlLayers.stagingArea.next')} (Right)`}
aria-label={`${t('controlLayers.stagingArea.next')} (Right)`}
icon={<PiArrowRightBold />}
onClick={selectNext}
onPointerUp={selectNext}
colorScheme="invokeBlue"
isDisabled={imageCount <= 1 || !shouldShowStagedImage}
/>

View File

@@ -40,7 +40,7 @@ export const StagingAreaToolbarPrevButton = memo(() => {
tooltip={`${t('controlLayers.stagingArea.previous')} (Left)`}
aria-label={`${t('controlLayers.stagingArea.previous')} (Left)`}
icon={<PiArrowLeftBold />}
onClick={selectPrev}
onPointerUp={selectPrev}
colorScheme="invokeBlue"
isDisabled={imageCount <= 1 || !shouldShowStagedImage}
/>

View File

@@ -40,7 +40,7 @@ export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
tooltip={t('controlLayers.stagingArea.saveToGallery')}
aria-label={t('controlLayers.stagingArea.saveToGallery')}
icon={<PiFloppyDiskBold />}
onClick={saveSelectedImageToGallery}
onPointerUp={saveSelectedImageToGallery}
colorScheme="invokeBlue"
isDisabled={!selectedImage || !selectedImage.imageDTO.is_intermediate}
/>

View File

@@ -29,7 +29,7 @@ export const StagingAreaToolbarToggleShowResultsButton = memo(() => {
}
data-alert={!shouldShowStagedImage}
icon={shouldShowStagedImage ? <PiEyeBold /> : <PiEyeSlashBold />}
onClick={toggleShowResults}
onPointerUp={toggleShowResults}
colorScheme="invokeBlue"
/>
);

View File

@@ -27,7 +27,7 @@ export const ToolBboxButton = memo(() => {
icon={<PiBoundingBoxBold />}
colorScheme={isSelected ? 'invokeBlue' : 'base'}
variant="solid"
onClick={selectBbox}
onPointerUp={selectBbox}
/>
</Tooltip>
);

View File

@@ -27,7 +27,7 @@ export const ToolBrushButton = memo(() => {
icon={<PiPaintBrushBold />}
colorScheme={isSelected ? 'invokeBlue' : 'base'}
variant="solid"
onClick={selectBrush}
onPointerUp={selectBrush}
/>
</Tooltip>
);

View File

@@ -164,7 +164,7 @@ export const ToolBrushWidth = memo(() => {
onKeyDown={onKeyDown}
clampValueOnBlur={false}
>
<NumberInputField _focusVisible={{ zIndex: 0 }} title="" paddingInlineEnd={7} />
<NumberInputField paddingInlineEnd={7} />
<PopoverTrigger>
<IconButton
aria-label="open-slider"

View File

@@ -27,7 +27,7 @@ export const ToolColorPickerButton = memo(() => {
icon={<PiEyedropperBold />}
colorScheme={isSelected ? 'invokeBlue' : 'base'}
variant="solid"
onClick={selectColorPicker}
onPointerUp={selectColorPicker}
/>
</Tooltip>
);

View File

@@ -27,7 +27,7 @@ export const ToolEraserButton = memo(() => {
icon={<PiEraserBold />}
colorScheme={isSelected ? 'invokeBlue' : 'base'}
variant="solid"
onClick={selectEraser}
onPointerUp={selectEraser}
/>
</Tooltip>
);

View File

@@ -167,7 +167,7 @@ export const ToolEraserWidth = memo(() => {
onKeyDown={onKeyDown}
clampValueOnBlur={false}
>
<NumberInputField _focusVisible={{ zIndex: 0 }} title="" paddingInlineEnd={7} />
<NumberInputField paddingInlineEnd={7} />
<PopoverTrigger>
<IconButton
aria-label="open-slider"

View File

@@ -27,7 +27,7 @@ export const ToolMoveButton = memo(() => {
icon={<PiCursorBold />}
colorScheme={isSelected ? 'invokeBlue' : 'base'}
variant="solid"
onClick={selectMove}
onPointerUp={selectMove}
/>
</Tooltip>
);

View File

@@ -27,7 +27,7 @@ export const ToolRectButton = memo(() => {
icon={<PiRectangleBold />}
colorScheme={isSelected ? 'invokeBlue' : 'base'}
variant="solid"
onClick={selectRect}
onPointerUp={selectRect}
/>
</Tooltip>
);

Some files were not shown because too many files have changed in this diff Show More