Compare commits

...

39 Commits

Author SHA1 Message Date
psychedelicious
84c9ecc83f chore: bump version to v5.7.1 2025-02-28 13:23:30 -05:00
Thomas Bolteau
52aa839b7e translationBot(ui): update translation (French)
Currently translated at 99.1% (1782 of 1797 strings)

Co-authored-by: Thomas Bolteau <thomas.bolteau50@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/fr/
Translation: InvokeAI/Web UI
2025-02-28 17:07:11 +11:00
Hiroto N
316ed1d478 translationBot(ui): update translation (Japanese)
Currently translated at 42.6% (766 of 1797 strings)

Co-authored-by: Hiroto N <hironow365@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ja/
Translation: InvokeAI/Web UI
2025-02-28 17:07:11 +11:00
Hosted Weblate
3519e8ae39 translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2025-02-28 17:07:11 +11:00
psychedelicious
82f645c7a1 feat(ui): add new workflow button to library menu 2025-02-28 16:06:02 +11:00
psychedelicious
cc36cfb617 feat(ui): reorg workflow menu buttons 2025-02-28 16:06:02 +11:00
psychedelicious
ded8a84284 feat(ui): increase spacing in form builder view mode 2025-02-28 16:06:02 +11:00
psychedelicious
94771ea626 feat(ui): add auto-links to text, heading, field description and workflow descriptions 2025-02-28 16:06:02 +11:00
psychedelicious
51d661023e Revert "feat(ui): increase spacing in form builder view mode"
This reverts commit 3766a3ba1e082f31bce09f794c47eb95cd76f1b1.
2025-02-28 16:06:02 +11:00
psychedelicious
d215829b91 feat(ui): increase spacing in form builder view mode 2025-02-28 16:06:02 +11:00
psychedelicious
fad6c67f01 fix(ui): workflow description cut off 2025-02-28 16:06:02 +11:00
psychedelicious
f366640d46 fix(ui): invoke button not showing loading indicator on canvas tab
On the Canvas tab, when we made the network request to enqueue a batch, we were immediately resetting the request. This effectively disabled RTKQ's tracking of the request - including the loading state.

As a result, when you click the Invoke button on the Canvas tab, it didn't show a spinner, and it was not clear that anything was happening.

The solution is simple - just await the enqueue request before resetting the tracking, same as we already did on the workflows and upscaling tabs.

I also added some extra logging messages for enqueuing, so we get the same JS console logs for each tab on success or failure.
2025-02-28 15:58:17 +11:00
skunkworxdark
36a3fba8cb Update metadata_linked.py
Fix input type of default_value on MetadataToFloatInvocation
2025-02-27 04:55:29 -05:00
psychedelicious
b2ff83092f fix(ui): form element settings obscured by container 2025-02-27 14:49:52 +11:00
psychedelicious
d2db38a5b9 chore(ui): update whats new 2025-02-27 13:01:07 +11:00
psychedelicious
fa988a6273 chore: bump version to v5.7.0 2025-02-27 13:01:07 +11:00
HAL
149f60946c translationBot(ui): update translation (Japanese)
Currently translated at 37.7% (680 of 1801 strings)

Co-authored-by: HAL <HALQME@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ja/
Translation: InvokeAI/Web UI
2025-02-27 12:42:03 +11:00
Hiroto N
ee9d620a36 translationBot(ui): update translation (Japanese)
Currently translated at 40.3% (727 of 1801 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 37.7% (680 of 1801 strings)

Co-authored-by: Hiroto N <hironow365@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ja/
Translation: InvokeAI/Web UI
2025-02-27 12:42:03 +11:00
psychedelicious
4e8ce4abab feat(app): more detailed messages when loading custom nodes 2025-02-27 12:39:37 +11:00
psychedelicious
d40f2fa37c feat(app): improved custom load loading ordering
Previously, custom node loading occurred _during module imports_. A consequence of this is that when a custom node import fails (e.g. its type clobbers an existing node), the app fails to start up.

In fact, any time we import basically anything from the app, we trigger custom node imports! Not good.

This logic is now in its own function, called as the API app starts up.

If a custom node load fails for any reason, it no longer prevents the app from starting up.

One other bonus we get from this is that we can now ensure custom nodes are loaded _after_ core nodes.

Any clobbering that may occur while loading custom nodes is now guaranteed to be a custom node clobbering a core node's type - and not the other way round.
2025-02-27 12:39:37 +11:00
psychedelicious
933f4f6857 feat(app): improve error messages when registering invocations and they clobber 2025-02-27 12:39:37 +11:00
psychedelicious
f499b2db7b feat(app): add get_invocation_for_type method to BaseInvocation 2025-02-27 12:39:37 +11:00
psychedelicious
706aaf7460 tidy(app): remove unused variable 2025-02-27 12:39:37 +11:00
psychedelicious
4a706d00bb feat(app): use generic for append_list util 2025-02-27 12:28:00 +11:00
psychedelicious
2a8bff601f chore(ui): typegen 2025-02-27 12:28:00 +11:00
psychedelicious
3f0e3192f6 chore(app): mark metadata_field_extractor as deprecated 2025-02-27 12:28:00 +11:00
psychedelicious
c65147e2ff feat(app): adopt @skunkworxdark's popular metadata nodes
Thank you!
2025-02-27 12:28:00 +11:00
psychedelicious
1c14e257a3 feat(app): do not pull PIL image from disk in image primitive 2025-02-27 12:19:27 +11:00
psychedelicious
fe24217082 fix(ui): image usage checks collection fields
When deleting a board w/ images, the image usage checking logic was not checking image collection fields. This could result in a nonexistent image lingering in a node.

We already handle single image fields correctly, it's only the image collection fields taht were affected.
2025-02-27 10:24:59 +11:00
psychedelicious
aee847065c revert(ui): images from board generator only works on boards 2025-02-27 10:19:13 +11:00
psychedelicious
525da3257c chore(ui): typegen 2025-02-27 10:19:13 +11:00
psychedelicious
559654f0ca revert(app): get_all_board_image_names_for_board requires board_id 2025-02-27 10:19:13 +11:00
Eugene Brodsky
5d33874d58 fix(backend): ValuesToInsertTuple.retried_from_item_id should be an int 2025-02-27 07:35:41 +11:00
Mary Hipp
0063315139 fix(api): add new args to all uses of get_all_board_image_names_for_board 2025-02-26 15:05:40 -05:00
psychedelicious
1cbd609860 chore: bump version to v5.7.0rc2 2025-02-26 21:04:23 +11:00
psychedelicious
047c643295 tidy(app): document & clean up batch prep logic 2025-02-26 21:04:23 +11:00
psychedelicious
d1e03aa1c5 tidy(app): remove timing debug logs 2025-02-26 21:04:23 +11:00
psychedelicious
1bb8edf57e perf(app): optimise batch prep logic even more
Found another place where we deepcopy a dict, but it is safe to mutate.

Restructured the prep logic a bit to support this. Updated tests to use the new structure.
2025-02-26 21:04:23 +11:00
psychedelicious
a3e78f0db6 perf(app): optimise batch prep logic
- Avoid pydantic models when dict manipulation works
- Avoid extraneous deep copies when we can safely mutate
- Avoid NamedTuple construct and its overhead
- Fix tests to use altered function signatures
- Remove extraneous populate_graph function
2025-02-26 21:04:23 +11:00
49 changed files with 3575 additions and 398 deletions

View File

@@ -1,4 +1,4 @@
from typing import Literal, Optional, Union
from typing import Optional, Union
from fastapi import Body, HTTPException, Path, Query
from fastapi.routing import APIRouter
@@ -146,7 +146,7 @@ async def list_boards(
response_model=list[str],
)
async def list_all_board_image_names(
board_id: str | Literal["none"] = Path(description="The id of the board"),
board_id: str = Path(description="The id of the board"),
categories: list[ImageCategory] | None = Query(default=None, description="The categories of image to include."),
is_intermediate: bool | None = Query(default=None, description="Whether to list intermediate images."),
) -> list[str]:

View File

@@ -36,6 +36,7 @@ from invokeai.app.api.routers import (
workflows,
)
from invokeai.app.api.sockets import SocketIO
from invokeai.app.invocations.load_custom_nodes import load_custom_nodes
from invokeai.app.services.config.config_default import get_config
from invokeai.app.util.custom_openapi import get_openapi_func
from invokeai.backend.util.devices import TorchDevice
@@ -63,6 +64,11 @@ loop = asyncio.new_event_loop()
# the correct port when the server starts in the lifespan handler.
port = app_config.port
# Load custom nodes. This must be done after importing the Graph class, which itself imports all modules from the
# invocations module. The ordering here is implicit, but important - we want to load custom nodes after all the
# core nodes have been imported so that we can catch when a custom node clobbers a core node.
load_custom_nodes(custom_nodes_path=app_config.custom_nodes_path)
@asynccontextmanager
async def lifespan(app: FastAPI):

View File

@@ -1,33 +1,5 @@
import shutil
import sys
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
from invokeai.app.services.config.config_default import get_config
custom_nodes_path = Path(get_config().custom_nodes_path)
custom_nodes_path.mkdir(parents=True, exist_ok=True)
custom_nodes_init_path = str(custom_nodes_path / "__init__.py")
custom_nodes_readme_path = str(custom_nodes_path / "README.md")
# copy our custom nodes __init__.py to the custom nodes directory
shutil.copy(Path(__file__).parent / "custom_nodes/init.py", custom_nodes_init_path)
shutil.copy(Path(__file__).parent / "custom_nodes/README.md", custom_nodes_readme_path)
# set the same permissions as the destination directory, in case our source is read-only,
# so that the files are user-writable
for p in custom_nodes_path.glob("**/*"):
p.chmod(custom_nodes_path.stat().st_mode)
# Import custom nodes, see https://docs.python.org/3/library/importlib.html#importing-programmatically
spec = spec_from_file_location("custom_nodes", custom_nodes_init_path)
if spec is None or spec.loader is None:
raise RuntimeError(f"Could not load custom nodes from {custom_nodes_init_path}")
module = module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
# add core nodes to __all__
python_files = filter(lambda f: not f.name.startswith("_"), Path(__file__).parent.glob("*.py"))
__all__ = [f.stem for f in python_files] # type: ignore

View File

@@ -44,8 +44,6 @@ if TYPE_CHECKING:
logger = InvokeAILogger.get_logger()
CUSTOM_NODE_PACK_SUFFIX = "__invokeai-custom-node"
class InvalidVersionError(ValueError):
pass
@@ -240,6 +238,11 @@ class BaseInvocation(ABC, BaseModel):
"""Gets the invocation's output annotation (i.e. the return annotation of its `invoke()` method)."""
return signature(cls.invoke).return_annotation
@classmethod
def get_invocation_for_type(cls, invocation_type: str) -> BaseInvocation | None:
"""Gets the invocation class for a given invocation type."""
return cls.get_invocations_map().get(invocation_type)
@staticmethod
def json_schema_extra(schema: dict[str, Any], model_class: Type[BaseInvocation]) -> None:
"""Adds various UI-facing attributes to the invocation's OpenAPI schema."""
@@ -446,8 +449,27 @@ def invocation(
if re.compile(r"^\S+$").match(invocation_type) is None:
raise ValueError(f'"invocation_type" must consist of non-whitespace characters, got "{invocation_type}"')
# The node pack is the module name - will be "invokeai" for built-in nodes
node_pack = cls.__module__.split(".")[0]
# Handle the case where an existing node is being clobbered by the one we are registering
if invocation_type in BaseInvocation.get_invocation_types():
raise ValueError(f'Invocation type "{invocation_type}" already exists')
clobbered_invocation = BaseInvocation.get_invocation_for_type(invocation_type)
# This should always be true - we just checked if the invocation type was in the set
assert clobbered_invocation is not None
clobbered_node_pack = clobbered_invocation.UIConfig.node_pack
if clobbered_node_pack == "invokeai":
# The node being clobbered is a core node
raise ValueError(
f'Cannot load node "{invocation_type}" from node pack "{node_pack}" - a core node with the same type already exists'
)
else:
# The node being clobbered is a custom node
raise ValueError(
f'Cannot load node "{invocation_type}" from node pack "{node_pack}" - a node with the same type already exists in node pack "{clobbered_node_pack}"'
)
validate_fields(cls.model_fields, invocation_type)
@@ -457,8 +479,7 @@ def invocation(
uiconfig["tags"] = tags
uiconfig["category"] = category
uiconfig["classification"] = classification
# The node pack is the module name - will be "invokeai" for built-in nodes
uiconfig["node_pack"] = cls.__module__.split(".")[0]
uiconfig["node_pack"] = node_pack
if version is not None:
try:

View File

@@ -10,10 +10,12 @@ from pathlib import Path
from invokeai.backend.util.logging import InvokeAILogger
logger = InvokeAILogger.get_logger()
loaded_count = 0
loaded_packs: list[str] = []
failed_packs: list[str] = []
custom_nodes_dir = Path(__file__).parent
for d in Path(__file__).parent.iterdir():
for d in custom_nodes_dir.iterdir():
# skip files
if not d.is_dir():
continue
@@ -47,12 +49,16 @@ for d in Path(__file__).parent.iterdir():
sys.modules[spec.name] = module
spec.loader.exec_module(module)
loaded_count += 1
loaded_packs.append(module_name)
except Exception:
failed_packs.append(module_name)
full_error = traceback.format_exc()
logger.error(f"Failed to load node pack {module_name}:\n{full_error}")
logger.error(f"Failed to load node pack {module_name} (may have partially loaded):\n{full_error}")
del init, module_name
loaded_count = len(loaded_packs)
if loaded_count > 0:
logger.info(f"Loaded {loaded_count} node packs from {Path(__file__).parent}")
logger.info(
f"Loaded {loaded_count} node pack{'s' if loaded_count != 1 else ''} from {custom_nodes_dir}: {', '.join(loaded_packs)}"
)

View File

@@ -0,0 +1,40 @@
import shutil
import sys
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
def load_custom_nodes(custom_nodes_path: Path):
"""
Loads all custom nodes from the custom_nodes_path directory.
This function copies a custom __init__.py file to the custom_nodes_path directory, effectively turning it into a
python module.
The custom __init__.py file itself imports all the custom node packs as python modules from the custom_nodes_path
directory.
Then,the custom __init__.py file is programmatically imported using importlib. As it executes, it imports all the
custom node packs as python modules.
"""
custom_nodes_path.mkdir(parents=True, exist_ok=True)
custom_nodes_init_path = str(custom_nodes_path / "__init__.py")
custom_nodes_readme_path = str(custom_nodes_path / "README.md")
# copy our custom nodes __init__.py to the custom nodes directory
shutil.copy(Path(__file__).parent / "custom_nodes/init.py", custom_nodes_init_path)
shutil.copy(Path(__file__).parent / "custom_nodes/README.md", custom_nodes_readme_path)
# set the same permissions as the destination directory, in case our source is read-only,
# so that the files are user-writable
for p in custom_nodes_path.glob("**/*"):
p.chmod(custom_nodes_path.stat().st_mode)
# Import custom nodes, see https://docs.python.org/3/library/importlib.html#importing-programmatically
spec = spec_from_file_location("custom_nodes", custom_nodes_init_path)
if spec is None or spec.loader is None:
raise RuntimeError(f"Could not load custom nodes from {custom_nodes_init_path}")
module = module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)

View File

@@ -284,6 +284,7 @@ class CoreMetadataInvocation(BaseInvocation):
tags=["metadata"],
category="metadata",
version="1.0.0",
classification=Classification.Deprecated,
)
class MetadataFieldExtractorInvocation(BaseInvocation):
"""Extracts the text value from an image's metadata given a key.

File diff suppressed because it is too large Load Diff

View File

@@ -265,13 +265,9 @@ class ImageInvocation(BaseInvocation):
image: ImageField = InputField(description="The image to load")
def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.images.get_pil(self.image.image_name)
image_dto = context.images.get_dto(self.image.image_name)
return ImageOutput(
image=ImageField(image_name=self.image.image_name),
width=image.width,
height=image.height,
)
return ImageOutput.build(image_dto=image_dto)
@invocation(

View File

@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from typing import Literal, Optional
from typing import Optional
from invokeai.app.services.image_records.image_records_common import ImageCategory
@@ -27,7 +27,7 @@ class BoardImageRecordStorageBase(ABC):
@abstractmethod
def get_all_board_image_names_for_board(
self,
board_id: str | Literal["none"],
board_id: str,
categories: list[ImageCategory] | None,
is_intermediate: bool | None,
) -> list[str]:

View File

@@ -1,6 +1,6 @@
import sqlite3
import threading
from typing import Literal, Optional, cast
from typing import Optional, cast
from invokeai.app.services.board_image_records.board_image_records_base import BoardImageRecordStorageBase
from invokeai.app.services.image_records.image_records_common import (
@@ -103,7 +103,7 @@ class SqliteBoardImageRecordStorage(BoardImageRecordStorageBase):
def get_all_board_image_names_for_board(
self,
board_id: str | Literal["none"],
board_id: str,
categories: list[ImageCategory] | None,
is_intermediate: bool | None,
) -> list[str]:
@@ -118,14 +118,9 @@ class SqliteBoardImageRecordStorage(BoardImageRecordStorageBase):
FROM images
LEFT JOIN board_images ON board_images.image_name = images.image_name
WHERE 1=1
AND board_images.board_id = ?
"""
# Add the board_id filter
if board_id == "none":
stmt += "AND board_images.board_id IS NULL"
else:
stmt += "AND board_images.board_id = ?"
params.append(board_id)
params.append(board_id)
# Add the category filter
if categories is not None:

View File

@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from typing import Literal, Optional
from typing import Optional
from invokeai.app.services.image_records.image_records_common import ImageCategory
@@ -27,7 +27,7 @@ class BoardImagesServiceABC(ABC):
@abstractmethod
def get_all_board_image_names_for_board(
self,
board_id: str | Literal["none"],
board_id: str,
categories: list[ImageCategory] | None,
is_intermediate: bool | None,
) -> list[str]:

View File

@@ -1,4 +1,4 @@
from typing import Literal, Optional
from typing import Optional
from invokeai.app.services.board_images.board_images_base import BoardImagesServiceABC
from invokeai.app.services.image_records.image_records_common import ImageCategory
@@ -26,7 +26,7 @@ class BoardImagesService(BoardImagesServiceABC):
def get_all_board_image_names_for_board(
self,
board_id: str | Literal["none"],
board_id: str,
categories: list[ImageCategory] | None,
is_intermediate: bool | None,
) -> list[str]:

View File

@@ -63,7 +63,11 @@ class BulkDownloadService(BulkDownloadBase):
return [self._invoker.services.images.get_dto(image_name) for image_name in image_names]
def _board_handler(self, board_id: str) -> list[ImageDTO]:
image_names = self._invoker.services.board_image_records.get_all_board_image_names_for_board(board_id)
image_names = self._invoker.services.board_image_records.get_all_board_image_names_for_board(
board_id,
categories=None,
is_intermediate=None,
)
return self._image_handler(image_names)
def generate_item_id(self, board_id: Optional[str]) -> str:

View File

@@ -265,7 +265,11 @@ class ImageService(ImageServiceABC):
def delete_images_on_board(self, board_id: str):
try:
image_names = self.__invoker.services.board_image_records.get_all_board_image_names_for_board(board_id)
image_names = self.__invoker.services.board_image_records.get_all_board_image_names_for_board(
board_id,
categories=None,
is_intermediate=None,
)
for image_name in image_names:
self.__invoker.services.image_files.delete(image_name)
self.__invoker.services.image_records.delete_many(image_names)
@@ -278,7 +282,7 @@ class ImageService(ImageServiceABC):
self.__invoker.services.logger.error("Failed to delete image files")
raise
except Exception as e:
self.__invoker.services.logger.error("Problem deleting image records and files")
self.__invoker.services.logger.error(f"Problem deleting image records and files: {str(e)}")
raise e
def delete_intermediates(self) -> int:

View File

@@ -1,7 +1,7 @@
import datetime
import json
from itertools import chain, product
from typing import Generator, Iterable, Literal, NamedTuple, Optional, TypeAlias, Union, cast
from typing import Generator, Literal, Optional, TypeAlias, Union, cast
from pydantic import (
AliasChoices,
@@ -406,61 +406,143 @@ class IsFullResult(BaseModel):
# region Util
def populate_graph(graph: Graph, node_field_values: Iterable[NodeFieldValue]) -> Graph:
def create_session_nfv_tuples(batch: Batch, maximum: int) -> Generator[tuple[str, str, str], None, None]:
"""
Populates the given graph with the given batch data items.
"""
graph_clone = graph.model_copy(deep=True)
for item in node_field_values:
node = graph_clone.get_node(item.node_path)
if node is None:
continue
setattr(node, item.field_name, item.value)
graph_clone.update_node(item.node_path, node)
return graph_clone
Given a batch and a maximum number of sessions to create, generate a tuple of session_id, session_json, and
field_values_json for each session.
The batch has a "source" graph and a data property. The data property is a list of lists of BatchDatum objects.
Each BatchDatum has a field identifier (e.g. a node id and field name), and a list of values to substitute into
the field.
def create_session_nfv_tuples(
batch: Batch, maximum: int
) -> Generator[tuple[GraphExecutionState, list[NodeFieldValue], Optional[WorkflowWithoutID]], None, None]:
"""
Create all graph permutations from the given batch data and graph. Yields tuples
of the form (graph, batch_data_items) where batch_data_items is the list of BatchDataItems
that was applied to the graph.
This structure allows us to create a new graph for every possible permutation of BatchDatum objects:
- Each BatchDatum can be "expanded" into a dict of node-field-value tuples - one for each item in the BatchDatum.
- Zip each inner list of expanded BatchDatum objects together. Call this a "batch_data_list".
- Take the cartesian product of all zipped batch_data_lists, resulting in a list of permutations of BatchDatum
- Take the cartesian product of all zipped batch_data_lists, resulting in a list of lists of BatchDatum objects.
Each inner list now represents the substitution values for a single permutation (session).
- For each permutation, substitute the values into the graph
This function is optimized for performance, as it is used to generate a large number of sessions at once.
Args:
batch: The batch to generate sessions from
maximum: The maximum number of sessions to generate
Returns:
A generator that yields tuples of session_id, session_json, and field_values_json for each session. The
generator will stop early if the maximum number of sessions is reached.
"""
# TODO: Should this be a class method on Batch?
data: list[list[tuple[NodeFieldValue]]] = []
data: list[list[tuple[dict]]] = []
batch_data_collection = batch.data if batch.data is not None else []
for batch_datum_list in batch_data_collection:
# each batch_datum_list needs to be convered to NodeFieldValues and then zipped
node_field_values_to_zip: list[list[NodeFieldValue]] = []
for batch_datum_list in batch_data_collection:
node_field_values_to_zip: list[list[dict]] = []
# Expand each BatchDatum into a list of dicts - one for each item in the BatchDatum
for batch_datum in batch_datum_list:
node_field_values = [
NodeFieldValue(node_path=batch_datum.node_path, field_name=batch_datum.field_name, value=item)
# Note: A tuple here is slightly faster than a dict, but we need the object in dict form to be inserted
# in the session_queue table anyways. So, overall creating NFVs as dicts is faster.
{"node_path": batch_datum.node_path, "field_name": batch_datum.field_name, "value": item}
for item in batch_datum.items
]
node_field_values_to_zip.append(node_field_values)
# Zip the dicts together to create a list of dicts for each permutation
data.append(list(zip(*node_field_values_to_zip, strict=True))) # type: ignore [arg-type]
# create generator to yield session,nfv tuples
# We serialize the graph and session once, then mutate the graph dict in place for each session.
#
# This sounds scary, but it's actually fine.
#
# The batch prep logic injects field values into the same fields for each generated session.
#
# For example, after the product operation, we'll end up with a list of node-field-value tuples like this:
# [
# (
# {"node_path": "1", "field_name": "a", "value": 1},
# {"node_path": "2", "field_name": "b", "value": 2},
# {"node_path": "3", "field_name": "c", "value": 3},
# ),
# (
# {"node_path": "1", "field_name": "a", "value": 4},
# {"node_path": "2", "field_name": "b", "value": 5},
# {"node_path": "3", "field_name": "c", "value": 6},
# )
# ]
#
# Note that each tuple has the same length, and each tuple substitutes values in for exactly the same node fields.
# No matter the complexity of the batch, this property holds true.
#
# This means each permutation's substitution can be done in-place on the same graph dict, because it overwrites the
# previous mutation. We only need to serialize the graph once, and then we can mutate it in place for each session.
#
# Previously, we had created new Graph objects for each session, but this was very slow for large (1k+ session
# batches). We then tried dumping the graph to dict and using deep-copy to create a new dict for each session,
# but this was also slow.
#
# Overall, we achieved a 100x speedup by mutating the graph dict in place for each session over creating new Graph
# objects for each session.
#
# We will also mutate the session dict in place, setting a new ID for each session and setting the mutated graph
# dict as the session's graph.
# Dump the batch's graph to a dict once
graph_as_dict = batch.graph.model_dump(warnings=False, exclude_none=True)
# We must provide a Graph object when creating the "dummy" session dict, but we don't actually use it. It will be
# overwritten for each session by the mutated graph_as_dict.
session_dict = GraphExecutionState(graph=Graph()).model_dump(warnings=False, exclude_none=True)
# Now we can create a generator that yields the session_id, session_json, and field_values_json for each session.
count = 0
# Each batch may have multiple runs, so we need to generate the same number of sessions for each run. The total is
# still limited by the maximum number of sessions.
for _ in range(batch.runs):
for d in product(*data):
if count >= maximum:
# We've reached the maximum number of sessions we may generate
return
# Flatten the list of lists of dicts into a single list of dicts
# TODO(psyche): Is the a more efficient way to do this?
flat_node_field_values = list(chain.from_iterable(d))
graph = populate_graph(batch.graph, flat_node_field_values)
yield (GraphExecutionState(graph=graph), flat_node_field_values, batch.workflow)
# Need a fresh ID for each session
session_id = uuid_string()
# Mutate the session dict in place
session_dict["id"] = session_id
# Substitute the values into the graph
for nfv in flat_node_field_values:
graph_as_dict["nodes"][nfv["node_path"]][nfv["field_name"]] = nfv["value"]
# Mutate the session dict in place
session_dict["graph"] = graph_as_dict
# Serialize the session and field values
# Note the use of pydantic's to_jsonable_python to handle serialization of any python object, including sets.
session_json = json.dumps(session_dict, default=to_jsonable_python)
field_values_json = json.dumps(flat_node_field_values, default=to_jsonable_python)
# Yield the session_id, session_json, and field_values_json
yield (session_id, session_json, field_values_json)
# Increment the count so we know when to stop
count += 1
def calc_session_count(batch: Batch) -> int:
"""
Calculates the number of sessions that would be created by the batch, without incurring
the overhead of actually generating them. Adapted from `create_sessions().
Calculates the number of sessions that would be created by the batch, without incurring the overhead of actually
creating them, as is done in `create_session_nfv_tuples()`.
The count is used to communicate to the user how many sessions were _requested_ to be created, as opposed to how
many were _actually_ created (which may be less due to the maximum number of sessions).
"""
# TODO: Should this be a class method on Batch?
if not batch.data:
@@ -476,42 +558,75 @@ def calc_session_count(batch: Batch) -> int:
return len(data_product) * batch.runs
class SessionQueueValueToInsert(NamedTuple):
"""A tuple of values to insert into the session_queue table"""
# Careful with the ordering of this - it must match the insert statement
queue_id: str # queue_id
session: str # session json
session_id: str # session_id
batch_id: str # batch_id
field_values: Optional[str] # field_values json
priority: int # priority
workflow: Optional[str] # workflow json
origin: str | None
destination: str | None
retried_from_item_id: int | None = None
ValueToInsertTuple: TypeAlias = tuple[
str, # queue_id
str, # session (as stringified JSON)
str, # session_id
str, # batch_id
str | None, # field_values (optional, as stringified JSON)
int, # priority
str | None, # workflow (optional, as stringified JSON)
str | None, # origin (optional)
str | None, # destination (optional)
int | None, # retried_from_item_id (optional, this is always None for new items)
]
"""A type alias for the tuple of values to insert into the session queue table."""
ValuesToInsert: TypeAlias = list[SessionQueueValueToInsert]
def prepare_values_to_insert(
queue_id: str, batch: Batch, priority: int, max_new_queue_items: int
) -> list[ValueToInsertTuple]:
"""
Given a batch, prepare the values to insert into the session queue table. The list of tuples can be used with an
`executemany` statement to insert multiple rows at once.
Args:
queue_id: The ID of the queue to insert the items into
batch: The batch to prepare the values for
priority: The priority of the queue items
max_new_queue_items: The maximum number of queue items to insert
def prepare_values_to_insert(queue_id: str, batch: Batch, priority: int, max_new_queue_items: int) -> ValuesToInsert:
values_to_insert: ValuesToInsert = []
for session, field_values, workflow in create_session_nfv_tuples(batch, max_new_queue_items):
# sessions must have unique id
session.id = uuid_string()
Returns:
A list of tuples to insert into the session queue table. Each tuple contains the following values:
- queue_id
- session (as stringified JSON)
- session_id
- batch_id
- field_values (optional, as stringified JSON)
- priority
- workflow (optional, as stringified JSON)
- origin (optional)
- destination (optional)
- retried_from_item_id (optional, this is always None for new items)
"""
# A tuple is a fast and memory-efficient way to store the values to insert. Previously, we used a NamedTuple, but
# measured a ~5% performance improvement by using a normal tuple instead. For very large batches (10k+ items), the
# this difference becomes noticeable.
#
# So, despite the inferior DX with normal tuples, we use one here for performance reasons.
values_to_insert: list[ValueToInsertTuple] = []
# pydantic's to_jsonable_python handles serialization of any python object, including sets, which json.dumps does
# not support by default. Apparently there are sets somewhere in the graph.
# The same workflow is used for all sessions in the batch - serialize it once
workflow_json = json.dumps(batch.workflow, default=to_jsonable_python) if batch.workflow else None
for session_id, session_json, field_values_json in create_session_nfv_tuples(batch, max_new_queue_items):
values_to_insert.append(
SessionQueueValueToInsert(
queue_id=queue_id,
session=session.model_dump_json(warnings=False, exclude_none=True), # as json
session_id=session.id,
batch_id=batch.batch_id,
# must use pydantic_encoder bc field_values is a list of models
field_values=json.dumps(field_values, default=to_jsonable_python) if field_values else None, # as json
priority=priority,
workflow=json.dumps(workflow, default=to_jsonable_python) if workflow else None, # as json
origin=batch.origin,
destination=batch.destination,
(
queue_id,
session_json,
session_id,
batch.batch_id,
field_values_json,
priority,
workflow_json,
batch.origin,
batch.destination,
None,
)
)
return values_to_insert

View File

@@ -27,7 +27,6 @@ from invokeai.app.services.session_queue.session_queue_common import (
SessionQueueItemDTO,
SessionQueueItemNotFoundError,
SessionQueueStatus,
SessionQueueValueToInsert,
calc_session_count,
prepare_values_to_insert,
)
@@ -772,7 +771,7 @@ class SqliteSessionQueue(SessionQueueBase):
try:
self.__lock.acquire()
values_to_insert: list[SessionQueueValueToInsert] = []
values_to_insert: list[tuple] = []
retried_item_ids: list[int] = []
for item_id in item_ids:
@@ -798,17 +797,17 @@ class SqliteSessionQueue(SessionQueueBase):
else queue_item.item_id
)
value_to_insert = SessionQueueValueToInsert(
queue_id=queue_item.queue_id,
batch_id=queue_item.batch_id,
destination=queue_item.destination,
field_values=field_values_json,
origin=queue_item.origin,
priority=queue_item.priority,
workflow=workflow_json,
session=cloned_session_json,
session_id=cloned_session.id,
retried_from_item_id=retried_from_item_id,
value_to_insert = (
queue_item.queue_id,
queue_item.batch_id,
queue_item.destination,
field_values_json,
queue_item.origin,
queue_item.priority,
workflow_json,
cloned_session_json,
cloned_session.id,
retried_from_item_id,
)
values_to_insert.append(value_to_insert)

View File

@@ -9,6 +9,7 @@ from torch import Tensor
from invokeai.app.invocations.constants import IMAGE_MODES
from invokeai.app.invocations.fields import MetadataField, WithBoard, WithMetadata
from invokeai.app.services.board_records.board_records_common import BoardRecordOrderBy
from invokeai.app.services.boards.boards_common import BoardDTO
from invokeai.app.services.config.config_default import InvokeAIAppConfig
from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
@@ -16,6 +17,7 @@ from invokeai.app.services.images.images_common import ImageDTO
from invokeai.app.services.invocation_services import InvocationServices
from invokeai.app.services.model_records.model_records_base import UnknownModelException
from invokeai.app.services.session_processor.session_processor_common import ProgressImage
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
from invokeai.app.util.step_callback import flux_step_callback, stable_diffusion_step_callback
from invokeai.backend.model_manager.config import (
AnyModel,
@@ -102,7 +104,9 @@ class BoardsInterface(InvocationContextInterface):
Returns:
A list of all boards.
"""
return self._services.boards.get_all()
return self._services.boards.get_all(
order_by=BoardRecordOrderBy.CreatedAt, direction=SQLiteDirection.Descending
)
def add_image_to_board(self, board_id: str, image_name: str) -> None:
"""Adds an image to a board.
@@ -122,7 +126,11 @@ class BoardsInterface(InvocationContextInterface):
Returns:
A list of all image names for the board.
"""
return self._services.board_images.get_all_board_image_names_for_board(board_id)
return self._services.board_images.get_all_board_image_names_for_board(
board_id,
categories=None,
is_intermediate=None,
)
class LoggerInterface(InvocationContextInterface):
@@ -283,7 +291,7 @@ class ImagesInterface(InvocationContextInterface):
Returns:
The local path of the image or thumbnail.
"""
return self._services.images.get_path(image_name, thumbnail)
return Path(self._services.images.get_path(image_name, thumbnail))
class TensorsInterface(InvocationContextInterface):

View File

@@ -75,6 +75,8 @@
"idb-keyval": "^6.2.1",
"jsondiffpatch": "^0.6.0",
"konva": "^9.3.15",
"linkify-react": "^4.2.0",
"linkifyjs": "^4.2.0",
"lodash-es": "^4.17.21",
"lru-cache": "^11.0.1",
"mtwist": "^1.0.2",

View File

@@ -74,6 +74,12 @@ dependencies:
konva:
specifier: ^9.3.15
version: 9.3.15
linkify-react:
specifier: ^4.2.0
version: 4.2.0(linkifyjs@4.2.0)(react@18.3.1)
linkifyjs:
specifier: ^4.2.0
version: 4.2.0
lodash-es:
specifier: ^4.17.21
version: 4.17.21
@@ -6714,6 +6720,20 @@ packages:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
dev: false
/linkify-react@4.2.0(linkifyjs@4.2.0)(react@18.3.1):
resolution: {integrity: sha512-dIcDGo+n4FP2FPIHDcqB7cUE+omkcEgQJpc7sNNP4+XZ9FUhFAkKjGnHMzsZM+B4yF93sK166z9K5cKTe/JpzA==}
peerDependencies:
linkifyjs: ^4.0.0
react: '>= 15.0.0'
dependencies:
linkifyjs: 4.2.0
react: 18.3.1
dev: false
/linkifyjs@4.2.0:
resolution: {integrity: sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw==}
dev: false
/liqe@3.8.0:
resolution: {integrity: sha512-cZ1rDx4XzxONBTskSPBp7/KwJ9qbUdF8EPnY4VjKXwHF1Krz9lgnlMTh1G7kd+KtPYvUte1mhuZeQSnk7KiSBg==}
engines: {node: '>=12.0'}

View File

@@ -2301,12 +2301,8 @@
"whatsNew": {
"whatsNewInInvoke": "What's New in Invoke",
"items": [
"Improved VRAM setting defaults",
"On-demand model cache clearing",
"Expanded FLUX LoRA compatibility",
"Canvas Adjust Image filter",
"Cancel all but current queue item",
"Copy from and paste to Canvas"
"Workflow Editor: New drag-and-drop form builder for easier workflow creation.",
"Other improvements: Faster batch queuing, better upscaling, improved color picker, and metadata nodes."
],
"readReleaseNotes": "Read Release Notes",
"watchRecentReleaseVideos": "Watch Recent Release Videos",

View File

@@ -98,7 +98,22 @@
"close": "Fermer",
"clipboard": "Presse-papier",
"loadingModel": "Chargement du modèle",
"generating": "En Génération"
"generating": "En Génération",
"warnings": "Alertes",
"layout": "Disposition",
"row": "Ligne",
"column": "Colonne",
"start": "Commencer",
"board": "Planche",
"count": "Quantité",
"step": "Étape",
"end": "Fin",
"min": "Min",
"max": "Max",
"values": "Valeurs",
"resetToDefaults": "Réinitialiser par défaut",
"seed": "Graine",
"combinatorial": "Combinatoire"
},
"gallery": {
"galleryImageSize": "Taille de l'image",
@@ -165,7 +180,9 @@
"imagesSettings": "Paramètres des images de la galerie",
"assetsTab": "Fichiers que vous avez importés pour vos projets.",
"imagesTab": "Images que vous avez créées et enregistrées dans Invoke.",
"boardsSettings": "Paramètres des planches"
"boardsSettings": "Paramètres des planches",
"assets": "Ressources",
"images": "Images"
},
"modelManager": {
"modelManager": "Gestionnaire de modèle",
@@ -289,7 +306,7 @@
"usingDefaultSettings": "Utilisation des paramètres par défaut du modèle",
"defaultSettingsOutOfSync": "Certain paramètres ne correspondent pas aux valeurs par défaut du modèle :",
"restoreDefaultSettings": "Cliquez pour utiliser les paramètres par défaut du modèle.",
"hfForbiddenErrorMessage": "Nous vous recommandons de visiter la page du modèle sur HuggingFace.com. Le propriétaire peut exiger l'acceptation des conditions pour pouvoir télécharger.",
"hfForbiddenErrorMessage": "Nous vous recommandons de visiter la page du modèle. Le propriétaire peut exiger l'acceptation des conditions pour pouvoir télécharger.",
"hfTokenRequired": "Vous essayez de télécharger un modèle qui nécessite un token HuggingFace valide.",
"clipLEmbed": "CLIP-L Embed",
"hfTokenSaved": "Token HF enregistré",
@@ -303,7 +320,10 @@
"hfForbidden": "Vous n'avez pas accès à ce modèle HF.",
"hfTokenInvalidErrorMessage2": "Mettre à jour dans le ",
"controlLora": "Controle LoRA",
"urlUnauthorizedErrorMessage2": "Découvrir comment ici."
"urlUnauthorizedErrorMessage2": "Découvrir comment ici.",
"urlUnauthorizedErrorMessage": "Vous devrez peut-être configurer un jeton API pour accéder à ce modèle.",
"urlForbidden": "Vous n'avez pas accès à ce modèle",
"urlForbiddenErrorMessage": "Vous devrez peut-être demander l'autorisation du site qui distribue le modèle."
},
"parameters": {
"images": "Images",
@@ -345,19 +365,31 @@
"fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), la hauteur de la bounding box est {{height}}",
"fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), la hauteur de la bounding box est {{height}}",
"noFLUXVAEModelSelected": "Aucun modèle VAE sélectionné pour la génération FLUX",
"canvasIsTransforming": "La Toile se transforme",
"canvasIsRasterizing": "La Toile se rastérise",
"canvasIsTransforming": "La Toile est occupée (en transformation)",
"canvasIsRasterizing": "La Toile est occupée (en rastérisation)",
"noCLIPEmbedModelSelected": "Aucun modèle CLIP Embed sélectionné pour la génération FLUX",
"canvasIsFiltering": "La Toile est en train de filtrer",
"canvasIsFiltering": "La Toile est occupée (en filtration)",
"fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), la largeur de la bounding box est {{width}}",
"noT5EncoderModelSelected": "Aucun modèle T5 Encoder sélectionné pour la génération FLUX",
"fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), la largeur de la bounding box mise à l'échelle est {{width}}",
"canvasIsCompositing": "La toile est en train de composer",
"collectionTooFewItems": "{{nodeLabel}} -> {{fieldLabel}} : trop peu d'éléments, minimum {{minItems}}",
"collectionTooManyItems": "{{nodeLabel}} -> {{fieldLabel}} : trop d'éléments, maximum {{maxItems}}",
"canvasIsCompositing": "La Toile est occupée (en composition)",
"collectionTooFewItems": "trop peu d'éléments, minimum {{minItems}}",
"collectionTooManyItems": "trop d'éléments, maximum {{maxItems}}",
"canvasIsSelectingObject": "La toile est occupée (sélection d'objet)",
"emptyBatches": "lots vides",
"batchNodeNotConnected": "Noeud de lots non connecté : {{label}}"
"batchNodeNotConnected": "Noeud de lots non connecté : {{label}}",
"fluxModelMultipleControlLoRAs": "Vous ne pouvez utiliser qu'un seul Control LoRA à la fois",
"collectionNumberLTMin": "{{value}} < {{minimum}} (incl. min)",
"collectionNumberGTMax": "{{value}} > {{maximum}} (incl. max)",
"collectionNumberGTExclusiveMax": "{{value}} >= {{exclusiveMaximum}} (max exc)",
"batchNodeEmptyCollection": "Certains nœuds de lot ont des collections vides",
"batchNodeCollectionSizeMismatch": "Non-concordance de taille de collection sur le lot {{batchGroupId}}",
"collectionStringTooLong": "trop long, max {{maxLength}}",
"collectionNumberNotMultipleOf": "{{value}} n'est pas un multiple de {{multipleOf}}",
"collectionEmpty": "collection vide",
"collectionStringTooShort": "trop court, min {{minLength}}",
"collectionNumberLTExclusiveMin": "{{value}} <= {{exclusiveMinimum}} (min exc)",
"batchNodeCollectionSizeMismatchNoGroupId": "Taille de collection de groupe par lot non conforme"
},
"negativePromptPlaceholder": "Prompt Négatif",
"positivePromptPlaceholder": "Prompt Positif",
@@ -501,7 +533,13 @@
"uploadFailedInvalidUploadDesc_withCount_one": "Doit être au maximum une image PNG ou JPEG.",
"uploadFailedInvalidUploadDesc_withCount_many": "Doit être au maximum {{count}} images PNG ou JPEG.",
"uploadFailedInvalidUploadDesc_withCount_other": "Doit être au maximum {{count}} images PNG ou JPEG.",
"addedToUncategorized": "Ajouté aux ressources de la planche $t(boards.uncategorized)"
"addedToUncategorized": "Ajouté aux ressources de la planche $t(boards.uncategorized)",
"pasteSuccess": "Collé à {{destination}}",
"pasteFailed": "Échec du collage",
"outOfMemoryErrorDescLocal": "Suivez notre <LinkComponent>guide Low VRAM</LinkComponent> pour réduire les OOMs.",
"unableToCopy": "Incapable de Copier",
"unableToCopyDesc": "Votre navigateur ne prend pas en charge l'accès au presse-papiers. Les utilisateurs de Firefox peuvent peut-être résoudre ce problème en suivant ",
"unableToCopyDesc_theseSteps": "ces étapes"
},
"accessibility": {
"uploadImage": "Importer une image",
@@ -659,7 +697,14 @@
"iterations_many": "Itérations",
"iterations_other": "Itérations",
"back": "fin",
"batchSize": "Taille de lot"
"batchSize": "Taille de lot",
"retryFailed": "Problème de nouvelle tentative de l'élément",
"retrySucceeded": "Élément Retenté",
"retryItem": "Réessayer l'élement",
"cancelAllExceptCurrentQueueItemAlertDialog": "Annuler tous les éléments de la file d'attente, sauf celui en cours, arrêtera les éléments en attente mais permettra à celui en cours de se terminer.",
"cancelAllExceptCurrentQueueItemAlertDialog2": "Êtes-vous sûr de vouloir annuler tous les éléments en attente dans la file d'attente?",
"cancelAllExceptCurrentTooltip": "Annuler tout sauf l'élément actuel",
"confirm": "Confirmer"
},
"prompt": {
"noMatchingTriggers": "Pas de déclancheurs correspondants",
@@ -1031,7 +1076,9 @@
"controlNetWeight": {
"heading": "Poids",
"paragraphs": [
"Poids du Control Adapter. Un poids plus élevé aura un impact plus important sur l'image finale."
"Poids du Control Adapter. Un poids plus élevé aura un impact plus important sur l'image finale.",
"• Poids plus élevé (.75-2) : Crée un impact plus significatif sur le résultat final.",
"• Poids inférieur (0-.75) : Crée un impact plus faible sur le résultat final."
]
},
"compositingMaskAdjustments": {
@@ -1076,8 +1123,9 @@
"controlNetBeginEnd": {
"heading": "Pourcentage de début / de fin d'étape",
"paragraphs": [
"La partie du processus de débruitage à laquelle le Control Adapter sera appliqué.",
"En général, les Control Adapter appliqués au début du processus guident la composition, tandis que les Control Adapter appliqués à la fin guident les détails."
"Ce paramètre détérmine quelle portion du processus de débruitage (génération) utilisera cette couche comme guide.",
"En général, les Control Adapter appliqués au début du processus guident la composition, tandis que les Control Adapter appliqués à la fin guident les détails.",
"• Étape de fin (%): Spécifie quand arrêter d'appliquer le guide de cette couche et revenir aux guides généraux du modèle et aux autres paramètres."
]
},
"controlNetControlMode": {
@@ -1442,7 +1490,8 @@
"showDynamicPrompts": "Afficher les Prompts dynamiques",
"dynamicPrompts": "Prompts Dynamiques",
"promptsPreview": "Prévisualisation des Prompts",
"loading": "Génération des Pompts Dynamiques..."
"loading": "Génération des Pompts Dynamiques...",
"promptsToGenerate": "Prompts à générer"
},
"metadata": {
"positivePrompt": "Prompt Positif",
@@ -1653,7 +1702,22 @@
"internalDesc": "Cette invocation est utilisée internalement par Invoke. En fonction des mises à jours il est possible que des changements y soit effectués ou qu'elle soit supprimé sans prévention.",
"splitOn": "Diviser sur",
"generatorNoValues": "vide",
"addItem": "Ajouter un élément"
"addItem": "Ajouter un élément",
"specialDesc": "Cette invocation nécessite un traitement spécial dans l'application. Par exemple, les nœuds Batch sont utilisés pour mettre en file d'attente plusieurs graphes à partir d'un seul workflow.",
"unableToUpdateNode": "La mise à jour du nœud a échoué : nœud {{node}} de type {{type}} (peut nécessiter la suppression et la recréation).",
"deletedMissingNodeFieldFormElement": "Champ de formulaire manquant supprimé : nœud {{nodeId}} champ {{fieldName}}",
"nodeName": "Nom du nœud",
"description": "Description",
"loadWorkflowDesc": "Charger le workflow?",
"missingSourceOrTargetNode": "Nœud source ou cible manquant",
"generatorImagesCategory": "Catégorie",
"generatorImagesFromBoard": "Images de la Planche",
"missingSourceOrTargetHandle": "Manque de gestionnaire source ou cible",
"loadingTemplates": "Chargement de {{name}}",
"loadWorkflowDesc2": "Votre workflow actuel contient des modifications non enregistrées.",
"generatorImages_one": "{{count}} image",
"generatorImages_many": "{{count}} images",
"generatorImages_other": "{{count}} images"
},
"models": {
"noMatchingModels": "Aucun modèle correspondant",
@@ -1712,13 +1776,41 @@
"deleteWorkflow2": "Êtes-vous sûr de vouloir supprimer ce Workflow? Cette action ne peut pas être annulé.",
"download": "Télécharger",
"copyShareLinkForWorkflow": "Copier le lien de partage pour le Workflow",
"delete": "Supprimer"
"delete": "Supprimer",
"builder": {
"component": "Composant",
"numberInput": "Entrée de nombre",
"slider": "Curseur",
"both": "Les deux",
"singleLine": "Ligne unique",
"multiLine": "Multi Ligne",
"headingPlaceholder": "En-tête vide",
"emptyRootPlaceholderEditMode": "Faites glisser un élément de formulaire ou un champ de nœud ici pour commencer.",
"emptyRootPlaceholderViewMode": "Cliquez sur Modifier pour commencer à créer un formulaire pour ce workflow.",
"containerPlaceholder": "Conteneur Vide",
"row": "Ligne",
"column": "Colonne",
"layout": "Mise en page",
"nodeField": "Champ de nœud",
"zoomToNode": "Zoomer sur le nœud",
"nodeFieldTooltip": "Pour ajouter un champ de nœud, cliquez sur le petit bouton plus sur le champ dans l'Éditeur de Workflow, ou faites glisser le champ par son nom dans le formulaire.",
"addToForm": "Ajouter au formulaire",
"label": "Étiquette",
"textPlaceholder": "Texte vide",
"builder": "Constructeur de Formulaire",
"resetAllNodeFields": "Réinitialiser tous les champs de nœud",
"deleteAllElements": "Supprimer tous les éléments de formulaire",
"workflowBuilderAlphaWarning": "Le constructeur de workflow est actuellement en version alpha. Il peut y avoir des changements majeurs avant la version stable.",
"showDescription": "Afficher la description"
},
"openLibrary": "Ouvrir la Bibliothèque"
},
"whatsNew": {
"whatsNewInInvoke": "Quoi de neuf dans Invoke",
"watchRecentReleaseVideos": "Regarder les vidéos des dernières versions",
"items": [
"<StrongComponent>FLUX Guidage Régional (bêta)</StrongComponent> : Notre version bêta de FLUX Guidage Régional est en ligne pour le contrôle des prompt régionaux."
"<StrongComponent>FLUX Guidage Régional (bêta)</StrongComponent> : Notre version bêta de FLUX Guidage Régional est en ligne pour le contrôle des prompt régionaux.",
"Autres améliorations : mise en file d'attente par lots plus rapide, meilleur redimensionnement, sélecteur de couleurs amélioré et nœuds de métadonnées."
],
"readReleaseNotes": "Notes de version",
"watchUiUpdatesOverview": "Aperçu des mises à jour de l'interface utilisateur"
@@ -1832,7 +1924,49 @@
"cancel": "Annuler",
"advanced": "Avancé",
"processingLayerWith": "Calque de traitement avec le filtre {{type}}.",
"forMoreControl": "Pour plus de contrôle, cliquez sur Avancé ci-dessous."
"forMoreControl": "Pour plus de contrôle, cliquez sur Avancé ci-dessous.",
"adjust_image": {
"b": "B (LAB)",
"blue": "Bleu (RGBA)",
"alpha": "Alpha (RGBA)",
"magenta": "Magenta (CMJN)",
"yellow": "Jaune (CMJN)",
"cb": "Cb (YCbCr)",
"cr": "Cr (YCbCr)",
"cyan": "Cyan (CMJN)",
"label": "Ajuster l'image",
"description": "Ajuste le canal sélectionné d'une image.",
"channel": "Canal",
"value_setting": "Valeur",
"scale_values": "Valeurs d'échelle",
"red": "Rouge (RGBA)",
"green": "Vert (RGBA)",
"black": "Noir (CMJN)",
"hue": "Teinte (HSV)",
"saturation": "Saturation (HSV)",
"value": "Valeur (HSV)",
"luminosity": "Luminosité (LAB)",
"a": "A (LAB)",
"y": "Y (YCbCr)"
},
"img_blur": {
"label": "Flou de l'image",
"blur_type": "Type de flou",
"box_type": "Boîte",
"description": "Floute la couche sélectionnée.",
"blur_radius": "Rayon",
"gaussian_type": "Gaussien"
},
"img_noise": {
"label": "Image de bruit",
"description": "Ajoute du bruit à la couche sélectionnée.",
"gaussian_type": "Gaussien",
"size": "Taille du bruit",
"noise_amount": "Quantité",
"noise_type": "Type de bruit",
"salt_and_pepper_type": "Sel et Poivre",
"noise_color": "Bruit coloré"
}
},
"canvasContextMenu": {
"saveToGalleryGroup": "Enregistrer dans la galerie",
@@ -1846,7 +1980,10 @@
"newGlobalReferenceImage": "Nouvelle image de référence globale",
"newControlLayer": "Nouveau couche de contrôle",
"newInpaintMask": "Nouveau Masque Inpaint",
"newRegionalGuidance": "Nouveau Guide Régional"
"newRegionalGuidance": "Nouveau Guide Régional",
"copyToClipboard": "Copier dans le presse-papiers",
"copyBboxToClipboard": "Copier Bbox dans le presse-papiers",
"copyCanvasToClipboard": "Copier la Toile dans le presse-papiers"
},
"bookmark": "Marque-page pour Changement Rapide",
"saveLayerToAssets": "Enregistrer la couche dans les ressources",
@@ -2012,7 +2149,10 @@
"ipAdapterMethod": "Méthode d'IP Adapter",
"full": "Complet",
"style": "Style uniquement",
"composition": "Composition uniquement"
"composition": "Composition uniquement",
"fullDesc": "Applique le style visuel (couleurs, textures) et la composition (mise en page, structure).",
"styleDesc": "Applique un style visuel (couleurs, textures) sans tenir compte de sa mise en page.",
"compositionDesc": "Réplique la mise en page et la structure tout en ignorant le style de la référence."
},
"fitBboxToLayers": "Ajuster la bounding box aux calques",
"regionIsEmpty": "La zone sélectionnée est vide",
@@ -2095,7 +2235,40 @@
"asRasterLayerResize": "En tant que $t(controlLayers.rasterLayer) (Redimensionner)",
"asControlLayer": "En tant que $t(controlLayers.controlLayer)",
"asControlLayerResize": "En $t(controlLayers.controlLayer) (Redimensionner)",
"newSession": "Nouvelle session"
"newSession": "Nouvelle session",
"warnings": {
"controlAdapterIncompatibleBaseModel": "modèle de base de la couche de contrôle incompatible",
"controlAdapterNoControl": "aucun contrôle sélectionné/dessiné",
"rgNoPromptsOrIPAdapters": "pas de textes d'instructions ni d'images de référence",
"rgAutoNegativeNotSupported": "Auto-négatif non pris en charge pour le modèle de base sélectionné",
"rgNoRegion": "aucune région dessinée",
"ipAdapterNoModelSelected": "aucun modèle d'image de référence sélectionné",
"rgReferenceImagesNotSupported": "Les images de référence régionales ne sont pas prises en charge pour le modèle de base sélectionné",
"problemsFound": "Problèmes trouvés",
"unsupportedModel": "couche non prise en charge pour le modèle de base sélectionné",
"rgNegativePromptNotSupported": "Prompt négatif non pris en charge pour le modèle de base sélectionné",
"ipAdapterIncompatibleBaseModel": "modèle de base d'image de référence incompatible",
"controlAdapterNoModelSelected": "aucun modèle de couche de contrôle sélectionné",
"ipAdapterNoImageSelected": "Aucune image de référence sélectionnée."
},
"pasteTo": "Coller vers",
"pasteToAssets": "Ressources",
"pasteToAssetsDesc": "Coller dans les ressources",
"pasteToBbox": "Bbox",
"regionCopiedToClipboard": "{{region}} Copié dans le presse-papiers",
"copyRegionError": "Erreur de copie {{region}}",
"pasteToCanvas": "Toile",
"errors": {
"unableToFindImage": "Impossible de trouver l'image",
"unableToLoadImage": "Impossible de charger l'image"
},
"referenceImageRegional": "Image de référence (régionale)",
"pasteToBboxDesc": "Nouvelle couche (dans Bbox)",
"pasteToCanvasDesc": "Nouvelle couche (dans la Toile)",
"useImage": "Utiliser l'image",
"pastedTo": "Collé à {{destination}}",
"referenceImageEmptyState": "<UploadButton>Séléctionner une image</UploadButton> ou faites glisser une image depuis la <GalleryButton>galerie</GalleryButton> sur cette couche pour commencer.",
"referenceImageGlobal": "Image de référence (Globale)"
},
"upscaling": {
"exceedsMaxSizeDetails": "La limite maximale d'agrandissement est de {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixels. Veuillez essayer une image plus petite ou réduire votre sélection d'échelle.",
@@ -2175,7 +2348,8 @@
"queue": "File d'attente",
"events": "Événements",
"metadata": "Métadonnées",
"gallery": "Galerie"
"gallery": "Galerie",
"dnd": "Glisser et déposer"
},
"logLevel": {
"trace": "Trace",
@@ -2192,7 +2366,8 @@
"toGetStarted": "Pour commencer, saisissez un prompt dans la boîte et cliquez sur <StrongComponent>Invoke</StrongComponent> pour générer votre première image. Sélectionnez un template de prompt pour améliorer les résultats. Vous pouvez choisir de sauvegarder vos images directement dans la <StrongComponent>Galerie</StrongComponent> ou de les modifier sur la <StrongComponent>Toile</StrongComponent>.",
"gettingStartedSeries": "Vous souhaitez plus de conseils? Consultez notre <LinkComponent>Série de démarrage</LinkComponent> pour des astuces sur l'exploitation du plein potentiel de l'Invoke Studio.",
"noModelsInstalled": "Il semble qu'aucun modèle ne soit installé",
"toGetStartedLocal": "Pour commencer, assurez-vous de télécharger ou d'importer des modèles nécessaires pour exécuter Invoke. Ensuite, saisissez le prompt dans la boîte et cliquez sur <StrongComponent>Invoke</StrongComponent> pour générer votre première image. Sélectionnez un template de prompt pour améliorer les résultats. Vous pouvez choisir de sauvegarder vos images directement sur <StrongComponent>Galerie</StrongComponent> ou les modifier sur la <StrongComponent>Toile</StrongComponent>."
"toGetStartedLocal": "Pour commencer, assurez-vous de télécharger ou d'importer des modèles nécessaires pour exécuter Invoke. Ensuite, saisissez le prompt dans la boîte et cliquez sur <StrongComponent>Invoke</StrongComponent> pour générer votre première image. Sélectionnez un template de prompt pour améliorer les résultats. Vous pouvez choisir de sauvegarder vos images directement sur <StrongComponent>Galerie</StrongComponent> ou les modifier sur la <StrongComponent>Toile</StrongComponent>.",
"lowVRAMMode": "Pour de meilleures performances, suivez notre <LinkComponent>guide Low VRAM</LinkComponent>."
},
"upsell": {
"shareAccess": "Partager l'accès",
@@ -2240,7 +2415,8 @@
"description": "Introduction à l'ajout d'images de référence et IP Adapters globaux."
},
"howDoIUseInpaintMasks": {
"title": "Comment utiliser les masques d'inpainting?"
"title": "Comment utiliser les masques d'inpainting?",
"description": "Comment appliquer des masques de retourche pour la correction et la variation d'image."
},
"creatingYourFirstImage": {
"title": "Créer votre première image",
@@ -2260,5 +2436,10 @@
"studioSessionsDesc2": "Rejoignez notre <DiscordLink /> pour participer aux sessions en direct et poser vos questions. Les sessions sont ajoutée dans la playlist la semaine suivante.",
"supportVideos": "Vidéos d'assistance",
"controlCanvas": "Contrôler la toile"
},
"modelCache": {
"clear": "Effacer le cache du modèle",
"clearSucceeded": "Cache du modèle effacée",
"clearFailed": "Problème de nettoyage du cache du modèle"
}
}

View File

@@ -2277,11 +2277,7 @@
"watchUiUpdatesOverview": "Guarda le novità dell'interfaccia",
"items": [
"Impostazioni predefinite VRAM migliorate",
"Cancellazione della cache del modello su richiesta",
"Compatibilità estesa FLUX LoRA",
"Filtro Regola Immagine su Tela",
"Annulla tutto tranne l'elemento della coda corrente",
"Copia da e incolla sulla Tela"
"Cancellazione della cache del modello su richiesta"
]
},
"system": {

View File

@@ -32,29 +32,29 @@
"learnMore": "もっと学ぶ",
"random": "ランダム",
"batch": "バッチマネージャー",
"advanced": "高度な設定",
"advanced": "高度",
"created": "作成済",
"green": "緑",
"blue": "青",
"alpha": "アルファ",
"outpaint": "アウトペイント",
"outpaint": "outpaint",
"unknown": "不明",
"updated": "更新済",
"add": "追加",
"ai": "AI",
"ai": "ai",
"copyError": "$t(gallery.copy) エラー",
"data": "データ",
"template": "テンプレート",
"red": "赤",
"or": "または",
"checkpoint": "チェックポイント",
"checkpoint": "Checkpoint",
"direction": "方向",
"simple": "シンプル",
"save": "保存",
"saveAs": "名前をつけて保存",
"somethingWentWrong": "何かの問題が発生しました",
"details": "詳細",
"inpaint": "インペイント",
"inpaint": "inpaint",
"delete": "削除",
"nextPage": "次のページ",
"copy": "コピー",
@@ -70,12 +70,12 @@
"unknownError": "未知のエラー",
"orderBy": "並び順:",
"enabled": "有効",
"notInstalled": "未インストール",
"notInstalled": "未 $t(common.installed)",
"positivePrompt": "ポジティブプロンプト",
"negativePrompt": "ネガティブプロンプト",
"selected": "選択済み",
"aboutDesc": "Invokeを業務で利用する場合はマークしてください:",
"beta": "ベータ",
"beta": "Beta",
"disabled": "無効",
"editor": "エディタ",
"safetensors": "Safetensors",
@@ -93,7 +93,27 @@
"reset": "リセット",
"none": "なし",
"new": "新規",
"close": "閉じる"
"close": "閉じる",
"warnings": "警告",
"dontShowMeThese": "次回から表示しない",
"goTo": "移動",
"generating": "生成中",
"loadingModel": "モデルをロード中",
"layout": "レイアウト",
"step": "ステップ",
"start": "開始",
"count": "回数",
"end": "終了",
"min": "最小",
"max": "最大",
"values": "値",
"resetToDefaults": "デフォルトに戻す",
"row": "行",
"column": "列",
"board": "ボード",
"seed": "シード",
"combinatorial": "組み合わせ",
"aboutHeading": "想像力をこの手に"
},
"gallery": {
"galleryImageSize": "画像のサイズ",
@@ -109,7 +129,7 @@
"currentlyInUse": "この画像は現在下記の機能を使用しています:",
"drop": "ドロップ",
"dropOrUpload": "$t(gallery.drop) またはアップロード",
"deleteImage_other": "画像を削除",
"deleteImage_other": "画像 {{count}} 枚を削除",
"deleteImagePermanent": "削除された画像は復元できません。",
"download": "ダウンロード",
"unableToLoad": "ギャラリーをロードできません",
@@ -155,7 +175,12 @@
"displayBoardSearch": "ボード検索",
"displaySearch": "画像を検索",
"boardsSettings": "ボード設定",
"imagesSettings": "ギャラリー画像設定"
"imagesSettings": "ギャラリー画像設定",
"selectAllOnPage": "ページ上のすべてを選択",
"images": "画像",
"assetsTab": "プロジェクトで使用するためにアップロードされたファイル。",
"imagesTab": "Invoke内で作成および保存された画像。",
"assets": "アセット"
},
"hotkeys": {
"searchHotkeys": "ホットキーを検索",
@@ -180,44 +205,121 @@
},
"canvas": {
"redo": {
"title": "やり直し"
"title": "やり直し",
"desc": "最後のキャンバス操作をやり直します。"
},
"transformSelected": {
"title": "変形"
"title": "変形",
"desc": "選択したレイヤーを変形します。"
},
"undo": {
"title": "取り消し"
"title": "取り消し",
"desc": "最後のキャンバス操作を取り消します。"
},
"selectEraserTool": {
"title": "消しゴムツール"
"title": "消しゴムツール",
"desc": "消しゴムツールを選択します。"
},
"cancelTransform": {
"title": "変形をキャンセル"
"title": "変形をキャンセル",
"desc": "保留中の変形をキャンセルします。"
},
"resetSelected": {
"title": "レイヤーをリセット"
"title": "レイヤーをリセット",
"desc": "選択したレイヤーをリセットします。この操作はInpaint MaskおよびRegional Guidanceにのみ適用されます。"
},
"applyTransform": {
"title": "変形を適用"
"title": "変形を適用",
"desc": "保留中の変形を選択したレイヤーに適用します。"
},
"selectColorPickerTool": {
"title": "スポイトツール"
"title": "スポイトツール",
"desc": "スポイトツールを選択します。"
},
"fitBboxToCanvas": {
"title": "バウンディングボックスをキャンバスにフィット"
"title": "バウンディングボックスをキャンバスにフィット",
"desc": "バウンディングボックスがキャンバスに収まるように表示を拡大、位置調整します。"
},
"selectBrushTool": {
"title": "ブラシツール"
"title": "ブラシツール",
"desc": "ブラシツールを選択します。"
},
"selectMoveTool": {
"title": "移動ツール"
"title": "移動ツール",
"desc": "移動ツールを選択します。"
},
"selectBboxTool": {
"title": "バウンディングボックスツール"
"title": "バウンディングボックスツール",
"desc": "バウンディングボックスツールを選択します。"
},
"title": "キャンバス",
"fitLayersToCanvas": {
"title": "レイヤーをキャンバスにフィット"
"title": "レイヤーをキャンバスにフィット",
"desc": "すべての表示レイヤーがキャンバスに収まるように表示を拡大、位置調整します。"
},
"setZoomTo400Percent": {
"desc": "キャンバスのズームを400%に設定します。",
"title": "400%にズーム"
},
"setZoomTo800Percent": {
"title": "800%にズーム",
"desc": "キャンバスのズームを800%に設定します。"
},
"quickSwitch": {
"title": "レイヤーのクイックスイッチ",
"desc": "最後に選択した2つのレイヤー間を切り替えます。レイヤーがブックマークされている場合、常にそのレイヤーと最後に選択したブックマークされていないレイヤーの間を切り替えます。"
},
"nextEntity": {
"title": "次のレイヤー",
"desc": "リスト内の次のレイヤーを選択します。"
},
"filterSelected": {
"title": "フィルター",
"desc": "選択したレイヤーをフィルターします。RasterおよびControlレイヤーにのみ適用されます。"
},
"prevEntity": {
"desc": "リスト内の前のレイヤーを選択します。",
"title": "前のレイヤー"
},
"setFillToWhite": {
"title": "ツール色を白に設定",
"desc": "現在のツールの色を白色に設定します。"
},
"selectViewTool": {
"title": "表示ツール",
"desc": "表示ツールを選択します。"
},
"setZoomTo100Percent": {
"title": "100%にズーム",
"desc": "キャンバスのズームを100%に設定します。"
},
"deleteSelected": {
"desc": "選択したレイヤーを削除します。",
"title": "レイヤーを削除"
},
"cancelFilter": {
"desc": "保留中のフィルターをキャンセルします。",
"title": "フィルターをキャンセル"
},
"applyFilter": {
"title": "フィルターを適用",
"desc": "保留中のフィルターを選択したレイヤーに適用します。"
},
"setZoomTo200Percent": {
"title": "200%にズーム",
"desc": "キャンバスのズームを200%に設定します。"
},
"decrementToolWidth": {
"title": "ツール幅を縮小する",
"desc": "選択中のブラシまたは消しゴムツールの幅を減少させます。"
},
"incrementToolWidth": {
"desc": "選択中のブラシまたは消しゴムツールの幅を増加させます。",
"title": "ツール幅を増加する"
},
"selectRectTool": {
"title": "矩形ツール",
"desc": "矩形ツールを選択します。"
}
},
"workflows": {
@@ -226,6 +328,13 @@
},
"redo": {
"title": "やり直し"
},
"title": "ワークフロー",
"pasteSelection": {
"title": "ペースト"
},
"copySelection": {
"title": "コピー"
}
},
"app": {
@@ -235,16 +344,62 @@
},
"title": "アプリケーション",
"invoke": {
"title": "Invoke"
"title": "生成",
"desc": "生成をキューに追加し、キューの末尾に加えます。"
},
"cancelQueueItem": {
"title": "キャンセル"
"title": "キャンセル",
"desc": "現在処理中のキュー項目をキャンセルします。"
},
"clearQueue": {
"title": "キューをクリア"
"title": "キューをクリア",
"desc": "すべてのキュー項目をキャンセルして消去します。"
},
"selectCanvasTab": {
"desc": "キャンバスタブを選択します。",
"title": "キャンバスタブを選択"
},
"selectUpscalingTab": {
"desc": "アップスケーリングタブを選択します。",
"title": "アップスケーリングタブを選択"
},
"toggleRightPanel": {
"desc": "右パネルを表示または非表示。",
"title": "右パネルをトグル"
},
"selectModelsTab": {
"title": "モデルタブを選択",
"desc": "モデルタブを選択します。"
},
"invokeFront": {
"desc": "生成をキューに追加し、キューの先頭に加えます。",
"title": "生成(先頭)"
},
"resetPanelLayout": {
"title": "パネルレイアウトをリセット",
"desc": "左パネルと右パネルをデフォルトのサイズとレイアウトにリセットします。"
},
"togglePanels": {
"desc": "左パネルと右パネルを合わせて表示または非表示。",
"title": "パネルをトグル"
},
"selectWorkflowsTab": {
"desc": "ワークフロータブを選択します。",
"title": "ワークフロータブを選択"
},
"selectQueueTab": {
"title": "キュータブを選択",
"desc": "キュータブを選択します。"
},
"focusPrompt": {
"title": "プロンプトにフォーカス",
"desc": "カーソルをポジティブプロンプト欄に移動します。"
}
},
"hotkeys": "ホットキー"
"hotkeys": "ホットキー",
"gallery": {
"title": "ギャラリー"
}
},
"modelManager": {
"modelManager": "モデルマネージャ",
@@ -255,13 +410,13 @@
"name": "名前",
"description": "概要",
"config": "コンフィグ",
"repo_id": "Repo ID",
"repo_id": "リポジトリID",
"width": "幅",
"height": "高さ",
"addModel": "モデルを追加",
"availableModels": "モデルを有効化",
"search": "検索",
"load": "Load",
"load": "ロード",
"active": "active",
"selected": "選択済",
"delete": "削除",
@@ -281,7 +436,7 @@
"modelConverted": "モデル変換が完了しました",
"predictionType": "予測タイプSD 2.x モデルおよび一部のSD 1.x モデル用)",
"selectModel": "モデルを選択",
"advanced": "高度な設定",
"advanced": "高度",
"modelDeleted": "モデルが削除されました",
"convertToDiffusersHelpText2": "このプロセスでは、モデルマネージャーのエントリーを同じモデルのディフューザーバージョンに置き換えます。",
"modelUpdateFailed": "モデル更新が失敗しました",
@@ -294,7 +449,20 @@
"convertToDiffusersHelpText4": "これは一回限りのプロセスです。コンピュータの仕様によっては、約30秒から60秒かかる可能性があります。",
"cancel": "キャンセル",
"uploadImage": "画像をアップロード",
"addModels": "モデルを追加"
"addModels": "モデルを追加",
"modelName": "モデル名",
"source": "ソース",
"path": "パス",
"modelSettings": "モデル設定",
"vae": "VAE",
"huggingFace": "HuggingFace",
"huggingFaceRepoID": "HuggingFace リポジトリID",
"metadata": "メタデータ",
"loraModels": "LoRA",
"edit": "編集",
"install": "インストール",
"huggingFacePlaceholder": "owner/model-name",
"variant": "Variant"
},
"parameters": {
"images": "画像",
@@ -305,7 +473,7 @@
"shuffle": "シャッフル",
"strength": "強度",
"upscaling": "アップスケーリング",
"scale": "Scale",
"scale": "スケール",
"scaleBeforeProcessing": "処理前のスケール",
"scaledWidth": "幅のスケール",
"scaledHeight": "高さのスケール",
@@ -314,7 +482,7 @@
"useSeed": "シード値を使用",
"useAll": "すべてを使用",
"info": "情報",
"showOptionsPanel": "オプションパネルを表示",
"showOptionsPanel": "サイドパネルを表示 (O or T)",
"iterations": "生成回数",
"general": "基本設定",
"setToOptimalSize": "サイズをモデルに最適化",
@@ -328,16 +496,29 @@
"useSize": "サイズを使用",
"postProcessing": "ポストプロセス (Shift + U)",
"denoisingStrength": "ノイズ除去強度",
"recallMetadata": "メタデータを再使用"
"recallMetadata": "メタデータを再使用",
"copyImage": "画像をコピー",
"positivePromptPlaceholder": "ポジティブプロンプト",
"negativePromptPlaceholder": "ネガティブプロンプト",
"type": "タイプ",
"cancel": {
"cancel": "キャンセル"
},
"cfgScale": "CFGスケール",
"tileSize": "タイルサイズ",
"coherenceMode": "モード"
},
"settings": {
"models": "モデル",
"displayInProgress": "生成中の画像を表示する",
"displayInProgress": "生成中の画像を表示",
"confirmOnDelete": "削除時に確認",
"resetWebUI": "WebUIをリセット",
"resetWebUIDesc1": "WebUIのリセットは、画像と保存された設定のキャッシュをリセットするだけです。画像を削除するわけではありません。",
"resetWebUIDesc2": "もしギャラリーに画像が表示されないなど、何か問題が発生した場合はGitHubにissueを提出する前にリセットを試してください。",
"resetComplete": "WebUIはリセットされました。F5を押して再読み込みしてください。"
"resetComplete": "WebUIはリセットされました。",
"ui": "ユーザーインターフェイス",
"beta": "ベータ",
"developer": "開発者"
},
"toast": {
"uploadFailed": "アップロード失敗",
@@ -345,7 +526,8 @@
"imageUploadFailed": "画像のアップロードに失敗しました",
"uploadFailedInvalidUploadDesc": "画像はPNGかJPGである必要があります。",
"sentToUpscale": "アップスケーラーに転送しました",
"imageUploaded": "画像をアップロードしました"
"imageUploaded": "画像をアップロードしました",
"serverError": "サーバーエラー"
},
"accessibility": {
"invokeProgressBar": "進捗バー",
@@ -356,7 +538,7 @@
"menu": "メニュー",
"createIssue": "問題を報告",
"resetUI": "$t(accessibility.reset) UI",
"mode": "モード:",
"mode": "モード",
"about": "Invoke について",
"submitSupportTicket": "サポート依頼を送信する",
"uploadImages": "画像をアップロード",
@@ -373,7 +555,20 @@
"positivePrompt": "ポジティブプロンプト",
"strength": "Image to Image 強度",
"recallParameters": "パラメータを再使用",
"recallParameter": "{{label}} を再使用"
"recallParameter": "{{label}} を再使用",
"imageDimensions": "画像サイズ",
"imageDetails": "画像の詳細",
"model": "モデル",
"allPrompts": "すべてのプロンプト",
"cfgScale": "CFGスケール",
"createdBy": "作成:",
"metadata": "メタデータ",
"height": "高さ",
"negativePrompt": "ネガティブプロンプト",
"generationMode": "生成モード",
"vae": "VAE",
"cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)",
"canvasV2Metadata": "キャンバス"
},
"queue": {
"queueEmpty": "キューが空です",
@@ -405,7 +600,7 @@
"batchQueuedDesc_other": "{{count}} セッションをキューの{{direction}}に追加しました",
"graphQueued": "グラフをキューに追加しました",
"batch": "バッチ",
"clearQueueAlertDialog": "キューをクリアすると、処理中のアイテムは直ちにキャンセルされ、キューは完全にクリアされます。",
"clearQueueAlertDialog": "キューをクリアすると、処理中の項目は直ちにキャンセルされ、キューは完全にクリアされます。保留中のフィルターもキャンセルされます。",
"pending": "保留中",
"resumeFailed": "処理の再開に問題があります",
"clear": "クリア",
@@ -423,7 +618,7 @@
"enqueueing": "バッチをキューに追加",
"cancelBatchFailed": "バッチのキャンセルに問題があります",
"clearQueueAlertDialog2": "キューをクリアしてもよろしいですか?",
"item": "アイテム",
"item": "項目",
"graphFailedToQueue": "グラフをキューに追加できませんでした",
"batchFieldValues": "バッチの詳細",
"openQueue": "キューを開く",
@@ -439,7 +634,17 @@
"upscaling": "アップスケール",
"generation": "生成",
"other": "その他",
"gallery": "ギャラリー"
"gallery": "ギャラリー",
"cancelAllExceptCurrentQueueItemAlertDialog2": "すべての保留中のキュー項目をキャンセルしてもよいですか?",
"cancelAllExceptCurrentTooltip": "現在の項目を除いてすべてキャンセル",
"origin": "先頭",
"destination": "宛先",
"confirm": "確認",
"retryItem": "項目をリトライ",
"batchSize": "バッチサイズ",
"retryFailed": "項目のリトライに問題があります",
"cancelAllExceptCurrentQueueItemAlertDialog": "現在の項目を除くすべてのキュー項目をキャンセルすると、保留中の項目は停止しますが、進行中の項目は完了します。",
"retrySucceeded": "項目がリトライされました"
},
"models": {
"noMatchingModels": "一致するモデルがありません",
@@ -448,13 +653,14 @@
"noModelsAvailable": "使用可能なモデルがありません",
"selectModel": "モデルを選択してください",
"concepts": "コンセプト",
"addLora": "LoRAを追加"
"addLora": "LoRAを追加",
"lora": "LoRA"
},
"nodes": {
"addNode": "ノードを追加",
"boolean": "ブーリアン",
"addNodeToolTip": "ノードを追加 (Shift+A, Space)",
"missingTemplate": "テンプレートが見つかりません",
"missingTemplate": "Invalid node: タイプ {{type}} のノード {{node}} にテンプレートがりません(未インストール?)",
"loadWorkflow": "ワークフローを読み込み",
"hideLegendNodes": "フィールドタイプの凡例を非表示",
"float": "浮動小数点",
@@ -465,7 +671,7 @@
"currentImageDescription": "ノードエディタ内の現在の画像を表示",
"downloadWorkflow": "ワークフローのJSONをダウンロード",
"fieldTypesMustMatch": "フィールドタイプが一致している必要があります",
"edge": "輪郭",
"edge": "エッジ",
"animatedEdgesHelp": "選択したエッジおよび選択したノードに接続されたエッジをアニメーション化します",
"cannotDuplicateConnection": "重複した接続は作れません",
"noWorkflow": "ワークフローがありません",
@@ -484,7 +690,20 @@
"cannotConnectToSelf": "自身のノードには接続できません",
"colorCodeEdges": "カラー-Code Edges",
"loadingNodes": "ノードを読み込み中...",
"scheduler": "スケジューラー"
"scheduler": "スケジューラー",
"version": "バージョン",
"edit": "編集",
"nodeVersion": "ノードバージョン",
"workflowTags": "タグ",
"string": "文字列",
"workflowVersion": "バージョン",
"workflowAuthor": "作者",
"ipAdapter": "IP-Adapter",
"notes": "ノート",
"workflow": "ワークフロー",
"workflowName": "名前",
"workflowNotes": "ノート",
"enum": "Enum"
},
"boards": {
"autoAddBoard": "自動追加するボード",
@@ -506,7 +725,7 @@
"deleteBoard": "ボードの削除",
"deleteBoardAndImages": "ボードと画像の削除",
"deleteBoardOnly": "ボードのみ削除",
"deletedBoardsCannotbeRestored": "削除されたボードは復元できません",
"deletedBoardsCannotbeRestored": "削除されたボードは復元できません。\"ボードのみ削除\"を選択すると画像は未分類に移動されます。",
"movingImagesToBoard_other": "{{count}} の画像をボードに移動:",
"hideBoards": "ボードを隠す",
"assetsWithCount_other": "{{count}} のアセット",
@@ -518,7 +737,12 @@
"archiveBoard": "ボードをアーカイブ",
"archived": "アーカイブ完了",
"unarchiveBoard": "アーカイブされていないボード",
"imagesWithCount_other": "{{count}} の画像"
"imagesWithCount_other": "{{count}} の画像",
"updateBoardError": "ボード更新エラー",
"selectedForAutoAdd": "自動追加に選択済み",
"deletedPrivateBoardsCannotbeRestored": "削除されたボードは復元できません。\"ボードのみ削除\"を選択すると画像はその作成者のプライベートな未分類に移動されます。",
"noBoards": "{{boardType}} ボードがありません",
"viewBoards": "ボードを表示"
},
"invocationCache": {
"invocationCache": "呼び出しキャッシュ",
@@ -570,6 +794,57 @@
},
"paramAspect": {
"heading": "縦横比"
},
"refinerSteps": {
"heading": "ステップ"
},
"paramVAE": {
"heading": "VAE"
},
"scale": {
"heading": "スケール"
},
"refinerScheduler": {
"heading": "スケジューラー"
},
"compositingCoherenceMode": {
"heading": "モード"
},
"paramModel": {
"heading": "モデル"
},
"paramHeight": {
"heading": "高さ"
},
"paramSteps": {
"heading": "ステップ"
},
"ipAdapterMethod": {
"heading": "モード"
},
"paramSeed": {
"heading": "シード"
},
"paramIterations": {
"heading": "生成回数"
},
"controlNet": {
"heading": "ControlNet"
},
"paramWidth": {
"heading": "幅"
},
"lora": {
"heading": "LoRA"
},
"loraWeight": {
"heading": "重み"
},
"patchmatchDownScaleSize": {
"heading": "Downscale"
},
"controlNetWeight": {
"heading": "重み"
}
},
"accordions": {
@@ -579,7 +854,8 @@
"coherenceTab": "コヒーレンスパス"
},
"advanced": {
"title": "高度な設定"
"title": "高度",
"options": "$t(accordions.advanced.title) オプション"
},
"control": {
"title": "コントロール"
@@ -608,7 +884,11 @@
},
"ui": {
"tabs": {
"queue": "キュー"
"queue": "キュー",
"canvas": "キャンバス",
"workflows": "ワークフロー",
"models": "モデル",
"gallery": "ギャラリー"
}
},
"controlLayers": {
@@ -623,7 +903,8 @@
"bboxGroup": "バウンディングボックスから作成",
"cropCanvasToBbox": "キャンバスをバウンディングボックスでクロップ",
"newGlobalReferenceImage": "新規全域参照画像",
"newRegionalReferenceImage": "新規領域参照画像"
"newRegionalReferenceImage": "新規領域参照画像",
"canvasGroup": "キャンバス"
},
"regionalGuidance": "領域ガイダンス",
"globalReferenceImage": "全域参照画像",
@@ -644,7 +925,8 @@
"brush": "ブラシ",
"rectangle": "矩形",
"move": "移動",
"eraser": "消しゴム"
"eraser": "消しゴム",
"bbox": "Bbox"
},
"saveCanvasToGallery": "キャンバスをギャラリーに保存",
"saveBboxToGallery": "バウンディングボックスをギャラリーへ保存",
@@ -662,7 +944,27 @@
"canvas": "キャンバス",
"fitBboxToLayers": "バウンディングボックスをレイヤーにフィット",
"removeBookmark": "ブックマークを外す",
"savedToGalleryOk": "ギャラリーに保存しました"
"savedToGalleryOk": "ギャラリーに保存しました",
"controlMode": {
"prompt": "プロンプト"
},
"prompt": "プロンプト",
"settings": {
"snapToGrid": {
"off": "オフ",
"on": "オン"
}
},
"filter": {
"filter": "フィルター",
"spandrel_filter": {
"model": "モデル"
},
"apply": "適用",
"reset": "リセット",
"cancel": "キャンセル"
},
"weight": "重み"
},
"stylePresets": {
"clearTemplateSelection": "選択したテンプレートをクリア",
@@ -674,15 +976,54 @@
"createPromptTemplate": "プロンプトテンプレートを作成",
"promptTemplateCleared": "プロンプトテンプレートをクリアしました",
"searchByName": "名前で検索",
"toggleViewMode": "表示モードを切り替え"
"toggleViewMode": "表示モードを切り替え",
"negativePromptColumn": "'negative_prompt'",
"preview": "プレビュー",
"nameColumn": "'name'",
"type": "タイプ",
"private": "プライベート",
"name": "名称"
},
"upscaling": {
"upscaleModel": "アップスケールモデル",
"postProcessingModel": "ポストプロセスモデル",
"upscale": "アップスケール"
"upscale": "アップスケール",
"scale": "スケール"
},
"sdxl": {
"denoisingStrength": "ノイズ除去強度",
"scheduler": "スケジューラー"
"scheduler": "スケジューラー",
"loading": "ロード中...",
"steps": "ステップ",
"refiner": "Refiner"
},
"modelCache": {
"clear": "モデルキャッシュを消去",
"clearSucceeded": "モデルキャッシュを消去しました",
"clearFailed": "モデルキャッシュの消去中に問題が発生"
},
"workflows": {
"workflows": "ワークフロー",
"ascending": "昇順",
"name": "名前",
"descending": "降順"
},
"system": {
"logNamespaces": {
"system": "システム",
"gallery": "ギャラリー",
"workflows": "ワークフロー",
"models": "モデル",
"canvas": "キャンバス",
"metadata": "メタデータ",
"queue": "キュー"
},
"logLevel": {
"debug": "Debug",
"info": "Info",
"error": "Error",
"fatal": "Fatal",
"warn": "Warn"
}
}
}

View File

@@ -2311,11 +2311,7 @@
"watchUiUpdatesOverview": "Xem Tổng Quan Về Những Cập Nhật Cho Giao Diện Người Dùng",
"items": [
"Cải thiện các thiết lập mặc định của VRAM",
"Xoá bộ nhớ đệm của model theo yêu cầu",
"Mở rộng khả năng tương thích LoRA trên FLUX",
"Bộ lọc điều chỉnh ảnh trên Canvas",
"Huỷ tất cả trừ mục đang xếp hàng hiện tại",
"Sao chép và dán trên Canvas"
"Xoá bộ nhớ đệm của model theo yêu cầu"
]
},
"upsell": {

View File

@@ -3,6 +3,7 @@ import { enqueueRequested } from 'app/store/actions';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { extractMessageFromAssertionError } from 'common/util/extractMessageFromAssertionError';
import { withResult, withResultAsync } from 'common/util/result';
import { parseify } from 'common/util/serialize';
import { $canvasManager } from 'features/controlLayers/store/ephemeral';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildFLUXGraph } from 'features/nodes/util/graph/generation/buildFLUXGraph';
@@ -13,7 +14,6 @@ import { toast } from 'features/toast/toast';
import { serializeError } from 'serialize-error';
import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue';
import { assert, AssertionError } from 'tsafe';
import type { JsonObject } from 'type-fest';
const log = logger('generation');
@@ -80,16 +80,15 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(prepareBatchResult.value, enqueueMutationFixedCacheKeyOptions)
);
req.reset();
const enqueueResult = await withResultAsync(() => req.unwrap());
if (enqueueResult.isErr()) {
log.error({ error: serializeError(enqueueResult.error) }, 'Failed to enqueue batch');
return;
try {
await req.unwrap();
log.debug(parseify({ batchConfig: prepareBatchResult.value }), 'Enqueued batch');
} catch (error) {
log.error({ error: serializeError(error) }, 'Failed to enqueue batch');
} finally {
req.reset();
}
log.debug({ batchConfig: prepareBatchResult.value } as JsonObject, 'Enqueued batch');
},
});
};

View File

@@ -1,5 +1,7 @@
import { logger } from 'app/logging/logger';
import { enqueueRequested } from 'app/store/actions';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { parseify } from 'common/util/serialize';
import { $templates } from 'features/nodes/store/nodesSlice';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { isBatchNode, isInvocationNode } from 'features/nodes/types/invocation';
@@ -7,9 +9,12 @@ import { buildNodesGraph } from 'features/nodes/util/graph/buildNodesGraph';
import { resolveBatchValue } from 'features/nodes/util/node/resolveBatchValue';
import { buildWorkflowWithValidation } from 'features/nodes/util/workflow/buildWorkflow';
import { groupBy } from 'lodash-es';
import { serializeError } from 'serialize-error';
import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue';
import type { Batch, BatchConfig } from 'services/api/types';
const log = logger('generation');
export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) => {
startAppListening({
predicate: (action): action is ReturnType<typeof enqueueRequested> =>
@@ -101,6 +106,9 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) =
const req = dispatch(queueApi.endpoints.enqueueBatch.initiate(batchConfig, enqueueMutationFixedCacheKeyOptions));
try {
await req.unwrap();
log.debug(parseify({ batchConfig }), 'Enqueued batch');
} catch (error) {
log.error({ error: serializeError(error) }, 'Failed to enqueue batch');
} finally {
req.reset();
}

View File

@@ -1,9 +1,14 @@
import { logger } from 'app/logging/logger';
import { enqueueRequested } from 'app/store/actions';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { parseify } from 'common/util/serialize';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildMultidiffusionUpscaleGraph } from 'features/nodes/util/graph/buildMultidiffusionUpscaleGraph';
import { serializeError } from 'serialize-error';
import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue';
const log = logger('generation');
export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening) => {
startAppListening({
predicate: (action): action is ReturnType<typeof enqueueRequested> =>
@@ -19,6 +24,9 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening)
const req = dispatch(queueApi.endpoints.enqueueBatch.initiate(batchConfig, enqueueMutationFixedCacheKeyOptions));
try {
await req.unwrap();
log.debug(parseify({ batchConfig }), 'Enqueued batch');
} catch (error) {
log.error({ error: serializeError(error) }, 'Failed to enqueue batch');
} finally {
req.reset();
}

View File

@@ -0,0 +1,17 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import type { Opts as LinkifyOpts } from 'linkifyjs';
export const linkifySx: SystemStyleObject = {
a: {
fontWeight: 'semibold',
},
'a:hover': {
textDecoration: 'underline',
},
};
export const linkifyOptions: LinkifyOpts = {
target: '_blank',
rel: 'noopener noreferrer',
validate: (value) => /^https?:\/\//.test(value),
};

View File

@@ -4,7 +4,7 @@ import type { CanvasState } from 'features/controlLayers/store/types';
import { selectDeleteImageModalSlice } from 'features/deleteImageModal/store/slice';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import type { NodesState } from 'features/nodes/store/types';
import { isImageFieldInputInstance } from 'features/nodes/types/field';
import { isImageFieldCollectionInputInstance, isImageFieldInputInstance } from 'features/nodes/types/field';
import { isInvocationNode } from 'features/nodes/types/invocation';
import type { UpscaleState } from 'features/parameters/store/upscaleSlice';
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
@@ -13,11 +13,23 @@ import { some } from 'lodash-es';
import type { ImageUsage } from './types';
// TODO(psyche): handle image deletion (canvas staging area?)
export const getImageUsage = (nodes: NodesState, canvas: CanvasState, upscale: UpscaleState, image_name: string) => {
const isNodesImage = nodes.nodes
.filter(isInvocationNode)
.some((node) =>
some(node.data.inputs, (input) => isImageFieldInputInstance(input) && input.value?.image_name === image_name)
);
const isNodesImage = nodes.nodes.filter(isInvocationNode).some((node) =>
some(node.data.inputs, (input) => {
if (isImageFieldInputInstance(input)) {
if (input.value?.image_name === image_name) {
return true;
}
}
if (isImageFieldCollectionInputInstance(input)) {
if (input.value?.some((value) => value?.image_name === image_name)) {
return true;
}
}
return false;
})
);
const isUpscaleImage = upscale.upscaleInitialImage?.image_name === image_name;

View File

@@ -1,5 +1,6 @@
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
import { Combobox, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { EMPTY_ARRAY } from 'app/store/constants';
import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
import type { ImageGeneratorImagesFromBoard } from 'features/nodes/types/field';
import { memo, useCallback, useMemo } from 'react';
@@ -49,31 +50,21 @@ const BoardCombobox = ({
board_id,
onChange: _onChange,
}: {
board_id: string;
board_id: string | undefined;
onChange: (board_id: string) => void;
}) => {
const { t } = useTranslation();
const listAllBoardsQuery = useListAllBoardsQuery(listAllBoardsQueryArg);
const noneOption = useMemo<ComboboxOption>(() => {
return {
label: `${t('common.none')} (${t('boards.uncategorized')})`,
value: 'none',
};
}, [t]);
const options = useMemo<ComboboxOption[]>(() => {
const _options: ComboboxOption[] = [noneOption];
if (listAllBoardsQuery.data) {
for (const board of listAllBoardsQuery.data) {
_options.push({
label: board.board_name,
value: board.board_id,
});
}
if (!listAllBoardsQuery.data) {
return EMPTY_ARRAY;
}
return _options;
}, [listAllBoardsQuery.data, noneOption]);
return listAllBoardsQuery.data.map((board) => ({
label: board.board_name,
value: board.board_id,
}));
}, [listAllBoardsQuery.data]);
const onChange = useCallback<ComboboxOnChange>(
(v) => {
@@ -87,13 +78,7 @@ const BoardCombobox = ({
[_onChange]
);
const value = useMemo(() => {
if (board_id === 'none') {
return noneOption;
}
const boardOption = options.find((o) => o.value === board_id);
return boardOption ?? noneOption;
}, [board_id, options, noneOption]);
const value = useMemo(() => options.find((o) => o.value === board_id) ?? null, [board_id, options]);
const noOptionsMessage = useCallback(() => t('boards.noMatching'), [t]);

View File

@@ -1,16 +1,21 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { Flex, IconButton, Spacer } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import AddNodeButton from 'features/nodes/components/flow/panels/TopPanel/AddNodeButton';
import ClearFlowButton from 'features/nodes/components/flow/panels/TopPanel/ClearFlowButton';
import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton';
import UpdateNodesButton from 'features/nodes/components/flow/panels/TopPanel/UpdateNodesButton';
import { useWorkflowEditorSettingsModal } from 'features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings';
import { WorkflowName } from 'features/nodes/components/sidePanel/WorkflowName';
import { selectWorkflowName } from 'features/nodes/store/workflowSlice';
import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiGearSixFill } from 'react-icons/pi';
const TopCenterPanel = () => {
const name = useAppSelector(selectWorkflowName);
const modal = useWorkflowEditorSettingsModal();
const { t } = useTranslation();
return (
<Flex gap={2} top={2} left={2} right={2} position="absolute" alignItems="flex-start" pointerEvents="none">
<Flex gap="2">
@@ -22,7 +27,12 @@ const TopCenterPanel = () => {
<Spacer />
<ClearFlowButton />
<SaveWorkflowButton />
<WorkflowLibraryMenu />
<IconButton
pointerEvents="auto"
aria-label={t('workflows.workflowEditorMenu')}
icon={<PiGearSixFill />}
onClick={modal.setTrue}
/>
</Flex>
);
};

View File

@@ -1,6 +1,8 @@
import { Text } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { linkifyOptions, linkifySx } from 'common/components/linkify';
import { selectWorkflowDescription } from 'features/nodes/store/workflowSlice';
import Linkify from 'linkify-react';
import { memo } from 'react';
export const ActiveWorkflowDescription = memo(() => {
@@ -11,8 +13,8 @@ export const ActiveWorkflowDescription = memo(() => {
}
return (
<Text color="base.300" fontStyle="italic" noOfLines={1} pb={2}>
{description}
<Text color="base.300" fontStyle="italic" pb={2} sx={linkifySx}>
<Linkify options={linkifyOptions}>{description}</Linkify>
</Text>
);
});

View File

@@ -1,9 +1,9 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { NewWorkflowButton } from 'features/nodes/components/sidePanel/NewWorkflowButton';
import { WorkflowListMenuTrigger } from 'features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListMenuTrigger';
import { WorkflowViewEditToggleButton } from 'features/nodes/components/sidePanel/WorkflowViewEditToggleButton';
import { selectWorkflowMode } from 'features/nodes/store/workflowSlice';
import { WorkflowLibraryMenu } from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu';
import { memo } from 'react';
import SaveWorkflowButton from './SaveWorkflowButton';
@@ -17,7 +17,7 @@ export const ActiveWorkflowNameAndActions = memo(() => {
<Spacer />
{mode === 'edit' && <SaveWorkflowButton />}
<WorkflowViewEditToggleButton />
<NewWorkflowButton />
<WorkflowLibraryMenu />
</Flex>
);
});

View File

@@ -15,6 +15,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { useWorkflowListMenu } from 'features/nodes/store/workflowListMenu';
import { selectWorkflowName } from 'features/nodes/store/workflowSlice';
import { NewWorkflowButton } from 'features/workflowLibrary/components/NewWorkflowButton';
import UploadWorkflowButton from 'features/workflowLibrary/components/UploadWorkflowButton';
import { useRef } from 'react';
import { useTranslation } from 'react-i18next';
@@ -63,6 +64,7 @@ export const WorkflowListMenuTrigger = () => {
<WorkflowSearch searchInputRef={searchInputRef} />
<WorkflowSortControl />
<UploadWorkflowButton />
<NewWorkflowButton />
</Flex>
<Box position="relative" w="full" h="full">
<ScrollableContent>

View File

@@ -50,7 +50,7 @@ const ContainerElement = memo(({ id }: { id: string }) => {
ContainerElement.displayName = 'ContainerElementComponent';
const containerViewModeSx: SystemStyleObject = {
gap: 4,
gap: 2,
'&[data-self-layout="column"]': {
flexDir: 'column',
alignItems: 'stretch',
@@ -197,7 +197,7 @@ const rootViewModeSx: SystemStyleObject = {
borderRadius: 'base',
w: 'full',
h: 'full',
gap: 4,
gap: 2,
display: 'flex',
flex: 1,
maxW: '768px',
@@ -232,6 +232,7 @@ RootContainerElementViewMode.displayName = 'RootContainerElementViewMode';
const rootEditModeSx: SystemStyleObject = {
...rootViewModeSx,
gap: 4,
'&[data-is-dragging-over="true"]': {
opacity: 1,
bg: 'base.850',

View File

@@ -9,6 +9,7 @@ import {
PopoverBody,
PopoverContent,
PopoverTrigger,
Portal,
} from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { formElementContainerDataChanged } from 'features/nodes/store/workflowSlice';
@@ -36,22 +37,24 @@ export const ContainerElementSettings = memo(({ element }: { element: ContainerE
<PopoverTrigger>
<IconButton aria-label="settings" icon={<PiWrenchFill />} variant="link" size="sm" alignSelf="stretch" />
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverBody>
<FormControl>
<FormLabel m={0}>{t('workflows.builder.layout')}</FormLabel>
<ButtonGroup variant="outline" size="sm">
<Button onClick={setLayoutToRow} colorScheme={layout === 'row' ? 'invokeBlue' : 'base'}>
{t('workflows.builder.row')}
</Button>
<Button onClick={setLayoutToColumn} colorScheme={layout === 'column' ? 'invokeBlue' : 'base'}>
{t('workflows.builder.column')}
</Button>
</ButtonGroup>
</FormControl>
</PopoverBody>
</PopoverContent>
<Portal>
<PopoverContent>
<PopoverArrow />
<PopoverBody>
<FormControl>
<FormLabel m={0}>{t('workflows.builder.layout')}</FormLabel>
<ButtonGroup variant="outline" size="sm">
<Button onClick={setLayoutToRow} colorScheme={layout === 'row' ? 'invokeBlue' : 'base'}>
{t('workflows.builder.row')}
</Button>
<Button onClick={setLayoutToColumn} colorScheme={layout === 'column' ? 'invokeBlue' : 'base'}>
{t('workflows.builder.column')}
</Button>
</ButtonGroup>
</FormControl>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
);
});

View File

@@ -1,5 +1,7 @@
import type { HeadingProps, SystemStyleObject } from '@invoke-ai/ui-library';
import { Text } from '@invoke-ai/ui-library';
import { linkifyOptions, linkifySx } from 'common/components/linkify';
import Linkify from 'linkify-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -9,13 +11,14 @@ const headingSx: SystemStyleObject = {
'&[data-is-empty="true"]': {
opacity: 0.3,
},
...linkifySx,
};
export const HeadingElementContent = memo(({ content, ...rest }: { content: string } & HeadingProps) => {
const { t } = useTranslation();
return (
<Text sx={headingSx} data-is-empty={content === ''} {...rest}>
{content || t('workflows.builder.headingPlaceholder')}
<Linkify options={linkifyOptions}>{content || t('workflows.builder.headingPlaceholder')}</Linkify>
</Text>
);
});

View File

@@ -1,10 +1,12 @@
import { FormHelperText, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { linkifyOptions, linkifySx } from 'common/components/linkify';
import { useEditable } from 'common/hooks/useEditable';
import { useInputFieldDescription } from 'features/nodes/hooks/useInputFieldDescription';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { fieldDescriptionChanged } from 'features/nodes/store/nodesSlice';
import type { NodeFieldElement } from 'features/nodes/types/workflow';
import Linkify from 'linkify-react';
import { memo, useCallback, useRef } from 'react';
export const NodeFieldElementDescriptionEditable = memo(({ el }: { el: NodeFieldElement }) => {
@@ -36,7 +38,11 @@ export const NodeFieldElementDescriptionEditable = memo(({ el }: { el: NodeField
});
if (!editable.isEditing) {
return <FormHelperText onDoubleClick={editable.startEditing}>{editable.value}</FormHelperText>;
return (
<FormHelperText onDoubleClick={editable.startEditing} sx={linkifySx}>
<Linkify options={linkifyOptions}>{editable.value}</Linkify>
</FormHelperText>
);
}
return (

View File

@@ -7,6 +7,7 @@ import {
PopoverBody,
PopoverContent,
PopoverTrigger,
Portal,
Switch,
} from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
@@ -65,18 +66,20 @@ export const NodeFieldElementSettings = memo(({ element }: { element: NodeFieldE
<PopoverTrigger>
<IconButton aria-label="settings" icon={<PiWrenchFill />} variant="link" size="sm" alignSelf="stretch" />
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverBody minW={48}>
<FormControl>
<FormLabel flex={1}>{t('workflows.builder.showDescription')}</FormLabel>
<Switch size="sm" isChecked={showDescription} onChange={toggleShowDescription} />
</FormControl>
{settings?.type === 'integer-field-config' && <NodeFieldElementIntegerConfig id={id} config={settings} />}
{settings?.type === 'float-field-config' && <NodeFieldElementFloatSettings id={id} config={settings} />}
{settings?.type === 'string-field-config' && <NodeFieldElementStringSettings id={id} config={settings} />}
</PopoverBody>
</PopoverContent>
<Portal>
<PopoverContent>
<PopoverArrow />
<PopoverBody minW={48}>
<FormControl>
<FormLabel flex={1}>{t('workflows.builder.showDescription')}</FormLabel>
<Switch size="sm" isChecked={showDescription} onChange={toggleShowDescription} />
</FormControl>
{settings?.type === 'integer-field-config' && <NodeFieldElementIntegerConfig id={id} config={settings} />}
{settings?.type === 'float-field-config' && <NodeFieldElementFloatSettings id={id} config={settings} />}
{settings?.type === 'string-field-config' && <NodeFieldElementStringSettings id={id} config={settings} />}
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
);
});

View File

@@ -1,5 +1,6 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex, FormControl, FormHelperText } from '@invoke-ai/ui-library';
import { linkifyOptions, linkifySx } from 'common/components/linkify';
import { InputFieldRenderer } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
import { useContainerContext } from 'features/nodes/components/sidePanel/builder/contexts';
import { NodeFieldElementLabel } from 'features/nodes/components/sidePanel/builder/NodeFieldElementLabel';
@@ -7,6 +8,7 @@ import { useInputFieldDescription } from 'features/nodes/hooks/useInputFieldDesc
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import type { NodeFieldElement } from 'features/nodes/types/workflow';
import { NODE_FIELD_CLASS_NAME } from 'features/nodes/types/workflow';
import Linkify from 'linkify-react';
import { memo, useMemo } from 'react';
const sx: SystemStyleObject = {
@@ -18,6 +20,9 @@ const sx: SystemStyleObject = {
flex: '1 1 0',
minW: 32,
},
'&[data-with-description="false"]': {
pb: 2,
},
};
export const NodeFieldElementViewMode = memo(({ el }: { el: NodeFieldElement }) => {
@@ -33,7 +38,13 @@ export const NodeFieldElementViewMode = memo(({ el }: { el: NodeFieldElement })
);
return (
<Flex id={id} className={NODE_FIELD_CLASS_NAME} sx={sx} data-parent-layout={containerCtx.layout}>
<Flex
id={id}
className={NODE_FIELD_CLASS_NAME}
sx={sx}
data-parent-layout={containerCtx.layout}
data-with-description={showDescription && !!_description}
>
<FormControl flex="1 1 0" orientation="vertical">
<NodeFieldElementLabel el={el} />
<Flex w="full" gap={4}>
@@ -43,7 +54,11 @@ export const NodeFieldElementViewMode = memo(({ el }: { el: NodeFieldElement })
settings={data.settings}
/>
</Flex>
{showDescription && _description && <FormHelperText>{_description}</FormHelperText>}
{showDescription && _description && (
<FormHelperText sx={linkifySx}>
<Linkify options={linkifyOptions}>{_description}</Linkify>
</FormHelperText>
)}
</FormControl>
</Flex>
);

View File

@@ -1,5 +1,7 @@
import type { SystemStyleObject, TextProps } from '@invoke-ai/ui-library';
import { Text } from '@invoke-ai/ui-library';
import { linkifyOptions, linkifySx } from 'common/components/linkify';
import Linkify from 'linkify-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -9,13 +11,14 @@ const textSx: SystemStyleObject = {
'&[data-is-empty="true"]': {
opacity: 0.3,
},
...linkifySx,
};
export const TextElementContent = memo(({ content, ...rest }: { content: string } & TextProps) => {
const { t } = useTranslation();
return (
<Text sx={textSx} data-is-empty={content === ''} {...rest}>
{content || t('workflows.builder.textPlaceholder')}
<Linkify options={linkifyOptions}>{content || t('workflows.builder.textPlaceholder')}</Linkify>
</Text>
);
});

View File

@@ -1534,7 +1534,7 @@ export const getStringGeneratorDefaults = (type: StringGeneratorFieldValue['type
export const ImageGeneratorImagesFromBoardType = 'image_generator_images_from_board';
const zImageGeneratorImagesFromBoard = z.object({
type: z.literal(ImageGeneratorImagesFromBoardType).default(ImageGeneratorImagesFromBoardType),
board_id: z.string().trim().min(1).optional().default('none'),
board_id: z.string().trim().min(1).optional(),
category: z.union([z.literal('images'), z.literal('assets')]).default('images'),
});
export type ImageGeneratorImagesFromBoard = z.infer<typeof zImageGeneratorImagesFromBoard>;
@@ -1544,10 +1544,13 @@ const getImageGeneratorImagesFromBoardValues = async (
dispatch: AppDispatch
) => {
const { board_id, category } = generator;
if (!board_id) {
return EMPTY_ARRAY;
}
const req = dispatch(
boardsApi.endpoints.listAllImageNamesForBoard.initiate(
{
board_id: board_id ?? 'none',
board_id,
categories: category === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES,
is_intermediate: false,
},

View File

@@ -23,7 +23,6 @@ export const NewWorkflowButton = memo(() => {
<IconButton
onClick={onClickNewWorkflow}
variant="ghost"
size="sm"
aria-label={t('nodes.newWorkflow')}
tooltip={t('nodes.newWorkflow')}
icon={<PiFilePlusBold />}

View File

@@ -1,18 +0,0 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useWorkflowEditorSettingsModal } from 'features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiGearSixFill } from 'react-icons/pi';
const DownloadWorkflowMenuItem = () => {
const { t } = useTranslation();
const modal = useWorkflowEditorSettingsModal();
return (
<MenuItem as="button" icon={<PiGearSixFill />} onClick={modal.setTrue}>
{t('nodes.workflowSettings')}
</MenuItem>
);
};
export default memo(DownloadWorkflowMenuItem);

View File

@@ -13,13 +13,12 @@ import LoadWorkflowFromGraphMenuItem from 'features/workflowLibrary/components/W
import { NewWorkflowMenuItem } from 'features/workflowLibrary/components/WorkflowLibraryMenu/NewWorkflowMenuItem';
import SaveWorkflowAsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem';
import SaveWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem';
import SettingsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem';
import UploadWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/UploadWorkflowMenuItem';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDotsThreeOutlineFill } from 'react-icons/pi';
const WorkflowLibraryMenu = () => {
export const WorkflowLibraryMenu = memo(() => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const shift = useShiftModifier();
@@ -31,6 +30,8 @@ const WorkflowLibraryMenu = () => {
aria-label={t('workflows.workflowEditorMenu')}
icon={<PiDotsThreeOutlineFill />}
pointerEvents="auto"
size="sm"
variant="ghost"
/>
<MenuList pointerEvents="auto">
<NewWorkflowMenuItem />
@@ -39,13 +40,10 @@ const WorkflowLibraryMenu = () => {
<SaveWorkflowMenuItem />
<SaveWorkflowAsMenuItem />
<DownloadWorkflowMenuItem />
<MenuDivider />
<SettingsMenuItem />
{shift && <MenuDivider />}
{shift && <LoadWorkflowFromGraphMenuItem />}
</MenuList>
</Menu>
);
};
export default memo(WorkflowLibraryMenu);
});
WorkflowLibraryMenu.displayName = 'WorkflowLibraryMenu';

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
__version__ = "5.7.0rc1"
__version__ = "5.7.1"

View File

@@ -1,3 +1,5 @@
import json
import pytest
from pydantic import TypeAdapter, ValidationError
@@ -44,46 +46,46 @@ def test_create_sessions_from_batch_with_runs(batch_data_collection, batch_graph
# 2 list[BatchDatum] * length 2 * 2 runs = 8
assert len(t) == 8
assert t[0][0].graph.get_node("1").prompt == "Banana sushi"
assert t[0][0].graph.get_node("2").prompt == "Strawberry sushi"
assert t[0][0].graph.get_node("3").prompt == "Orange sushi"
assert t[0][0].graph.get_node("4").prompt == "Nissan"
assert json.loads(t[0][1])["graph"]["nodes"]["1"]["prompt"] == "Banana sushi"
assert json.loads(t[0][1])["graph"]["nodes"]["2"]["prompt"] == "Strawberry sushi"
assert json.loads(t[0][1])["graph"]["nodes"]["3"]["prompt"] == "Orange sushi"
assert json.loads(t[0][1])["graph"]["nodes"]["4"]["prompt"] == "Nissan"
assert t[1][0].graph.get_node("1").prompt == "Banana sushi"
assert t[1][0].graph.get_node("2").prompt == "Strawberry sushi"
assert t[1][0].graph.get_node("3").prompt == "Apple sushi"
assert t[1][0].graph.get_node("4").prompt == "Nissan"
assert json.loads(t[1][1])["graph"]["nodes"]["1"]["prompt"] == "Banana sushi"
assert json.loads(t[1][1])["graph"]["nodes"]["2"]["prompt"] == "Strawberry sushi"
assert json.loads(t[1][1])["graph"]["nodes"]["3"]["prompt"] == "Apple sushi"
assert json.loads(t[1][1])["graph"]["nodes"]["4"]["prompt"] == "Nissan"
assert t[2][0].graph.get_node("1").prompt == "Grape sushi"
assert t[2][0].graph.get_node("2").prompt == "Blueberry sushi"
assert t[2][0].graph.get_node("3").prompt == "Orange sushi"
assert t[2][0].graph.get_node("4").prompt == "Nissan"
assert json.loads(t[2][1])["graph"]["nodes"]["1"]["prompt"] == "Grape sushi"
assert json.loads(t[2][1])["graph"]["nodes"]["2"]["prompt"] == "Blueberry sushi"
assert json.loads(t[2][1])["graph"]["nodes"]["3"]["prompt"] == "Orange sushi"
assert json.loads(t[2][1])["graph"]["nodes"]["4"]["prompt"] == "Nissan"
assert t[3][0].graph.get_node("1").prompt == "Grape sushi"
assert t[3][0].graph.get_node("2").prompt == "Blueberry sushi"
assert t[3][0].graph.get_node("3").prompt == "Apple sushi"
assert t[3][0].graph.get_node("4").prompt == "Nissan"
assert json.loads(t[3][1])["graph"]["nodes"]["1"]["prompt"] == "Grape sushi"
assert json.loads(t[3][1])["graph"]["nodes"]["2"]["prompt"] == "Blueberry sushi"
assert json.loads(t[3][1])["graph"]["nodes"]["3"]["prompt"] == "Apple sushi"
assert json.loads(t[3][1])["graph"]["nodes"]["4"]["prompt"] == "Nissan"
# repeat for second run
assert t[4][0].graph.get_node("1").prompt == "Banana sushi"
assert t[4][0].graph.get_node("2").prompt == "Strawberry sushi"
assert t[4][0].graph.get_node("3").prompt == "Orange sushi"
assert t[4][0].graph.get_node("4").prompt == "Nissan"
assert json.loads(t[4][1])["graph"]["nodes"]["1"]["prompt"] == "Banana sushi"
assert json.loads(t[4][1])["graph"]["nodes"]["2"]["prompt"] == "Strawberry sushi"
assert json.loads(t[4][1])["graph"]["nodes"]["3"]["prompt"] == "Orange sushi"
assert json.loads(t[4][1])["graph"]["nodes"]["4"]["prompt"] == "Nissan"
assert t[5][0].graph.get_node("1").prompt == "Banana sushi"
assert t[5][0].graph.get_node("2").prompt == "Strawberry sushi"
assert t[5][0].graph.get_node("3").prompt == "Apple sushi"
assert t[5][0].graph.get_node("4").prompt == "Nissan"
assert json.loads(t[5][1])["graph"]["nodes"]["1"]["prompt"] == "Banana sushi"
assert json.loads(t[5][1])["graph"]["nodes"]["2"]["prompt"] == "Strawberry sushi"
assert json.loads(t[5][1])["graph"]["nodes"]["3"]["prompt"] == "Apple sushi"
assert json.loads(t[5][1])["graph"]["nodes"]["4"]["prompt"] == "Nissan"
assert t[6][0].graph.get_node("1").prompt == "Grape sushi"
assert t[6][0].graph.get_node("2").prompt == "Blueberry sushi"
assert t[6][0].graph.get_node("3").prompt == "Orange sushi"
assert t[6][0].graph.get_node("4").prompt == "Nissan"
assert json.loads(t[6][1])["graph"]["nodes"]["1"]["prompt"] == "Grape sushi"
assert json.loads(t[6][1])["graph"]["nodes"]["2"]["prompt"] == "Blueberry sushi"
assert json.loads(t[6][1])["graph"]["nodes"]["3"]["prompt"] == "Orange sushi"
assert json.loads(t[6][1])["graph"]["nodes"]["4"]["prompt"] == "Nissan"
assert t[7][0].graph.get_node("1").prompt == "Grape sushi"
assert t[7][0].graph.get_node("2").prompt == "Blueberry sushi"
assert t[7][0].graph.get_node("3").prompt == "Apple sushi"
assert t[7][0].graph.get_node("4").prompt == "Nissan"
assert json.loads(t[7][1])["graph"]["nodes"]["1"]["prompt"] == "Grape sushi"
assert json.loads(t[7][1])["graph"]["nodes"]["2"]["prompt"] == "Blueberry sushi"
assert json.loads(t[7][1])["graph"]["nodes"]["3"]["prompt"] == "Apple sushi"
assert json.loads(t[7][1])["graph"]["nodes"]["4"]["prompt"] == "Nissan"
def test_create_sessions_from_batch_without_runs(batch_data_collection, batch_graph):
@@ -127,7 +129,7 @@ def test_prepare_values_to_insert(batch_data_collection, batch_graph):
GraphExecutionStateValidator = TypeAdapter(GraphExecutionState)
# graph should be serialized
ges = GraphExecutionStateValidator.validate_json(values[0].session)
ges = GraphExecutionStateValidator.validate_json(values[0][1])
# graph values should be populated
assert ges.graph.get_node("1").prompt == "Banana sushi"
@@ -136,26 +138,26 @@ def test_prepare_values_to_insert(batch_data_collection, batch_graph):
assert ges.graph.get_node("4").prompt == "Nissan"
# session ids should match deserialized graph
assert [v.session_id for v in values] == [GraphExecutionStateValidator.validate_json(v.session).id for v in values]
assert [v[2] for v in values] == [GraphExecutionStateValidator.validate_json(v[1]).id for v in values]
# should unique session ids
sids = [v.session_id for v in values]
sids = [v[2] for v in values]
assert len(sids) == len(set(sids))
NodeFieldValueValidator = TypeAdapter(list[NodeFieldValue])
# should have 3 node field values
assert isinstance(values[0].field_values, str)
assert len(NodeFieldValueValidator.validate_json(values[0].field_values)) == 3
assert isinstance(values[0][4], str)
assert len(NodeFieldValueValidator.validate_json(values[0][4])) == 3
# should have batch id and priority
assert all(v.batch_id == b.batch_id for v in values)
assert all(v.priority == 0 for v in values)
assert all(v[3] == b.batch_id for v in values)
assert all(v[5] == 0 for v in values)
def test_prepare_values_to_insert_with_priority(batch_data_collection, batch_graph):
b = Batch(graph=batch_graph, data=batch_data_collection, runs=2)
values = prepare_values_to_insert(queue_id="default", batch=b, priority=1, max_new_queue_items=1000)
assert all(v.priority == 1 for v in values)
assert all(v[5] == 1 for v in values)
def test_prepare_values_to_insert_with_max(batch_data_collection, batch_graph):