mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-19 00:08:04 -05:00
Compare commits
33 Commits
ryan/flux-
...
v5.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8957aa50d | ||
|
|
807f458f13 | ||
|
|
68dbe45315 | ||
|
|
bd3d1dcdf9 | ||
|
|
386c01ede1 | ||
|
|
c224971cb4 | ||
|
|
ca55ef1da5 | ||
|
|
3072d80171 | ||
|
|
d5f2f4dc4e | ||
|
|
b2552323b8 | ||
|
|
61d217e377 | ||
|
|
57a80c456a | ||
|
|
211b2f84ed | ||
|
|
ea4104c7c4 | ||
|
|
8b46c2dc53 | ||
|
|
9812b9676b | ||
|
|
6efa1597eb | ||
|
|
cd6ef3edb3 | ||
|
|
fcdbb729d3 | ||
|
|
c0657072ec | ||
|
|
7167a5d3f4 | ||
|
|
8cf0d8c8d3 | ||
|
|
48311f38ba | ||
|
|
7631d55c2a | ||
|
|
ea0dc09c64 | ||
|
|
a424552c82 | ||
|
|
ba8ef6ff0f | ||
|
|
3463a968c7 | ||
|
|
c256826015 | ||
|
|
7d38a9b7fb | ||
|
|
249da858df | ||
|
|
d332d81866 | ||
|
|
21017edcde |
@@ -39,7 +39,8 @@ To use a community workflow, download the `.json` node graph file and load it in
|
||||
+ [Match Histogram](#match-histogram)
|
||||
+ [Metadata-Linked](#metadata-linked-nodes)
|
||||
+ [Negative Image](#negative-image)
|
||||
+ [Nightmare Promptgen](#nightmare-promptgen)
|
||||
+ [Nightmare Promptgen](#nightmare-promptgen)
|
||||
+ [One Button Prompt](#one-button-prompt)
|
||||
+ [Oobabooga](#oobabooga)
|
||||
+ [Prompt Tools](#prompt-tools)
|
||||
+ [Remote Image](#remote-image)
|
||||
@@ -389,6 +390,21 @@ View:
|
||||
|
||||
**Node Link:** [https://github.com/gogurtenjoyer/nightmare-promptgen](https://github.com/gogurtenjoyer/nightmare-promptgen)
|
||||
|
||||
--------------------------------
|
||||
### One Button Prompt
|
||||
|
||||
<img src="https://github.com/AIrjen/OneButtonPrompt_X_InvokeAI/blob/main/images/background.png" width="800" />
|
||||
|
||||
**Description:** an extensive suite of auto prompt generation and prompt helper nodes based on extensive logic. Get creative with the best prompt generator in the world.
|
||||
|
||||
The main node generates interesting prompts based on a set of parameters. There are also some additional nodes such as Auto Negative Prompt, One Button Artify, Create Prompt Variant and other cool prompt toys to play around with.
|
||||
|
||||
**Node Link:** [https://github.com/AIrjen/OneButtonPrompt_X_InvokeAI](https://github.com/AIrjen/OneButtonPrompt_X_InvokeAI)
|
||||
|
||||
**Nodes:**
|
||||
|
||||
<img src="https://github.com/AIrjen/OneButtonPrompt_X_InvokeAI/blob/main/images/OBP_nodes_invokeai.png" width="800" />
|
||||
|
||||
--------------------------------
|
||||
### Oobabooga
|
||||
|
||||
|
||||
@@ -7,13 +7,14 @@ from pathlib import Path
|
||||
|
||||
import torch
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi_events.handlers.local import local_handler
|
||||
from fastapi_events.middleware import EventHandlerASGIMiddleware
|
||||
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
||||
from torch.backends.mps import is_available as is_mps_available
|
||||
|
||||
# for PyCharm:
|
||||
@@ -78,6 +79,29 @@ app = FastAPI(
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
|
||||
class RedirectRootWithQueryStringMiddleware(BaseHTTPMiddleware):
|
||||
"""When a request is made to the root path with a query string, redirect to the root path without the query string.
|
||||
|
||||
For example, to force a Gradio app to use dark mode, users may append `?__theme=dark` to the URL. Their browser may
|
||||
have this query string saved in history or a bookmark, so when the user navigates to `http://127.0.0.1:9090/`, the
|
||||
browser takes them to `http://127.0.0.1:9090/?__theme=dark`.
|
||||
|
||||
This breaks the static file serving in the UI, so we redirect the user to the root path without the query string.
|
||||
"""
|
||||
|
||||
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
|
||||
if request.url.path == "/" and request.url.query:
|
||||
return RedirectResponse(url="/")
|
||||
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
|
||||
# Add the middleware
|
||||
app.add_middleware(RedirectRootWithQueryStringMiddleware)
|
||||
|
||||
|
||||
# Add event handler
|
||||
event_handler_id: int = id(app)
|
||||
app.add_middleware(
|
||||
|
||||
@@ -30,6 +30,7 @@ from invokeai.backend.flux.sampling_utils import (
|
||||
pack,
|
||||
unpack,
|
||||
)
|
||||
from invokeai.backend.lora.conversions.flux_lora_constants import FLUX_LORA_TRANSFORMER_PREFIX
|
||||
from invokeai.backend.lora.lora_model_raw import LoRAModelRaw
|
||||
from invokeai.backend.lora.lora_patcher import LoRAPatcher
|
||||
from invokeai.backend.model_manager.config import ModelFormat
|
||||
@@ -208,7 +209,7 @@ class FluxDenoiseInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
LoRAPatcher.apply_lora_patches(
|
||||
model=transformer,
|
||||
patches=self._lora_iterator(context),
|
||||
prefix="",
|
||||
prefix=FLUX_LORA_TRANSFORMER_PREFIX,
|
||||
cached_weights=cached_weights,
|
||||
)
|
||||
)
|
||||
@@ -219,7 +220,7 @@ class FluxDenoiseInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
LoRAPatcher.apply_lora_sidecar_patches(
|
||||
model=transformer,
|
||||
patches=self._lora_iterator(context),
|
||||
prefix="",
|
||||
prefix=FLUX_LORA_TRANSFORMER_PREFIX,
|
||||
dtype=inference_dtype,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ from invokeai.app.invocations.baseinvocation import (
|
||||
invocation_output,
|
||||
)
|
||||
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIType
|
||||
from invokeai.app.invocations.model import LoRAField, ModelIdentifierField, TransformerField
|
||||
from invokeai.app.invocations.model import CLIPField, LoRAField, ModelIdentifierField, TransformerField
|
||||
from invokeai.app.services.shared.invocation_context import InvocationContext
|
||||
from invokeai.backend.model_manager.config import BaseModelType
|
||||
|
||||
@@ -20,6 +20,7 @@ class FluxLoRALoaderOutput(BaseInvocationOutput):
|
||||
transformer: Optional[TransformerField] = OutputField(
|
||||
default=None, description=FieldDescriptions.transformer, title="FLUX Transformer"
|
||||
)
|
||||
clip: Optional[CLIPField] = OutputField(default=None, description=FieldDescriptions.clip, title="CLIP")
|
||||
|
||||
|
||||
@invocation(
|
||||
@@ -27,21 +28,28 @@ class FluxLoRALoaderOutput(BaseInvocationOutput):
|
||||
title="FLUX LoRA",
|
||||
tags=["lora", "model", "flux"],
|
||||
category="model",
|
||||
version="1.0.0",
|
||||
version="1.1.0",
|
||||
classification=Classification.Prototype,
|
||||
)
|
||||
class FluxLoRALoaderInvocation(BaseInvocation):
|
||||
"""Apply a LoRA model to a FLUX transformer."""
|
||||
"""Apply a LoRA model to a FLUX transformer and/or text encoder."""
|
||||
|
||||
lora: ModelIdentifierField = InputField(
|
||||
description=FieldDescriptions.lora_model, title="LoRA", ui_type=UIType.LoRAModel
|
||||
)
|
||||
weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
|
||||
transformer: TransformerField = InputField(
|
||||
transformer: TransformerField | None = InputField(
|
||||
default=None,
|
||||
description=FieldDescriptions.transformer,
|
||||
input=Input.Connection,
|
||||
title="FLUX Transformer",
|
||||
)
|
||||
clip: CLIPField | None = InputField(
|
||||
default=None,
|
||||
title="CLIP",
|
||||
description=FieldDescriptions.clip,
|
||||
input=Input.Connection,
|
||||
)
|
||||
|
||||
def invoke(self, context: InvocationContext) -> FluxLoRALoaderOutput:
|
||||
lora_key = self.lora.key
|
||||
@@ -49,18 +57,33 @@ class FluxLoRALoaderInvocation(BaseInvocation):
|
||||
if not context.models.exists(lora_key):
|
||||
raise ValueError(f"Unknown lora: {lora_key}!")
|
||||
|
||||
if any(lora.lora.key == lora_key for lora in self.transformer.loras):
|
||||
# Check for existing LoRAs with the same key.
|
||||
if self.transformer and any(lora.lora.key == lora_key for lora in self.transformer.loras):
|
||||
raise ValueError(f'LoRA "{lora_key}" already applied to transformer.')
|
||||
if self.clip and any(lora.lora.key == lora_key for lora in self.clip.loras):
|
||||
raise ValueError(f'LoRA "{lora_key}" already applied to CLIP encoder.')
|
||||
|
||||
transformer = self.transformer.model_copy(deep=True)
|
||||
transformer.loras.append(
|
||||
LoRAField(
|
||||
lora=self.lora,
|
||||
weight=self.weight,
|
||||
output = FluxLoRALoaderOutput()
|
||||
|
||||
# Attach LoRA layers to the models.
|
||||
if self.transformer is not None:
|
||||
output.transformer = self.transformer.model_copy(deep=True)
|
||||
output.transformer.loras.append(
|
||||
LoRAField(
|
||||
lora=self.lora,
|
||||
weight=self.weight,
|
||||
)
|
||||
)
|
||||
if self.clip is not None:
|
||||
output.clip = self.clip.model_copy(deep=True)
|
||||
output.clip.loras.append(
|
||||
LoRAField(
|
||||
lora=self.lora,
|
||||
weight=self.weight,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return FluxLoRALoaderOutput(transformer=transformer)
|
||||
return output
|
||||
|
||||
|
||||
@invocation(
|
||||
@@ -68,7 +91,7 @@ class FluxLoRALoaderInvocation(BaseInvocation):
|
||||
title="FLUX LoRA Collection Loader",
|
||||
tags=["lora", "model", "flux"],
|
||||
category="model",
|
||||
version="1.0.0",
|
||||
version="1.1.0",
|
||||
classification=Classification.Prototype,
|
||||
)
|
||||
class FLUXLoRACollectionLoader(BaseInvocation):
|
||||
@@ -84,6 +107,12 @@ class FLUXLoRACollectionLoader(BaseInvocation):
|
||||
input=Input.Connection,
|
||||
title="Transformer",
|
||||
)
|
||||
clip: CLIPField | None = InputField(
|
||||
default=None,
|
||||
title="CLIP",
|
||||
description=FieldDescriptions.clip,
|
||||
input=Input.Connection,
|
||||
)
|
||||
|
||||
def invoke(self, context: InvocationContext) -> FluxLoRALoaderOutput:
|
||||
output = FluxLoRALoaderOutput()
|
||||
@@ -106,4 +135,9 @@ class FLUXLoRACollectionLoader(BaseInvocation):
|
||||
output.transformer = self.transformer.model_copy(deep=True)
|
||||
output.transformer.loras.append(lora)
|
||||
|
||||
if self.clip is not None:
|
||||
if output.clip is None:
|
||||
output.clip = self.clip.model_copy(deep=True)
|
||||
output.clip.loras.append(lora)
|
||||
|
||||
return output
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Literal
|
||||
from contextlib import ExitStack
|
||||
from typing import Iterator, Literal, Tuple
|
||||
|
||||
import torch
|
||||
from transformers import CLIPTextModel, CLIPTokenizer, T5EncoderModel, T5Tokenizer
|
||||
@@ -9,6 +10,10 @@ from invokeai.app.invocations.model import CLIPField, T5EncoderField
|
||||
from invokeai.app.invocations.primitives import FluxConditioningOutput
|
||||
from invokeai.app.services.shared.invocation_context import InvocationContext
|
||||
from invokeai.backend.flux.modules.conditioner import HFEncoder
|
||||
from invokeai.backend.lora.conversions.flux_lora_constants import FLUX_LORA_CLIP_PREFIX
|
||||
from invokeai.backend.lora.lora_model_raw import LoRAModelRaw
|
||||
from invokeai.backend.lora.lora_patcher import LoRAPatcher
|
||||
from invokeai.backend.model_manager.config import ModelFormat
|
||||
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData, FLUXConditioningInfo
|
||||
|
||||
|
||||
@@ -17,7 +22,7 @@ from invokeai.backend.stable_diffusion.diffusion.conditioning_data import Condit
|
||||
title="FLUX Text Encoding",
|
||||
tags=["prompt", "conditioning", "flux"],
|
||||
category="conditioning",
|
||||
version="1.0.0",
|
||||
version="1.1.0",
|
||||
classification=Classification.Prototype,
|
||||
)
|
||||
class FluxTextEncoderInvocation(BaseInvocation):
|
||||
@@ -78,15 +83,42 @@ class FluxTextEncoderInvocation(BaseInvocation):
|
||||
prompt = [self.prompt]
|
||||
|
||||
with (
|
||||
clip_text_encoder_info as clip_text_encoder,
|
||||
clip_text_encoder_info.model_on_device() as (cached_weights, clip_text_encoder),
|
||||
clip_tokenizer_info as clip_tokenizer,
|
||||
ExitStack() as exit_stack,
|
||||
):
|
||||
assert isinstance(clip_text_encoder, CLIPTextModel)
|
||||
assert isinstance(clip_tokenizer, CLIPTokenizer)
|
||||
|
||||
clip_text_encoder_config = clip_text_encoder_info.config
|
||||
assert clip_text_encoder_config is not None
|
||||
|
||||
# Apply LoRA models to the CLIP encoder.
|
||||
# Note: We apply the LoRA after the transformer has been moved to its target device for faster patching.
|
||||
if clip_text_encoder_config.format in [ModelFormat.Diffusers]:
|
||||
# The model is non-quantized, so we can apply the LoRA weights directly into the model.
|
||||
exit_stack.enter_context(
|
||||
LoRAPatcher.apply_lora_patches(
|
||||
model=clip_text_encoder,
|
||||
patches=self._clip_lora_iterator(context),
|
||||
prefix=FLUX_LORA_CLIP_PREFIX,
|
||||
cached_weights=cached_weights,
|
||||
)
|
||||
)
|
||||
else:
|
||||
# There are currently no supported CLIP quantized models. Add support here if needed.
|
||||
raise ValueError(f"Unsupported model format: {clip_text_encoder_config.format}")
|
||||
|
||||
clip_encoder = HFEncoder(clip_text_encoder, clip_tokenizer, True, 77)
|
||||
|
||||
pooled_prompt_embeds = clip_encoder(prompt)
|
||||
|
||||
assert isinstance(pooled_prompt_embeds, torch.Tensor)
|
||||
return pooled_prompt_embeds
|
||||
|
||||
def _clip_lora_iterator(self, context: InvocationContext) -> Iterator[Tuple[LoRAModelRaw, float]]:
|
||||
for lora in self.clip.loras:
|
||||
lora_info = context.models.load(lora.lora)
|
||||
assert isinstance(lora_info.model, LoRAModelRaw)
|
||||
yield (lora_info.model, lora.weight)
|
||||
del lora_info
|
||||
|
||||
@@ -2,6 +2,7 @@ from typing import Dict
|
||||
|
||||
import torch
|
||||
|
||||
from invokeai.backend.lora.conversions.flux_lora_constants import FLUX_LORA_TRANSFORMER_PREFIX
|
||||
from invokeai.backend.lora.layers.any_lora_layer import AnyLoRALayer
|
||||
from invokeai.backend.lora.layers.concatenated_lora_layer import ConcatenatedLoRALayer
|
||||
from invokeai.backend.lora.layers.lora_layer import LoRALayer
|
||||
@@ -189,7 +190,9 @@ def lora_model_from_flux_diffusers_state_dict(state_dict: Dict[str, torch.Tensor
|
||||
# Assert that all keys were processed.
|
||||
assert len(grouped_state_dict) == 0
|
||||
|
||||
return LoRAModelRaw(layers=layers)
|
||||
layers_with_prefix = {f"{FLUX_LORA_TRANSFORMER_PREFIX}{k}": v for k, v in layers.items()}
|
||||
|
||||
return LoRAModelRaw(layers=layers_with_prefix)
|
||||
|
||||
|
||||
def _group_by_layer(state_dict: Dict[str, torch.Tensor]) -> dict[str, dict[str, torch.Tensor]]:
|
||||
|
||||
@@ -3,18 +3,25 @@ from typing import Any, Dict, TypeVar
|
||||
|
||||
import torch
|
||||
|
||||
from invokeai.backend.lora.conversions.flux_lora_constants import FLUX_LORA_CLIP_PREFIX, FLUX_LORA_TRANSFORMER_PREFIX
|
||||
from invokeai.backend.lora.layers.any_lora_layer import AnyLoRALayer
|
||||
from invokeai.backend.lora.layers.utils import any_lora_layer_from_state_dict
|
||||
from invokeai.backend.lora.lora_model_raw import LoRAModelRaw
|
||||
|
||||
# A regex pattern that matches all of the keys in the Kohya FLUX LoRA format.
|
||||
# A regex pattern that matches all of the transformer keys in the Kohya FLUX LoRA format.
|
||||
# Example keys:
|
||||
# lora_unet_double_blocks_0_img_attn_proj.alpha
|
||||
# lora_unet_double_blocks_0_img_attn_proj.lora_down.weight
|
||||
# lora_unet_double_blocks_0_img_attn_proj.lora_up.weight
|
||||
FLUX_KOHYA_KEY_REGEX = (
|
||||
FLUX_KOHYA_TRANSFORMER_KEY_REGEX = (
|
||||
r"lora_unet_(\w+_blocks)_(\d+)_(img_attn|img_mlp|img_mod|txt_attn|txt_mlp|txt_mod|linear1|linear2|modulation)_?(.*)"
|
||||
)
|
||||
# A regex pattern that matches all of the CLIP keys in the Kohya FLUX LoRA format.
|
||||
# Example keys:
|
||||
# lora_te1_text_model_encoder_layers_0_mlp_fc1.alpha
|
||||
# lora_te1_text_model_encoder_layers_0_mlp_fc1.lora_down.weight
|
||||
# lora_te1_text_model_encoder_layers_0_mlp_fc1.lora_up.weight
|
||||
FLUX_KOHYA_CLIP_KEY_REGEX = r"lora_te1_text_model_encoder_layers_(\d+)_(mlp|self_attn)_(\w+)\.?.*"
|
||||
|
||||
|
||||
def is_state_dict_likely_in_flux_kohya_format(state_dict: Dict[str, Any]) -> bool:
|
||||
@@ -23,7 +30,10 @@ def is_state_dict_likely_in_flux_kohya_format(state_dict: Dict[str, Any]) -> boo
|
||||
This is intended to be a high-precision detector, but it is not guaranteed to have perfect precision. (A
|
||||
perfect-precision detector would require checking all keys against a whitelist and verifying tensor shapes.)
|
||||
"""
|
||||
return all(re.match(FLUX_KOHYA_KEY_REGEX, k) for k in state_dict.keys())
|
||||
return all(
|
||||
re.match(FLUX_KOHYA_TRANSFORMER_KEY_REGEX, k) or re.match(FLUX_KOHYA_CLIP_KEY_REGEX, k)
|
||||
for k in state_dict.keys()
|
||||
)
|
||||
|
||||
|
||||
def lora_model_from_flux_kohya_state_dict(state_dict: Dict[str, torch.Tensor]) -> LoRAModelRaw:
|
||||
@@ -35,13 +45,27 @@ def lora_model_from_flux_kohya_state_dict(state_dict: Dict[str, torch.Tensor]) -
|
||||
grouped_state_dict[layer_name] = {}
|
||||
grouped_state_dict[layer_name][param_name] = value
|
||||
|
||||
# Convert the state dict to the InvokeAI format.
|
||||
grouped_state_dict = convert_flux_kohya_state_dict_to_invoke_format(grouped_state_dict)
|
||||
# Split the grouped state dict into transformer and CLIP state dicts.
|
||||
transformer_grouped_sd: dict[str, dict[str, torch.Tensor]] = {}
|
||||
clip_grouped_sd: dict[str, dict[str, torch.Tensor]] = {}
|
||||
for layer_name, layer_state_dict in grouped_state_dict.items():
|
||||
if layer_name.startswith("lora_unet"):
|
||||
transformer_grouped_sd[layer_name] = layer_state_dict
|
||||
elif layer_name.startswith("lora_te1"):
|
||||
clip_grouped_sd[layer_name] = layer_state_dict
|
||||
else:
|
||||
raise ValueError(f"Layer '{layer_name}' does not match the expected pattern for FLUX LoRA weights.")
|
||||
|
||||
# Convert the state dicts to the InvokeAI format.
|
||||
transformer_grouped_sd = _convert_flux_transformer_kohya_state_dict_to_invoke_format(transformer_grouped_sd)
|
||||
clip_grouped_sd = _convert_flux_clip_kohya_state_dict_to_invoke_format(clip_grouped_sd)
|
||||
|
||||
# Create LoRA layers.
|
||||
layers: dict[str, AnyLoRALayer] = {}
|
||||
for layer_key, layer_state_dict in grouped_state_dict.items():
|
||||
layers[layer_key] = any_lora_layer_from_state_dict(layer_state_dict)
|
||||
for layer_key, layer_state_dict in transformer_grouped_sd.items():
|
||||
layers[FLUX_LORA_TRANSFORMER_PREFIX + layer_key] = any_lora_layer_from_state_dict(layer_state_dict)
|
||||
for layer_key, layer_state_dict in clip_grouped_sd.items():
|
||||
layers[FLUX_LORA_CLIP_PREFIX + layer_key] = any_lora_layer_from_state_dict(layer_state_dict)
|
||||
|
||||
# Create and return the LoRAModelRaw.
|
||||
return LoRAModelRaw(layers=layers)
|
||||
@@ -50,16 +74,34 @@ def lora_model_from_flux_kohya_state_dict(state_dict: Dict[str, torch.Tensor]) -
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def convert_flux_kohya_state_dict_to_invoke_format(state_dict: Dict[str, T]) -> Dict[str, T]:
|
||||
"""Converts a state dict from the Kohya FLUX LoRA format to LoRA weight format used internally by InvokeAI.
|
||||
def _convert_flux_clip_kohya_state_dict_to_invoke_format(state_dict: Dict[str, T]) -> Dict[str, T]:
|
||||
"""Converts a CLIP LoRA state dict from the Kohya FLUX LoRA format to LoRA weight format used internally by
|
||||
InvokeAI.
|
||||
|
||||
Example key conversions:
|
||||
|
||||
"lora_te1_text_model_encoder_layers_0_mlp_fc1" -> "text_model.encoder.layers.0.mlp.fc1",
|
||||
"lora_te1_text_model_encoder_layers_0_self_attn_k_proj" -> "text_model.encoder.layers.0.self_attn.k_proj"
|
||||
"""
|
||||
converted_sd: dict[str, T] = {}
|
||||
for k, v in state_dict.items():
|
||||
match = re.match(FLUX_KOHYA_CLIP_KEY_REGEX, k)
|
||||
if match:
|
||||
new_key = f"text_model.encoder.layers.{match.group(1)}.{match.group(2)}.{match.group(3)}"
|
||||
converted_sd[new_key] = v
|
||||
else:
|
||||
raise ValueError(f"Key '{k}' does not match the expected pattern for FLUX LoRA weights.")
|
||||
|
||||
return converted_sd
|
||||
|
||||
|
||||
def _convert_flux_transformer_kohya_state_dict_to_invoke_format(state_dict: Dict[str, T]) -> Dict[str, T]:
|
||||
"""Converts a FLUX tranformer LoRA state dict from the Kohya FLUX LoRA format to LoRA weight format used internally
|
||||
by InvokeAI.
|
||||
|
||||
Example key conversions:
|
||||
"lora_unet_double_blocks_0_img_attn_proj" -> "double_blocks.0.img_attn.proj"
|
||||
"lora_unet_double_blocks_0_img_attn_proj" -> "double_blocks.0.img_attn.proj"
|
||||
"lora_unet_double_blocks_0_img_attn_proj" -> "double_blocks.0.img_attn.proj"
|
||||
"lora_unet_double_blocks_0_img_attn_qkv" -> "double_blocks.0.img_attn.qkv"
|
||||
"lora_unet_double_blocks_0_img_attn_qkv" -> "double_blocks.0.img.attn.qkv"
|
||||
"lora_unet_double_blocks_0_img_attn_qkv" -> "double_blocks.0.img.attn.qkv"
|
||||
"""
|
||||
|
||||
def replace_func(match: re.Match[str]) -> str:
|
||||
@@ -70,9 +112,9 @@ def convert_flux_kohya_state_dict_to_invoke_format(state_dict: Dict[str, T]) ->
|
||||
|
||||
converted_dict: dict[str, T] = {}
|
||||
for k, v in state_dict.items():
|
||||
match = re.match(FLUX_KOHYA_KEY_REGEX, k)
|
||||
match = re.match(FLUX_KOHYA_TRANSFORMER_KEY_REGEX, k)
|
||||
if match:
|
||||
new_key = re.sub(FLUX_KOHYA_KEY_REGEX, replace_func, k)
|
||||
new_key = re.sub(FLUX_KOHYA_TRANSFORMER_KEY_REGEX, replace_func, k)
|
||||
converted_dict[new_key] = v
|
||||
else:
|
||||
raise ValueError(f"Key '{k}' does not match the expected pattern for FLUX LoRA weights.")
|
||||
|
||||
3
invokeai/backend/lora/conversions/flux_lora_constants.py
Normal file
3
invokeai/backend/lora/conversions/flux_lora_constants.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# Prefixes used to distinguish between transformer and CLIP text encoder keys in the FLUX InvokeAI LoRA format.
|
||||
FLUX_LORA_TRANSFORMER_PREFIX = "lora_transformer-"
|
||||
FLUX_LORA_CLIP_PREFIX = "lora_clip-"
|
||||
@@ -157,6 +157,7 @@ class MainModelDefaultSettings(BaseModel):
|
||||
)
|
||||
width: int | None = Field(default=None, multiple_of=8, ge=64, description="Default width for this model")
|
||||
height: int | None = Field(default=None, multiple_of=8, ge=64, description="Default height for this model")
|
||||
guidance: float | None = Field(default=None, ge=1, description="Default Guidance for this model")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
@@ -83,7 +83,17 @@
|
||||
"tab": "Tabulator",
|
||||
"enabled": "Aktiviert",
|
||||
"disabled": "Ausgeschaltet",
|
||||
"dontShowMeThese": "Zeig mir diese nicht"
|
||||
"dontShowMeThese": "Zeig mir diese nicht",
|
||||
"apply": "Anwenden",
|
||||
"edit": "Ändern",
|
||||
"openInViewer": "Im Viewer öffnen",
|
||||
"loadingImage": "Lade Bild",
|
||||
"off": "Aus",
|
||||
"view": "Anzeigen",
|
||||
"placeholderSelectAModel": "Modell auswählen",
|
||||
"reset": "Zurücksetzen",
|
||||
"none": "Keine",
|
||||
"new": "Neu"
|
||||
},
|
||||
"gallery": {
|
||||
"galleryImageSize": "Bildgröße",
|
||||
@@ -117,16 +127,333 @@
|
||||
"alwaysShowImageSizeBadge": "Zeige immer Bilder Größe Abzeichen",
|
||||
"selectForCompare": "Zum Vergleichen auswählen",
|
||||
"compareImage": "Bilder vergleichen",
|
||||
"exitSearch": "Suche beenden",
|
||||
"exitSearch": "Bildsuche beenden",
|
||||
"newestFirst": "Neueste zuerst",
|
||||
"oldestFirst": "Älteste zuerst",
|
||||
"openInViewer": "Im Viewer öffnen",
|
||||
"swapImages": "Bilder tauschen"
|
||||
"swapImages": "Bilder tauschen",
|
||||
"slider": "Slider",
|
||||
"showStarredImagesFirst": "Mit * markierte Bilder zuerst zeigen",
|
||||
"compareHelp1": "Halten Sie <Kbd>Alt</Kbd> gedrückt, während Sie auf ein Galeriebild klicken oder die Pfeiltasten verwenden, um das Vergleichsbild zu ändern.",
|
||||
"compareHelp4": "Drücken Sie <Kbd>Z</Kbd> oder <Kbd>Esc</Kbd> zum Beenden.",
|
||||
"move": "Bewegen",
|
||||
"exitBoardSearch": "Suchen beenden",
|
||||
"searchImages": "Suche mit Metadaten",
|
||||
"selectAllOnPage": "Alle auf Seite auswählen",
|
||||
"showArchivedBoards": "Archivierte Boards anzeigen",
|
||||
"hover": "Schweben",
|
||||
"compareHelp2": "Drücken Sie <Kbd>M</Kbd>, um durch alle Vergleichsmodi zu wechseln.",
|
||||
"compareHelp3": "Drücken Sie <Kbd>C</Kbd>, um die verglichenen Bilder zu wechseln.",
|
||||
"gallery": "Galerie",
|
||||
"sortDirection": "Sortierreihenfolge",
|
||||
"sideBySide": "Nebeneinander",
|
||||
"openViewer": "Viewer öffnen",
|
||||
"viewerImage": "Viewer-Bild",
|
||||
"exitCompare": "Vergleichen beenden",
|
||||
"closeViewer": "Viewer schließen",
|
||||
"selectAnImageToCompare": "Wählen Sie ein Bild zum Vergleichen",
|
||||
"stretchToFit": "Strecken bis es passt",
|
||||
"displayBoardSearch": "Board durchsuchen",
|
||||
"displaySearch": "Bild suchen",
|
||||
"go": "Los",
|
||||
"jump": "Springen"
|
||||
},
|
||||
"hotkeys": {
|
||||
"noHotkeysFound": "Kein Hotkey gefunden",
|
||||
"searchHotkeys": "Hotkeys durchsuchen",
|
||||
"clearSearch": "Suche leeren"
|
||||
"clearSearch": "Suche leeren",
|
||||
"canvas": {
|
||||
"fitBboxToCanvas": {
|
||||
"desc": "Skalierung und Positionierung der Ansicht auf Bbox-Größe.",
|
||||
"title": "Bbox auf Arbeitsfläche skalieren"
|
||||
},
|
||||
"selectBboxTool": {
|
||||
"title": "Bbox Werkzeug",
|
||||
"desc": "Bbox Werkzeug auswählen."
|
||||
},
|
||||
"setFillToWhite": {
|
||||
"title": "Farbe auf Weiß einstellen",
|
||||
"desc": "Setzt die aktuelle Werkzeugfarbe auf weiß."
|
||||
},
|
||||
"title": "Leinwand",
|
||||
"selectBrushTool": {
|
||||
"title": "Pinselwerkzeug",
|
||||
"desc": "Wählen Sie das Pinselwerkzeug aus."
|
||||
},
|
||||
"decrementToolWidth": {
|
||||
"title": "Werkzeugbreite verringern",
|
||||
"desc": "Verringern Sie die Breite des Pinsels oder Radiergummis, je nachdem, welches ausgewählt ist."
|
||||
},
|
||||
"incrementToolWidth": {
|
||||
"title": "Werkzeugbreite erhöhen",
|
||||
"desc": "Vergrößern Sie die Breite des Pinsels oder Radiergummis, je nachdem, welches ausgewählt ist."
|
||||
},
|
||||
"selectColorPickerTool": {
|
||||
"title": "Farbwähler-Werkzeug",
|
||||
"desc": "Farbwähler-Werkzeug auswählen."
|
||||
},
|
||||
"selectEraserTool": {
|
||||
"title": "Radiergummi-Werkzeug",
|
||||
"desc": "Radiergummi-Werkzeug auswählen."
|
||||
},
|
||||
"fitLayersToCanvas": {
|
||||
"title": "Ebenen an die Leinwand anpassen",
|
||||
"desc": "Alle sichtbaren Ebenen in der Ansicht einpassen."
|
||||
},
|
||||
"filterSelected": {
|
||||
"title": "Filter",
|
||||
"desc": "Gewählte Ebene filtern. Nur bei \"Raster\" und Kontroll-Ebenen."
|
||||
},
|
||||
"transformSelected": {
|
||||
"title": "Umwandeln",
|
||||
"desc": "Transformieren Sie die ausgewählte Ebene."
|
||||
},
|
||||
"setZoomTo100Percent": {
|
||||
"title": "Auf 100 % zoomen",
|
||||
"desc": "Leinwand-Zoom auf 100 % setzen."
|
||||
},
|
||||
"setZoomTo200Percent": {
|
||||
"title": "Auf 200 % zoomen",
|
||||
"desc": "Leinwand-Zoom auf 200 % setzen."
|
||||
},
|
||||
"setZoomTo400Percent": {
|
||||
"title": "Auf 400 % zoomen",
|
||||
"desc": "Leinwand-Zoom auf 400 % setzen."
|
||||
},
|
||||
"setZoomTo800Percent": {
|
||||
"title": "Auf 800 % zoomen",
|
||||
"desc": "Leinwand-Zoom auf 800 % setzen."
|
||||
},
|
||||
"deleteSelected": {
|
||||
"title": "Ebene löschen",
|
||||
"desc": "Ausgewählte Ebene löschen."
|
||||
},
|
||||
"undo": {
|
||||
"title": "Rückgängig",
|
||||
"desc": "Letzte Aktion rückgängig machen."
|
||||
},
|
||||
"redo": {
|
||||
"title": "Wiederholen",
|
||||
"desc": "Letzte Aktion wiederholen."
|
||||
},
|
||||
"nextEntity": {
|
||||
"title": "Nächste Ebene",
|
||||
"desc": "Nächste Ebene in der Liste auswählen."
|
||||
},
|
||||
"resetSelected": {
|
||||
"title": "Ebene zurücksetzen",
|
||||
"desc": "Ausgewählte Ebene zurücksetzen. Gilt nur für Malmaske bei \"Inpaint\" und \"Regionaler Führung\"."
|
||||
},
|
||||
"prevEntity": {
|
||||
"title": "Vorherige Ebene",
|
||||
"desc": "Vorherige Ebene in der Liste auswählen."
|
||||
},
|
||||
"selectMoveTool": {
|
||||
"title": "Verschieben-Werkzeug",
|
||||
"desc": "Verschieben-Werkzeug auswählen."
|
||||
},
|
||||
"selectRectTool": {
|
||||
"title": "Rechteck-Werkzeug",
|
||||
"desc": "Rechteck-Werkzeug auswählen."
|
||||
},
|
||||
"selectViewTool": {
|
||||
"desc": "Wählen Sie das Ansichts-Tool.",
|
||||
"title": "Ansichts-Tool"
|
||||
},
|
||||
"quickSwitch": {
|
||||
"title": "Ebenen schnell umschalten",
|
||||
"desc": "Wechseln Sie zwischen den beiden zuletzt gewählten Ebenen. Wenn eine Ebene mit einem Lesezeichen versehen ist, wird zwischen ihr und der letzten nicht markierten Ebene gewechselt."
|
||||
}
|
||||
},
|
||||
"viewer": {
|
||||
"useSize": {
|
||||
"desc": "Aktuelle Bildgröße als Bbox-Größe verwenden.",
|
||||
"title": "Maße übernehmen"
|
||||
},
|
||||
"title": "Bildbetrachter",
|
||||
"toggleViewer": {
|
||||
"title": "Bildbetrachter anzeigen/ausblenden",
|
||||
"desc": "Zeigen oder verbergen Sie den Bildbetrachter. Nur auf der Arbeitsflächen-Registerkarte."
|
||||
},
|
||||
"nextComparisonMode": {
|
||||
"title": "Nächster Vergleichsmodus",
|
||||
"desc": "Alle Vergleichsmodi durchlaufen."
|
||||
},
|
||||
"swapImages": {
|
||||
"title": "Vergleichsbilder tauschen",
|
||||
"desc": "Vergleichs-Bilder tauschen."
|
||||
},
|
||||
"runPostprocessing": {
|
||||
"title": "Nachbearbeitung ausführen",
|
||||
"desc": "Ausgewählte Nachbearbeitung/en auf aktuelles Bild anwenden."
|
||||
},
|
||||
"toggleMetadata": {
|
||||
"title": "Metadaten anzeigen/ausblenden",
|
||||
"desc": "Zeigen oder verbergen der Metadaten des Bildes."
|
||||
},
|
||||
"recallPrompts": {
|
||||
"title": "Prompts abrufen",
|
||||
"desc": "Rufen Sie die positiven und negativen Prompts für das aktuelle Bild ab."
|
||||
},
|
||||
"recallSeed": {
|
||||
"desc": "Seed für aktuelles Bild abrufen.",
|
||||
"title": "Seed abrufen"
|
||||
},
|
||||
"loadWorkflow": {
|
||||
"title": "Lade Arbeitsablauf/Workflow",
|
||||
"desc": "Laden Sie den gespeicherten Workflow des aktuellen Bildes (falls es einen hat)."
|
||||
},
|
||||
"recallAll": {
|
||||
"title": "Alle Metadaten abrufen",
|
||||
"desc": "Alle Metadaten für das aktuelle Bild abrufen."
|
||||
},
|
||||
"remix": {
|
||||
"desc": "Rufen Sie alle Metadaten außer dem Seed für das aktuelle Bild ab.",
|
||||
"title": "Remixen"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"invoke": {
|
||||
"title": "Invoke",
|
||||
"desc": "Stellt eine Generierung in die Warteschlange und fügt sie am Ende hinzu."
|
||||
},
|
||||
"invokeFront": {
|
||||
"title": "Invoke (Front)",
|
||||
"desc": "Stellt eine Generierung in die Warteschlange und fügt sie am Anfang hinzu."
|
||||
},
|
||||
"cancelQueueItem": {
|
||||
"title": "Abbrechen",
|
||||
"desc": "Aktuelles Warteschlangenelement abbrechen."
|
||||
},
|
||||
"clearQueue": {
|
||||
"title": "Warteschlange löschen",
|
||||
"desc": "Warteschlange abbrechen und komplett löschen."
|
||||
},
|
||||
"selectUpscalingTab": {
|
||||
"title": "Wählen Sie die Registerkarte Hochskalieren",
|
||||
"desc": "Wählt die Registerkarte Hochskalieren."
|
||||
},
|
||||
"selectCanvasTab": {
|
||||
"desc": "Wählt die Arbeitsflächen-Registerkarte.",
|
||||
"title": "Wählen Sie die Arbeitsflächen-Registerkarte"
|
||||
},
|
||||
"selectWorkflowsTab": {
|
||||
"title": "Wählt die Registerkarte Arbeitsabläufe",
|
||||
"desc": "Wählt die Registerkarte Arbeitsabläufe."
|
||||
},
|
||||
"selectModelsTab": {
|
||||
"title": "Wählt die Registerkarte Modelle",
|
||||
"desc": "Wählt die Registerkarte Modelle."
|
||||
},
|
||||
"selectQueueTab": {
|
||||
"title": "Wählt die Registerkarte Warteschlange",
|
||||
"desc": "Wählt die Registerkarte Warteschlange."
|
||||
},
|
||||
"focusPrompt": {
|
||||
"desc": "Bewegt den Cursor-Fokus auf den positiven Prompt.",
|
||||
"title": "Fokus-Prompt"
|
||||
},
|
||||
"toggleLeftPanel": {
|
||||
"title": "Linkes Panel ein-/ausblenden",
|
||||
"desc": "Linke Seite zeigen/verbergen."
|
||||
},
|
||||
"toggleRightPanel": {
|
||||
"title": "Rechte Seite umschalten",
|
||||
"desc": "Rechte Seite zeigen/verbergen."
|
||||
},
|
||||
"resetPanelLayout": {
|
||||
"title": "Layout zurücksetzen",
|
||||
"desc": "Beide Seiten auf Standard zurücksetzen."
|
||||
},
|
||||
"title": "Anwendung",
|
||||
"togglePanels": {
|
||||
"title": "Seiten umschalten",
|
||||
"desc": "Zeigen oder verbergen Sie beide Panels auf einmal."
|
||||
}
|
||||
},
|
||||
"hotkeys": "Tastaturbefehle",
|
||||
"gallery": {
|
||||
"title": "Galerie",
|
||||
"selectAllOnPage": {
|
||||
"title": "Alle auf der Seite auswählen",
|
||||
"desc": "Alle Bilder auf der aktuellen Seite auswählen."
|
||||
},
|
||||
"galleryNavRight": {
|
||||
"title": "Nach rechts navigieren",
|
||||
"desc": "Navigieren Sie im Galerieraster nach rechts, und wählen Sie das Bild aus. Wenn es sich um das letzte Bild in der Reihe handelt, gehen Sie zur nächsten Reihe. Wenn Sie sich beim letzten Bild der Seite befinden, gehen Sie zur nächsten Seite."
|
||||
},
|
||||
"galleryNavDownAlt": {
|
||||
"title": "Nach unten navigieren (Bild vergleichen)",
|
||||
"desc": "Wie \"Abwärts navigieren\", wählt aber das Vergleichsbild aus und öffnet den Vergleichsmodus, falls er nicht bereits geöffnet ist."
|
||||
},
|
||||
"galleryNavUp": {
|
||||
"title": "Nach oben navigieren",
|
||||
"desc": "Navigieren Sie im Galerieraster nach oben, und wählen Sie das Bild aus. Wenn Sie sich oben auf der Seite befinden, gehen Sie zur vorherigen Seite."
|
||||
},
|
||||
"galleryNavDown": {
|
||||
"title": "Nach unten navigieren",
|
||||
"desc": "Navigieren Sie im Galerieraster nach unten, und wählen Sie das Bild aus. Wenn Sie sich am Ende der Seite befinden, gehen Sie zur nächsten Seite."
|
||||
},
|
||||
"galleryNavLeft": {
|
||||
"title": "Nach links navigieren",
|
||||
"desc": "Navigieren Sie im Galerieraster nach links, und wählen Sie das Bild aus. Wenn Sie sich im ersten Bild der Reihe befinden, gehen Sie zur vorherigen Reihe. Wenn Sie sich beim ersten Bild der Seite befinden, gehen Sie zur vorherigen Seite."
|
||||
},
|
||||
"galleryNavUpAlt": {
|
||||
"title": "Nach oben navigieren (Bild vergleichen)",
|
||||
"desc": "Wie „Nach oben navigieren“, wählt aber das Vergleichsbild aus und öffnet den Vergleichsmodus, falls er nicht bereits geöffnet ist."
|
||||
},
|
||||
"galleryNavRightAlt": {
|
||||
"title": "Nach rechts navigieren (Bild vergleichen)",
|
||||
"desc": "Wie \"Navigieren nach rechts\", wählt aber das Vergleichsbild aus und öffnet den Vergleichsmodus, falls er nicht bereits geöffnet ist."
|
||||
},
|
||||
"clearSelection": {
|
||||
"title": "Auswahl aufheben",
|
||||
"desc": "Aktuelle Auswahl aufheben, falls vorhanden."
|
||||
},
|
||||
"galleryNavLeftAlt": {
|
||||
"title": "Nach links navigieren (Bild vergleichen)",
|
||||
"desc": "Wie „Nach links navigieren“, wählt aber das Vergleichsbild aus und öffnet den Vergleichsmodus, falls er nicht bereits geöffnet ist."
|
||||
},
|
||||
"deleteSelection": {
|
||||
"title": "Löschen",
|
||||
"desc": "Alle ausgewählten Bilder löschen. Standardmäßig werden Sie aufgefordert, den Löschvorgang zu bestätigen. Wenn die Bilder derzeit in der App verwendet werden, werden Sie gewarnt."
|
||||
}
|
||||
},
|
||||
"workflows": {
|
||||
"redo": {
|
||||
"title": "Wiederholen",
|
||||
"desc": "Letzte Workflow-Aktion wiederherstellen."
|
||||
},
|
||||
"copySelection": {
|
||||
"title": "Kopieren",
|
||||
"desc": "Ausgewählte Knoten und Kanten kopieren."
|
||||
},
|
||||
"title": "Arbeitsabläufe",
|
||||
"addNode": {
|
||||
"title": "Knoten hinzufügen",
|
||||
"desc": "Öffnen Sie das \"Knoten zufügen\"-Menü."
|
||||
},
|
||||
"pasteSelection": {
|
||||
"title": "Einfügen",
|
||||
"desc": "Kopierte Knoten und Kanten einfügen."
|
||||
},
|
||||
"selectAll": {
|
||||
"title": "Alles auswählen",
|
||||
"desc": "Alle Knoten und Kanten auswählen."
|
||||
},
|
||||
"deleteSelection": {
|
||||
"title": "Löschen",
|
||||
"desc": "Lösche ausgewählte Knoten und Kanten."
|
||||
},
|
||||
"undo": {
|
||||
"title": "Rückgängig",
|
||||
"desc": "Letzte Workflow-Aktion rückgängig machen."
|
||||
},
|
||||
"pasteSelectionWithEdges": {
|
||||
"desc": "Kopierte Knoten, Kanten und alle mit den kopierten Knoten verbundenen Kanten einfügen.",
|
||||
"title": "Einfügen mit Kanten"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelManager": {
|
||||
"modelUpdated": "Model aktualisiert",
|
||||
@@ -164,7 +491,7 @@
|
||||
"baseModel": "Basis Modell",
|
||||
"convertToDiffusers": "Konvertiere zu Diffusers",
|
||||
"vae": "VAE",
|
||||
"predictionType": "Vorhersagetyp (für Stable Diffusion 2.x-Modelle und gelegentliche Stable Diffusion 1.x-Modelle)",
|
||||
"predictionType": "Vorhersagetyp",
|
||||
"selectModel": "Wählen Sie Modell aus",
|
||||
"repo_id": "Repo-ID",
|
||||
"modelDeleted": "Modell gelöscht",
|
||||
@@ -187,7 +514,52 @@
|
||||
"deleteModelImage": "Lösche Model Bild",
|
||||
"huggingFaceRepoID": "HuggingFace Repo ID",
|
||||
"hfToken": "HuggingFace Schlüssel",
|
||||
"huggingFacePlaceholder": "besitzer/model-name"
|
||||
"huggingFacePlaceholder": "besitzer/model-name",
|
||||
"modelSettings": "Modelleinstellungen",
|
||||
"typePhraseHere": "Phrase hier eingeben",
|
||||
"spandrelImageToImage": "Bild zu Bild (Spandrel)",
|
||||
"starterModels": "Einstiegsmodelle",
|
||||
"t5Encoder": "T5-Kodierer",
|
||||
"useDefaultSettings": "Standardeinstellungen verwenden",
|
||||
"uploadImage": "Bild hochladen",
|
||||
"urlOrLocalPath": "URL oder lokaler Pfad",
|
||||
"install": "Installieren",
|
||||
"textualInversions": "Textuelle Inversionen",
|
||||
"ipAdapters": "IP-Adapter",
|
||||
"modelImageUpdated": "Modellbild aktualisiert",
|
||||
"path": "Pfad",
|
||||
"pathToConfig": "Pfad zur Konfiguration",
|
||||
"scanPlaceholder": "Pfad zu einem lokalen Ordner",
|
||||
"noMatchingModels": "Keine passenden Modelle",
|
||||
"localOnly": "nur lokal",
|
||||
"installAll": "Alles installieren",
|
||||
"main": "Haupt",
|
||||
"metadata": "Metadaten",
|
||||
"modelImageDeleted": "Modellbild gelöscht",
|
||||
"modelName": "Modellname",
|
||||
"noModelsInstalled": "Keine Modelle installiert",
|
||||
"source": "Quelle",
|
||||
"simpleModelPlaceholder": "URL oder Pfad zu einem lokalen Datei- oder Diffusers-Ordner",
|
||||
"imageEncoderModelId": "Bild Encoder Modell ID",
|
||||
"installRepo": "Repo installieren",
|
||||
"huggingFaceHelper": "Wenn mehrere Modelle in diesem Repo gefunden werden, werden Sie aufgefordert, eines für die Installation auszuwählen.",
|
||||
"inplaceInstall": "In-place-Installation",
|
||||
"modelImageDeleteFailed": "Modellbild konnte nicht gelöscht werden",
|
||||
"repoVariant": "Repo Variante",
|
||||
"learnMoreAboutSupportedModels": "Erfahren Sie mehr über die Modelle, die wir unterstützen",
|
||||
"clipEmbed": "CLIP einbetten",
|
||||
"starterModelsInModelManager": "Modelle für Ihren Start finden Sie im Modell-Manager",
|
||||
"noModelsInstalledDesc1": "Installiere Modelle mit dem",
|
||||
"modelImageUpdateFailed": "Modellbild-Update fehlgeschlagen",
|
||||
"prune": "Bereinigen",
|
||||
"loraModels": "LoRAs",
|
||||
"scanFolder": "Ordner scannen",
|
||||
"installQueue": "Installations-Warteschlange",
|
||||
"pruneTooltip": "Abgeschlossene Importe aus Warteschlange entfernen",
|
||||
"scanResults": "Ergebnisse des Scans",
|
||||
"urlOrLocalPathHelper": "URLs sollten auf eine einzelne Datei deuten. Lokale Pfade können zusätzlich auch auf einen Ordner für ein einzelnes Diffusers-Modell hinweisen.",
|
||||
"inplaceInstallDesc": "Installieren Sie Modelle, ohne die Dateien zu kopieren. Wenn Sie das Modell verwenden, wird es direkt von seinem Speicherort geladen. Wenn deaktiviert, werden die Dateien während der Installation in das von Invoke verwaltete Modellverzeichnis kopiert.",
|
||||
"scanFolderHelper": "Der Ordner wird rekursiv nach Modellen durchsucht. Dies kann bei sehr großen Ordnern etwas dauern."
|
||||
},
|
||||
"parameters": {
|
||||
"images": "Bilder",
|
||||
@@ -226,7 +598,19 @@
|
||||
"setToOptimalSize": "Optimiere Größe für Modell",
|
||||
"useSize": "Maße übernehmen",
|
||||
"remixImage": "Remix des Bilds erstellen",
|
||||
"imageActions": "Weitere Bildaktionen"
|
||||
"imageActions": "Weitere Bildaktionen",
|
||||
"invoke": {
|
||||
"layer": {
|
||||
"t2iAdapterIncompatibleBboxWidth": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, Bbox-Breite ist {{width}}",
|
||||
"t2iAdapterIncompatibleScaledBboxWidth": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, Skalierte Bbox-Breite ist {{width}}",
|
||||
"t2iAdapterIncompatibleScaledBboxHeight": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, Skalierte Bbox-Höhe ist {{height}}",
|
||||
"t2iAdapterIncompatibleBboxHeight": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, Bbox-Höhe ist {{height}}"
|
||||
},
|
||||
"fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), Skalierte Bbox-Breite ist {{width}}",
|
||||
"fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), Skalierte Bbox-Höhe ist {{height}}",
|
||||
"fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), Bbox-Breite ist {{width}}",
|
||||
"fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), Bbox-Höhe ist {{height}}"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"displayInProgress": "Zwischenbilder anzeigen",
|
||||
@@ -300,7 +684,6 @@
|
||||
"deleteBoardOnly": "Nur Ordner löschen",
|
||||
"deleteBoard": "Lösche Ordner",
|
||||
"deleteBoardAndImages": "Lösche Ordner und Bilder",
|
||||
"deletedBoardsCannotbeRestored": "Gelöschte Ordner können nicht wiederhergestellt werden",
|
||||
"movingImagesToBoard_one": "Verschiebe {{count}} Bild in Ordner:",
|
||||
"movingImagesToBoard_other": "Verschiebe {{count}} Bilder in Ordner:",
|
||||
"selectedForAutoAdd": "Ausgewählt für Automatisches hinzufügen",
|
||||
@@ -317,7 +700,9 @@
|
||||
"noBoards": "Kein {boardType}} Ordner",
|
||||
"hideBoards": "Ordner verstecken",
|
||||
"viewBoards": "Ordner ansehen",
|
||||
"deletedPrivateBoardsCannotbeRestored": "Gelöschte Boards können nicht wiederhergestellt werden. Wenn Sie „Nur Board löschen“ wählen, werden die Bilder in einen privaten, nicht kategorisierten Status für den Ersteller des Bildes versetzt."
|
||||
"deletedPrivateBoardsCannotbeRestored": "Gelöschte Boards können nicht wiederhergestellt werden. Wenn Sie „Nur Board löschen“ wählen, werden die Bilder in einen privaten, nicht kategorisierten Status für den Ersteller des Bildes versetzt.",
|
||||
"assetsWithCount_one": "{{count}} in der Sammlung",
|
||||
"assetsWithCount_other": "{{count}} in der Sammlung"
|
||||
},
|
||||
"queue": {
|
||||
"status": "Status",
|
||||
@@ -377,7 +762,19 @@
|
||||
"graphQueued": "Graph eingereiht",
|
||||
"graphFailedToQueue": "Fehler beim Einreihen des Graphen",
|
||||
"generations_one": "Generation",
|
||||
"generations_other": "Generationen"
|
||||
"generations_other": "Generationen",
|
||||
"iterations_one": "Iteration",
|
||||
"iterations_other": "Iterationen",
|
||||
"gallery": "Galerie",
|
||||
"generation": "Erstellung",
|
||||
"workflows": "Arbeitsabläufe",
|
||||
"other": "Sonstige",
|
||||
"origin": "Ursprung",
|
||||
"destination": "Ziel",
|
||||
"upscaling": "Hochskalierung",
|
||||
"canvas": "Leinwand",
|
||||
"prompts_one": "Prompt",
|
||||
"prompts_other": "Prompts"
|
||||
},
|
||||
"metadata": {
|
||||
"negativePrompt": "Negativ Beschreibung",
|
||||
@@ -407,7 +804,8 @@
|
||||
"imageDimensions": "Bilder Auslösungen",
|
||||
"parameterSet": "Parameter {{parameter}} setzen",
|
||||
"recallParameter": "{{label}} Abrufen",
|
||||
"parsingFailed": "Parsing Fehlgeschlagen"
|
||||
"parsingFailed": "Parsing Fehlgeschlagen",
|
||||
"canvasV2Metadata": "Leinwand"
|
||||
},
|
||||
"popovers": {
|
||||
"noiseUseCPU": {
|
||||
@@ -555,6 +953,9 @@
|
||||
"paragraphs": [
|
||||
"Die Skalierung steuert die Größe des Ausgabebildes und basiert auf einem Vielfachen der Auflösung des Originalbildes. So würde z. B. eine 2-fache Hochskalierung eines 1024x1024px Bildes eine 2048x2048px große Ausgabe erzeugen."
|
||||
]
|
||||
},
|
||||
"ipAdapterMethod": {
|
||||
"heading": "Methode"
|
||||
}
|
||||
},
|
||||
"invocationCache": {
|
||||
@@ -588,7 +989,7 @@
|
||||
"cannotConnectToSelf": "Es kann keine Verbindung zu sich selbst hergestellt werden",
|
||||
"colorCodeEdges": "Farbkodierte Kanten",
|
||||
"addNodeToolTip": "Knoten hinzufügen (Umschalt+A, Leertaste)",
|
||||
"collectionFieldType": "{{name}} Sammlung",
|
||||
"collectionFieldType": "{{name}} (Sammlung)",
|
||||
"connectionWouldCreateCycle": "Verbindung würde einen Kreislauf/cycle schaffen",
|
||||
"inputMayOnlyHaveOneConnection": "Eingang darf nur eine Verbindung haben",
|
||||
"hideLegendNodes": "Feldtyp-Legende ausblenden",
|
||||
@@ -652,7 +1053,13 @@
|
||||
"enum": "Aufzählung",
|
||||
"fullyContainNodes": "Vollständig ausgewählte Nodes auswählen",
|
||||
"editMode": "Im Workflow-Editor bearbeiten",
|
||||
"resetToDefaultValue": "Auf Standardwert zurücksetzen"
|
||||
"resetToDefaultValue": "Auf Standardwert zurücksetzen",
|
||||
"singleFieldType": "{{name}} (Einzeln)",
|
||||
"collectionOrScalarFieldType": "{{name}} (Einzeln oder Sammlung)",
|
||||
"missingFieldTemplate": "Fehlende Feldvorlage",
|
||||
"missingNode": "Fehlender Aufrufknoten",
|
||||
"missingInvocationTemplate": "Fehlende Aufrufvorlage",
|
||||
"edit": "Bearbeiten"
|
||||
},
|
||||
"hrf": {
|
||||
"enableHrf": "Korrektur für hohe Auflösungen",
|
||||
@@ -674,7 +1081,8 @@
|
||||
"noLoRAsInstalled": "Keine LoRAs installiert",
|
||||
"addLora": "LoRA hinzufügen",
|
||||
"defaultVAE": "Standard VAE",
|
||||
"lora": "LoRA"
|
||||
"lora": "LoRA",
|
||||
"concepts": "Konzepte"
|
||||
},
|
||||
"accordions": {
|
||||
"generation": {
|
||||
@@ -736,7 +1144,117 @@
|
||||
},
|
||||
"ui": {
|
||||
"tabs": {
|
||||
"queue": "Warteschlange"
|
||||
"queue": "Warteschlange",
|
||||
"generation": "Erzeugung",
|
||||
"gallery": "Galerie",
|
||||
"models": "Modelle",
|
||||
"upscaling": "Hochskalierung",
|
||||
"workflows": "Arbeitsabläufe",
|
||||
"canvas": "Leinwand"
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"logNamespaces": {
|
||||
"logNamespaces": "Namespaces loggen",
|
||||
"models": "Modelle",
|
||||
"gallery": "Galerie",
|
||||
"events": "Ereignisse",
|
||||
"queue": "Warteschlange",
|
||||
"system": "System",
|
||||
"workflows": "Arbeitsabläufe",
|
||||
"generation": "Erstellung",
|
||||
"metadata": "Metadaten",
|
||||
"config": "Konfiguration",
|
||||
"canvas": "Leinwand"
|
||||
},
|
||||
"logLevel": {
|
||||
"fatal": "Fatal",
|
||||
"trace": "Trace",
|
||||
"logLevel": "Protokollierungsstufe",
|
||||
"error": "Fehler",
|
||||
"info": "Infos",
|
||||
"warn": "Warnung",
|
||||
"debug": "Fehlerdiagnose"
|
||||
},
|
||||
"enableLogging": "Protokollierung aktivieren"
|
||||
},
|
||||
"whatsNew": {
|
||||
"whatsNewInInvoke": "Was gibt's Neues",
|
||||
"canvasV2Announcement": {
|
||||
"fluxSupport": "Unterstützung für Flux-Modelle",
|
||||
"newCanvas": "Eine leistungsstarke neue Kontrollfläche",
|
||||
"newLayerTypes": "Neue Ebenentypen für noch mehr Kontrolle",
|
||||
"readReleaseNotes": "Anmerkungen zu dieser Version lesen",
|
||||
"watchReleaseVideo": "Video über diese Version anzeigen",
|
||||
"watchUiUpdatesOverview": "Interface-Updates Übersicht"
|
||||
}
|
||||
},
|
||||
"stylePresets": {
|
||||
"name": "Name",
|
||||
"acceptedColumnsKeys": "Akzeptierte Spalten/Schlüssel:",
|
||||
"noTemplates": "Keine Vorlagen",
|
||||
"promptTemplatesDesc2": "Verwenden Sie die Platzhalterzeichenfolge<Pre>{{placeholder}}</Pre>, um anzugeben, wo Ihre Eingabeaufforderung in die Vorlage aufgenommen werden soll.",
|
||||
"noMatchingTemplates": "Keine passenden Vorlagen",
|
||||
"myTemplates": "Meine Vorlagen",
|
||||
"toggleViewMode": "Ansicht umschalten",
|
||||
"viewModeTooltip": "So sieht Ihr Prompt mit der aktuell ausgewählten Vorlage aus. Um Ihren Prompt zu bearbeiten, klicken Sie irgendwo in das Textfeld.",
|
||||
"templateDeleted": "Promptvorlage gelöscht",
|
||||
"unableToDeleteTemplate": "Promptvorlage kann nicht gelöscht werden",
|
||||
"insertPlaceholder": "Platzhalter einfügen",
|
||||
"type": "Typ",
|
||||
"uploadImage": "Bild hochladen",
|
||||
"updatePromptTemplate": "Promptvorlage aktualisieren",
|
||||
"exportFailed": "CSV kann nicht generiert und heruntergeladen werden",
|
||||
"viewList": "Vorlagenliste anzeigen",
|
||||
"useForTemplate": "Für Promptvorlage nutzen",
|
||||
"shared": "Geteilt",
|
||||
"private": "Privat",
|
||||
"promptTemplatesDesc1": "Promptvorlagen fügen den Prompts, die Sie in das Prompt-Feld schreiben, Text hinzu.",
|
||||
"negativePrompt": "Negativ-Prompt",
|
||||
"positivePromptColumn": "'prompt' oder 'positive_prompt'",
|
||||
"promptTemplatesDesc3": "Wenn Sie den Platzhalter weglassen, wird die Vorlage an das Ende Ihres Prompts angehängt.",
|
||||
"sharedTemplates": "Geteilte Vorlagen",
|
||||
"importTemplates": "Promptvorlagen importieren (CSV/JSON)",
|
||||
"flatten": "Ausgewählte Vorlage in aktuelle Eingabeaufforderung einblenden",
|
||||
"searchByName": "Nach Name suchen",
|
||||
"promptTemplateCleared": "Promptvorlage gelöscht",
|
||||
"preview": "Vorschau",
|
||||
"positivePrompt": "Positiv-Prompt"
|
||||
},
|
||||
"newUserExperience": {
|
||||
"gettingStartedSeries": "Wünschen Sie weitere Anleitungen? In unserer <LinkComponent>Einführungsserie</LinkComponent> finden Sie Tipps, wie Sie das Potenzial von Invoke Studio voll ausschöpfen können.",
|
||||
"toGetStarted": "Um zu beginnen, geben Sie einen Prompt in das Feld ein und klicken Sie auf <StrongComponent>Invoke</StrongComponent>, um Ihr erstes Bild zu erzeugen. Sie können Ihre Bilder direkt in der <StrongComponent>Galerie</StrongComponent> speichern oder sie auf der <StrongComponent>Leinwand</StrongComponent> bearbeiten."
|
||||
},
|
||||
"controlLayers": {
|
||||
"pullBboxIntoLayerOk": "Bbox in die Ebene gezogen",
|
||||
"saveBboxToGallery": "Bbox in Galerie speichern",
|
||||
"tool": {
|
||||
"bbox": "Bbox"
|
||||
},
|
||||
"transform": {
|
||||
"fitToBbox": "An Bbox anpassen"
|
||||
},
|
||||
"pullBboxIntoLayerError": "Problem, Bbox in die Ebene zu ziehen",
|
||||
"pullBboxIntoLayer": "Bbox in Ebene ziehen",
|
||||
"HUD": {
|
||||
"bbox": "Bbox",
|
||||
"scaledBbox": "Skalierte Bbox"
|
||||
},
|
||||
"fitBboxToLayers": "Bbox an Ebenen anpassen",
|
||||
"pullBboxIntoReferenceImage": "Bbox ins Referenzbild ziehen",
|
||||
"pullBboxIntoReferenceImageOk": "Bbox in Referenzbild gezogen",
|
||||
"pullBboxIntoReferenceImageError": "Problem, Bbox ins Referenzbild zu ziehen",
|
||||
"bboxOverlay": "Bbox Overlay anzeigen",
|
||||
"clipToBbox": "Pinselstriche auf Bbox beschränken",
|
||||
"canvasContextMenu": {
|
||||
"saveBboxToGallery": "Bbox in Galerie speichern",
|
||||
"bboxGroup": "Aus Bbox erstellen"
|
||||
}
|
||||
},
|
||||
"upsell": {
|
||||
"shareAccess": "Zugang teilen",
|
||||
"professional": "Professionell",
|
||||
"inviteTeammates": "Teamkollegen einladen",
|
||||
"professionalUpsell": "Verfügbar in der Professional Edition von Invoke. Klicken Sie hier oder besuchen Sie invoke.com/pricing für weitere Details."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,6 +503,22 @@
|
||||
"transformSelected": {
|
||||
"title": "Transform",
|
||||
"desc": "Transform the selected layer."
|
||||
},
|
||||
"applyFilter": {
|
||||
"title": "Apply Filter",
|
||||
"desc": "Apply the pending filter to the selected layer."
|
||||
},
|
||||
"cancelFilter": {
|
||||
"title": "Cancel Filter",
|
||||
"desc": "Cancel the pending filter."
|
||||
},
|
||||
"applyTransform": {
|
||||
"title": "Apply Transform",
|
||||
"desc": "Apply the pending transform to the selected layer."
|
||||
},
|
||||
"cancelTransform": {
|
||||
"title": "Cancel Transform",
|
||||
"desc": "Cancel the pending transform."
|
||||
}
|
||||
},
|
||||
"workflows": {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
"languagePickerLabel": "Langue",
|
||||
"reportBugLabel": "Signaler un bug",
|
||||
"settingsLabel": "Paramètres",
|
||||
"img2img": "Image en image",
|
||||
"nodes": "Nœuds",
|
||||
"img2img": "Image vers Image",
|
||||
"nodes": "Processus",
|
||||
"upload": "Télécharger",
|
||||
"load": "Charger",
|
||||
"back": "Retour",
|
||||
@@ -15,7 +15,7 @@
|
||||
"accept": "Accepter",
|
||||
"cancel": "Annuler",
|
||||
"loading": "Chargement",
|
||||
"txt2img": "Texte vers image",
|
||||
"txt2img": "Texte vers Image",
|
||||
"postprocessing": "Post-Traitement",
|
||||
"file": "Fichier",
|
||||
"orderBy": "Trier par",
|
||||
@@ -28,7 +28,7 @@
|
||||
"installed": "Installé",
|
||||
"format": "format",
|
||||
"goTo": "Aller à",
|
||||
"input": "Saisie",
|
||||
"input": "Entrée",
|
||||
"linear": "Linéaire",
|
||||
"localSystem": "Système local",
|
||||
"learnMore": "En savoir plus",
|
||||
@@ -39,7 +39,7 @@
|
||||
"created": "Créé",
|
||||
"tab": "Onglet",
|
||||
"folder": "Dossier",
|
||||
"imageFailedToLoad": "Impossible de charger l'image",
|
||||
"imageFailedToLoad": "Impossible de charger l'Image",
|
||||
"prevPage": "Page précédente",
|
||||
"nextPage": "Page suivante",
|
||||
"selected": "Sélectionné",
|
||||
@@ -59,13 +59,105 @@
|
||||
"alpha": "Alpha",
|
||||
"enabled": "Activé",
|
||||
"disabled": "Désactivé",
|
||||
"direction": "Direction"
|
||||
"direction": "Direction",
|
||||
"aboutHeading": "Possédez Votre Pouvoir Créatif",
|
||||
"ai": "ia",
|
||||
"safetensors": "Safetensors",
|
||||
"apply": "Appliquer",
|
||||
"communityLabel": "Communauté",
|
||||
"loadingImage": "Chargement de l'Image",
|
||||
"view": "Visualisateur",
|
||||
"beta": "Beta",
|
||||
"on": "Activé",
|
||||
"batch": "Gestionaire de Lots",
|
||||
"outpaint": "Extension",
|
||||
"openInViewer": "Ouvrir dans le Visualisateur",
|
||||
"edit": "Édition",
|
||||
"off": "Désactivé",
|
||||
"areYouSure": "Êtes-vous sûr ?",
|
||||
"data": "Donnée",
|
||||
"details": "Détails",
|
||||
"placeholderSelectAModel": "Séléctionner un modèle",
|
||||
"reset": "Réinitialiser",
|
||||
"none": "Aucun",
|
||||
"new": "Nouveau",
|
||||
"dontShowMeThese": "Ne pas me montrer ceci",
|
||||
"auto": "Auto",
|
||||
"or": "ou",
|
||||
"checkpoint": "Point de sauvegarde",
|
||||
"ipAdapter": "IP Adapter",
|
||||
"t2iAdapter": "T2I Adapter",
|
||||
"inpaint": "Retouche",
|
||||
"toResolve": "À résoudre",
|
||||
"aboutDesc": "Utilisez vous Invoke pour le travail ? Consultez :",
|
||||
"copyError": "$t(gallery.copy) Erreur",
|
||||
"controlNet": "ControlNet",
|
||||
"positivePrompt": "Prompt Positif",
|
||||
"negativePrompt": "Prompt Négatif"
|
||||
},
|
||||
"gallery": {
|
||||
"galleryImageSize": "Taille de l'image",
|
||||
"gallerySettings": "Paramètres de la galerie",
|
||||
"autoSwitchNewImages": "Basculer automatiquement vers de nouvelles images",
|
||||
"noImagesInGallery": "Aucune image dans la galerie"
|
||||
"noImagesInGallery": "Aucune image à afficher",
|
||||
"bulkDownloadRequestedDesc": "Votre demande de téléchargement est en cours de traitement. Cela peut prendre quelques instants.",
|
||||
"deleteSelection": "Supprimer la sélection",
|
||||
"selectAllOnPage": "Séléctionner toute la page",
|
||||
"unableToLoad": "Impossible de charger la Galerie",
|
||||
"featuresWillReset": "Si vous supprimez cette image, ces fonctionnalités vont être réinitialisés.",
|
||||
"loading": "Chargement",
|
||||
"sortDirection": "Direction de tri",
|
||||
"sideBySide": "Côte-à-Côte",
|
||||
"hover": "Au passage de la souris",
|
||||
"assets": "Ressources",
|
||||
"alwaysShowImageSizeBadge": "Toujours montrer le badge de taille de l'Image",
|
||||
"gallery": "Galerie",
|
||||
"bulkDownloadRequestFailed": "Problème lors de la préparation du téléchargement",
|
||||
"copy": "Copier",
|
||||
"autoAssignBoardOnClick": "Assigner automatiquement une Planche lors du clic",
|
||||
"dropToUpload": "$t(gallery.drop) pour Charger",
|
||||
"dropOrUpload": "$t(gallery.drop) ou Séléctioner",
|
||||
"oldestFirst": "Plus Ancien en premier",
|
||||
"deleteImagePermanent": "Les Images supprimées ne peuvent pas être restorées.",
|
||||
"displaySearch": "Recherche d'Image",
|
||||
"exitBoardSearch": "Sortir de la recherche de Planche",
|
||||
"go": "Aller",
|
||||
"newestFirst": "Plus Récents en permier",
|
||||
"showStarredImagesFirst": "Monter les Images partagées en premier",
|
||||
"bulkDownloadFailed": "Téléchargement échoué",
|
||||
"bulkDownloadRequested": "Préparation du téléchargement",
|
||||
"compareImage": "Comparer l'Image",
|
||||
"openInViewer": "Ouvrir dans le Visualiseur",
|
||||
"showArchivedBoards": "Montrer les Planches archivées",
|
||||
"selectForCompare": "Séléctionner pour comparaison",
|
||||
"selectAnImageToCompare": "Séléctionner une Image à comparer",
|
||||
"exitCompare": "Sortir de la comparaison",
|
||||
"compareHelp2": "Appuyez sur <Kbd>M</Kbd> pour faire défiler les modes de comparaison.",
|
||||
"swapImages": "Échanger les Images",
|
||||
"move": "Déplacer",
|
||||
"compareHelp1": "Maintenir <Kbd>Alt</Kbd> lors du clic d'une image dans la galerie ou en utilisant les flèches du clavier pour changer l'Image à comparer.",
|
||||
"compareHelp3": "Appuyer sur <Kbd>C</Kbd> pour échanger les images à comparer.",
|
||||
"image": "image",
|
||||
"openViewer": "Ouvrir le Visualisateur",
|
||||
"closeViewer": "Fermer le Visualisateur",
|
||||
"currentlyInUse": "Cette image est actuellement utilisée dans ces fonctionalités :",
|
||||
"jump": "Sauter",
|
||||
"starImage": "Marquer l'Image",
|
||||
"download": "Téléchargement",
|
||||
"deleteImage_one": "Supprimer l'Image",
|
||||
"deleteImage_many": "Supprimer {{count}} Images",
|
||||
"deleteImage_other": "Supprimer {{count}} Images",
|
||||
"displayBoardSearch": "Recherche dans la Planche",
|
||||
"searchImages": "Chercher par Métadonnées",
|
||||
"slider": "Curseur",
|
||||
"stretchToFit": "Étirer pour remplir",
|
||||
"compareHelp4": "Appuyer sur <Kbd>Z</Kbd> ou <Kbd>Esc</Kbd> pour sortir.",
|
||||
"drop": "Déposer",
|
||||
"noImageSelected": "Pas d'Image séléctionnée",
|
||||
"downloadSelection": "Télécharger la sélection",
|
||||
"exitSearch": "Sortir de la recherche d'Image",
|
||||
"unstarImage": "Retirer le marquage de l'Image",
|
||||
"viewerImage": "Visualisation de l'Image"
|
||||
},
|
||||
"modelManager": {
|
||||
"modelManager": "Gestionnaire de modèle",
|
||||
@@ -138,11 +230,15 @@
|
||||
"reset": "Réinitialiser",
|
||||
"nextImage": "Image suivante",
|
||||
"previousImage": "Image précédente",
|
||||
"showOptionsPanel": "Montrer la page d'options",
|
||||
"showOptionsPanel": "Afficher le panneau latéral",
|
||||
"invokeProgressBar": "Barre de Progression Invoke",
|
||||
"menu": "Menu",
|
||||
"about": "À propos",
|
||||
"mode": "Mode"
|
||||
"mode": "Mode",
|
||||
"createIssue": "Créer un ticket",
|
||||
"submitSupportTicket": "Envoyer un ticket de support",
|
||||
"showGalleryPanel": "Afficher la galerie",
|
||||
"resetUI": "$t(accessibility.reset) l'Interface Utilisateur"
|
||||
},
|
||||
"boards": {
|
||||
"move": "Déplacer",
|
||||
@@ -152,14 +248,60 @@
|
||||
"clearSearch": "Effacer la recherche",
|
||||
"imagesWithCount_one": "{{count}} image",
|
||||
"imagesWithCount_many": "{{count}} images",
|
||||
"imagesWithCount_other": "{{count}} images"
|
||||
"imagesWithCount_other": "{{count}} images",
|
||||
"bottomMessage": "Supprimer cette planche et ses images va réinitialiser toutes les fonctionnalités les utilisant.",
|
||||
"deleteBoardAndImages": "Supprimer la Planche et les Images",
|
||||
"deleteBoardOnly": "Supprimer la Planche uniquement",
|
||||
"assetsWithCount_one": "{{count}} ressource",
|
||||
"assetsWithCount_many": "{{count}} ressources",
|
||||
"assetsWithCount_other": "{{count}} ressources",
|
||||
"selectedForAutoAdd": "Séléctioné pour Ajout Automatique",
|
||||
"noMatching": "Pas de Planches correspondantes",
|
||||
"myBoard": "Ma Planche",
|
||||
"menuItemAutoAdd": "Ajouter automatiquement à cette Planche",
|
||||
"changeBoard": "Changer de Planche",
|
||||
"movingImagesToBoard_one": "Déplacer {{count}} image à cette planche :",
|
||||
"movingImagesToBoard_many": "Déplacer {{count}} images à cette planche :",
|
||||
"movingImagesToBoard_other": "Déplacer {{count}} image à cette planche :",
|
||||
"viewBoards": "Voir les Planches",
|
||||
"hideBoards": "Cacher les Planches",
|
||||
"noBoards": "Pas de Planches {{boardType}}",
|
||||
"shared": "Planches Partagées",
|
||||
"searchBoard": "Chercher les Planches...",
|
||||
"addSharedBoard": "Créer une Planche Partagée",
|
||||
"addPrivateBoard": "Créer une Planche Privée",
|
||||
"boards": "Planches",
|
||||
"deletedPrivateBoardsCannotbeRestored": "Les planches supprimées ne peuvent pas être restaurées. Séléctionner 'Supprimer la planche uniquement' placera les images dans un état non catégorisé pour le créateur des images.",
|
||||
"uncategorized": "Non catégorisé",
|
||||
"downloadBoard": "Télécharger la Planche",
|
||||
"private": "Planches Privées",
|
||||
"deleteBoard": "Supprimer la Planche",
|
||||
"autoAddBoard": "Création de Planche Automatique",
|
||||
"addBoard": "Créer une Planche",
|
||||
"topMessage": "Cette planche contient des images utilisée dans ces fonctionnalités :",
|
||||
"selectBoard": "Séléctionner une Planche",
|
||||
"archiveBoard": "Archiver la Planche",
|
||||
"unarchiveBoard": "Déarchiver la Planche",
|
||||
"deletedBoardsCannotbeRestored": "Les planches supprimées ne peuvent pas être restaurées. Séléctionner 'Supprimer la planche uniquement' placera les images dans un état non catégorisé."
|
||||
},
|
||||
"accordions": {
|
||||
"advanced": {
|
||||
"title": "Avancé"
|
||||
"title": "Avancé",
|
||||
"options": "$t(accordions.advanced.title) Options"
|
||||
},
|
||||
"image": {
|
||||
"title": "Image"
|
||||
},
|
||||
"compositing": {
|
||||
"title": "Composition",
|
||||
"coherenceTab": "Passe de Cohérence",
|
||||
"infillTab": "Remplissage"
|
||||
},
|
||||
"generation": {
|
||||
"title": "Génération"
|
||||
},
|
||||
"control": {
|
||||
"title": "Controle"
|
||||
}
|
||||
},
|
||||
"queue": {
|
||||
@@ -181,6 +323,96 @@
|
||||
"canceled": "Annulé",
|
||||
"clearQueueAlertDialog2": "Voulez-vous vraiment effacer la file d'attente ?",
|
||||
"queueBack": "Ajouter à la file d'attente",
|
||||
"completed": "Terminé"
|
||||
"completed": "Terminé",
|
||||
"pauseSucceeded": "Traitement intérompu",
|
||||
"cancelBatchFailed": "Problème lors de l'annulation du Lot",
|
||||
"resumeTooltip": "Reprendre le traitement",
|
||||
"resumeFailed": "Problème lors de la reprise du traitement",
|
||||
"cancelItem": "Annuler l'élément",
|
||||
"pruneSucceeded": "Purgé {{item_count}} éléments complété de la file d'attente",
|
||||
"cancelTooltip": "Annuler l'élément actuel",
|
||||
"current": "Actuel",
|
||||
"pause": "Pause",
|
||||
"clearTooltip": "Annuler et Effacer tous les éléments",
|
||||
"pauseFailed": "Problème lors de l'intéruption du traitement",
|
||||
"cancelBatch": "Annuler le Lot",
|
||||
"pauseTooltip": "Intérrompre le traitement",
|
||||
"prune": "Purger",
|
||||
"pruneFailed": "Problème lors du Purgeage de la file d'attente",
|
||||
"clearQueueAlertDialog": "Effacer la file d'attente immédiatement annule tous les éléments en cours de traitement et efface entièrement la file d'attente. Les filtres en attente seront également annulés.",
|
||||
"pruneTooltip": "Purger {{item_count}} élémentscomplétés",
|
||||
"cancelSucceeded": "Élément annulé",
|
||||
"cancelFailed": "Problème lors de l'annulation de l'élément",
|
||||
"clearFailed": "Problème lors de l'Effacement de la file d'attente",
|
||||
"cancelBatchSucceeded": "Lot Annulé",
|
||||
"resume": "Reprendre",
|
||||
"resumeSucceeded": "Traitement repris",
|
||||
"enqueueing": "Ajout du Lot à la file d'attente",
|
||||
"origin": "Origine",
|
||||
"destination": "Destination",
|
||||
"batch": "Lot",
|
||||
"completedIn": "Complété en",
|
||||
"upscaling": "Agrandissement",
|
||||
"canvas": "Toile",
|
||||
"batchQueuedDesc_one": "Ajouté {{count}} session à {{direction}} de la file d'attente",
|
||||
"batchQueuedDesc_many": "Ajouté {{count}} sessions à {{direction}} de la file d'attente",
|
||||
"batchQueuedDesc_other": "Ajouté {{count}} sessions à {{direction}} de la file d'attente",
|
||||
"prompts_one": "Prompt",
|
||||
"prompts_many": "Prompts",
|
||||
"prompts_other": "Prompts",
|
||||
"batchQueued": "Lot ajouté à la file d'attente",
|
||||
"gallery": "Galerie",
|
||||
"notReady": "Impossible d'ajouter à la file d'attente",
|
||||
"batchFieldValues": "Valeurs Champ Lot",
|
||||
"front": "début",
|
||||
"graphQueued": "Graph ajouté à la file d'attente",
|
||||
"other": "Autre",
|
||||
"generation": "Génération",
|
||||
"workflows": "Processus",
|
||||
"batchFailedToQueue": "Impossible d'ajouter le Lot dans à la file d'attente",
|
||||
"graphFailedToQueue": "Impossible d'ajouter le graph à la file d'attente",
|
||||
"item": "Élément",
|
||||
"generations_one": "Génération",
|
||||
"generations_many": "Générations",
|
||||
"generations_other": "Générations",
|
||||
"iterations_one": "Itération",
|
||||
"iterations_many": "Itérations",
|
||||
"iterations_other": "Itérations",
|
||||
"back": "fin"
|
||||
},
|
||||
"prompt": {
|
||||
"noMatchingTriggers": "Pas de déclancheurs correspondants",
|
||||
"addPromptTrigger": "Ajouter un déclencheur de Prompt",
|
||||
"compatibleEmbeddings": "Embeddings Compatibles"
|
||||
},
|
||||
"hrf": {
|
||||
"upscaleMethod": "Méthode d'Agrandissement",
|
||||
"metadata": {
|
||||
"enabled": "Correction Haute Résolution Activée",
|
||||
"strength": "Force de la Correction Haute Résolution",
|
||||
"method": "Méthode de la Correction Haute Résolution"
|
||||
},
|
||||
"enableHrf": "Activer la Correction Haute Résolution",
|
||||
"hrf": "Correction Haute Résolution"
|
||||
},
|
||||
"invocationCache": {
|
||||
"clear": "Vider",
|
||||
"useCache": "Utiliser le Cache",
|
||||
"invocationCache": "Cache des Invocations",
|
||||
"enableFailed": "Problème lors de l'activation du Cache d'Invocation",
|
||||
"enable": "Activer",
|
||||
"enableSucceeded": "Cache d'Invocation Activé",
|
||||
"clearSucceeded": "Cache d'Invocation vidé",
|
||||
"disable": "Désactiver",
|
||||
"disableSucceeded": "Cache d'Invocation désactivé",
|
||||
"maxCacheSize": "Taille du Cache maximum",
|
||||
"misses": "Non trouvé dans le Cache",
|
||||
"clearFailed": "Problème lors du vidage du Cache d'Invocation",
|
||||
"cacheSize": "Taille du Cache",
|
||||
"hits": "Trouvé dans le Cache",
|
||||
"disableFailed": "Problème lors de la désactivation du Cache d'Invocation"
|
||||
},
|
||||
"hotkeys": {
|
||||
"hotkeys": "Raccourci clavier"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,17 @@
|
||||
"tab": "Scheda",
|
||||
"enabled": "Abilitato",
|
||||
"disabled": "Disabilitato",
|
||||
"dontShowMeThese": "Non mostrare più"
|
||||
"dontShowMeThese": "Non mostrare più",
|
||||
"openInViewer": "Apri nel visualizzatore",
|
||||
"apply": "Applica",
|
||||
"loadingImage": "Caricamento immagine",
|
||||
"off": "Disattivato",
|
||||
"edit": "Modifica",
|
||||
"placeholderSelectAModel": "Seleziona un modello",
|
||||
"reset": "Reimposta",
|
||||
"none": "Niente",
|
||||
"new": "Nuovo",
|
||||
"view": "Vista"
|
||||
},
|
||||
"gallery": {
|
||||
"galleryImageSize": "Dimensione dell'immagine",
|
||||
@@ -135,18 +145,313 @@
|
||||
"showStarredImagesFirst": "Mostra prima le immagini contrassegnate",
|
||||
"showArchivedBoards": "Mostra le bacheche archiviate",
|
||||
"searchImages": "Ricerca per metadati",
|
||||
"displayBoardSearch": "Mostra la ricerca nelle Bacheche",
|
||||
"displaySearch": "Mostra la ricerca",
|
||||
"displayBoardSearch": "Ricerca nella Bacheca",
|
||||
"displaySearch": "Ricerca immagine",
|
||||
"selectAllOnPage": "Seleziona tutto nella pagina",
|
||||
"exitBoardSearch": "Esci da Ricerca bacheca",
|
||||
"exitSearch": "Esci dalla ricerca",
|
||||
"exitSearch": "Esci dalla ricerca immagini",
|
||||
"go": "Vai",
|
||||
"jump": "Salta"
|
||||
"jump": "Salta",
|
||||
"move": "Sposta",
|
||||
"gallery": "Galleria",
|
||||
"openViewer": "Apri visualizzatore",
|
||||
"closeViewer": "Chiudi visualizzatore"
|
||||
},
|
||||
"hotkeys": {
|
||||
"searchHotkeys": "Cerca tasti di scelta rapida",
|
||||
"noHotkeysFound": "Nessun tasto di scelta rapida trovato",
|
||||
"clearSearch": "Cancella ricerca"
|
||||
"clearSearch": "Cancella ricerca",
|
||||
"app": {
|
||||
"selectCanvasTab": {
|
||||
"title": "Seleziona la scheda Tela",
|
||||
"desc": "Seleziona la scheda Tela."
|
||||
},
|
||||
"title": "Applicazione",
|
||||
"invoke": {
|
||||
"desc": "Metti in coda una generazione, aggiungendola alla fine della coda."
|
||||
},
|
||||
"invokeFront": {
|
||||
"title": "Invoke (Fronte)",
|
||||
"desc": "Metti in coda una generazione, aggiungendola all'inizio della coda."
|
||||
},
|
||||
"cancelQueueItem": {
|
||||
"desc": "Annulla l'elemento della coda in elaborazione.",
|
||||
"title": "Annulla"
|
||||
},
|
||||
"clearQueue": {
|
||||
"title": "Cancella la coda",
|
||||
"desc": "Annulla e cancella tutti gli elementi in coda."
|
||||
},
|
||||
"selectUpscalingTab": {
|
||||
"title": "Seleziona la scheda Amplia",
|
||||
"desc": "Seleziona la scheda Amplia."
|
||||
},
|
||||
"selectModelsTab": {
|
||||
"title": "Seleziona la scheda Modelli",
|
||||
"desc": "Seleziona la scheda Modelli."
|
||||
},
|
||||
"selectQueueTab": {
|
||||
"title": "Seleziona la scheda della Coda",
|
||||
"desc": "Seleziona la scheda della Coda."
|
||||
},
|
||||
"selectWorkflowsTab": {
|
||||
"desc": "Seleziona la scheda dei Flussi di lavoro.",
|
||||
"title": "Seleziona la scheda dei Flussi di lavoro"
|
||||
},
|
||||
"focusPrompt": {
|
||||
"title": "Seleziona il Prompt",
|
||||
"desc": "Sposta il cursore sul prompt positivo."
|
||||
},
|
||||
"toggleLeftPanel": {
|
||||
"title": "Attiva/disattiva il pannello sinistro",
|
||||
"desc": "Attiva/disattiva il pannello sinistro."
|
||||
},
|
||||
"toggleRightPanel": {
|
||||
"title": "Attiva/disattiva il pannello destro",
|
||||
"desc": "Attiva/disattiva il pannello destro."
|
||||
},
|
||||
"resetPanelLayout": {
|
||||
"title": "Ripristina il layout del pannello",
|
||||
"desc": "Ripristina le dimensioni e il layout predefiniti dei pannelli sinistro e destro."
|
||||
},
|
||||
"togglePanels": {
|
||||
"title": "Attiva/disattiva i pannelli",
|
||||
"desc": "Mostra o nascondi contemporaneamente i pannelli sinistro e destro."
|
||||
}
|
||||
},
|
||||
"hotkeys": "Tasti di scelta rapida",
|
||||
"canvas": {
|
||||
"transformSelected": {
|
||||
"desc": "Trasforma il livello selezionato.",
|
||||
"title": "Trasforma"
|
||||
},
|
||||
"fitBboxToCanvas": {
|
||||
"desc": "Scala e posiziona la vista per adattarla al riquadro di delimitazione.",
|
||||
"title": "Adatta il riquadro di delimitazione alla tela"
|
||||
},
|
||||
"redo": {
|
||||
"title": "Ripeti",
|
||||
"desc": "Ripeti l'ultima azione sulla tela."
|
||||
},
|
||||
"selectBrushTool": {
|
||||
"title": "Strumento pennello",
|
||||
"desc": "Seleziona lo strumento pennello."
|
||||
},
|
||||
"selectBboxTool": {
|
||||
"title": "Strumento di selezione riquadro",
|
||||
"desc": "Seleziona lo strumento riquadro di delimitazione."
|
||||
},
|
||||
"decrementToolWidth": {
|
||||
"title": "Diminuisci la larghezza dello strumento",
|
||||
"desc": "Diminuisce la larghezza dello strumento pennello o gomma, a seconda di quello selezionato."
|
||||
},
|
||||
"incrementToolWidth": {
|
||||
"title": "Aumenta la larghezza dello strumento",
|
||||
"desc": "Aumenta la larghezza dello strumento pennello o gomma, a seconda di quello selezionato."
|
||||
},
|
||||
"selectColorPickerTool": {
|
||||
"title": "Strumento di selezione del colore",
|
||||
"desc": "Seleziona lo strumento di selezione del colore."
|
||||
},
|
||||
"resetSelected": {
|
||||
"title": "Reimposta il Livello",
|
||||
"desc": "Reimposta il livello selezionato. Si applica solo alla Maschera Inpaint e alla Guida Regionale."
|
||||
},
|
||||
"undo": {
|
||||
"title": "Annulla",
|
||||
"desc": "Annulla l'ultima azione sulla tela."
|
||||
},
|
||||
"nextEntity": {
|
||||
"title": "Livello successivo",
|
||||
"desc": "Seleziona il livello successivo nell'elenco."
|
||||
},
|
||||
"filterSelected": {
|
||||
"title": "Filtro",
|
||||
"desc": "Filtra il livello selezionato. Applicabile solo ai livelli Raster e Controllo."
|
||||
},
|
||||
"setZoomTo100Percent": {
|
||||
"title": "Zoom al 100%",
|
||||
"desc": "Imposta l'ingrandimento della tela al 100%."
|
||||
},
|
||||
"setZoomTo200Percent": {
|
||||
"title": "Zoom al 200%",
|
||||
"desc": "Imposta l'ingrandimento della tela al 200%."
|
||||
},
|
||||
"setZoomTo400Percent": {
|
||||
"title": "Zoom al 400%",
|
||||
"desc": "Imposta l'ingrandimento della tela al 400%."
|
||||
},
|
||||
"setZoomTo800Percent": {
|
||||
"title": "Zoom al 800%",
|
||||
"desc": "Imposta l'ingrandimento della tela al 800%."
|
||||
},
|
||||
"quickSwitch": {
|
||||
"title": "Cambio rapido livello",
|
||||
"desc": "Passa tra gli ultimi due livelli selezionati. Se un livello è aggiunto ai segnalibri, passa sempre tra questo e l'ultimo livello non aggiunto ai segnalibri."
|
||||
},
|
||||
"deleteSelected": {
|
||||
"title": "Elimina livello",
|
||||
"desc": "Elimina il livello selezionato."
|
||||
},
|
||||
"prevEntity": {
|
||||
"title": "Livello precedente",
|
||||
"desc": "Seleziona il livello precedente nell'elenco."
|
||||
},
|
||||
"setFillToWhite": {
|
||||
"title": "Imposta il colore su bianco",
|
||||
"desc": "Imposta il colore dello strumento corrente su bianco."
|
||||
},
|
||||
"title": "Tela",
|
||||
"selectMoveTool": {
|
||||
"title": "Strumento Sposta",
|
||||
"desc": "Seleziona lo strumento sposta."
|
||||
},
|
||||
"fitLayersToCanvas": {
|
||||
"desc": "Scala e posiziona la vista per adattarla a tutti i livelli visibili.",
|
||||
"title": "Adatta i livelli alla tela"
|
||||
},
|
||||
"selectEraserTool": {
|
||||
"title": "Strumento gomma",
|
||||
"desc": "Selezionare lo strumento gomma."
|
||||
},
|
||||
"selectRectTool": {
|
||||
"title": "Strumento Rettangolo",
|
||||
"desc": "Seleziona lo strumento rettangolo."
|
||||
},
|
||||
"selectViewTool": {
|
||||
"title": "Strumento Visualizza",
|
||||
"desc": "Seleziona lo strumento Visualizza."
|
||||
}
|
||||
},
|
||||
"workflows": {
|
||||
"addNode": {
|
||||
"title": "Aggiungi nodo",
|
||||
"desc": "Apri il menu aggiungi nodo."
|
||||
},
|
||||
"pasteSelectionWithEdges": {
|
||||
"title": "Incolla con collegamenti",
|
||||
"desc": "Incolla i nodi copiati, i collegamenti e tutti i collegamenti connessi ai nodi copiati."
|
||||
},
|
||||
"copySelection": {
|
||||
"title": "Copia",
|
||||
"desc": "Copia i nodi ed i collegamenti selezionati."
|
||||
},
|
||||
"pasteSelection": {
|
||||
"title": "Incolla",
|
||||
"desc": "Incolla i nodi ed i collegamenti copiati."
|
||||
},
|
||||
"deleteSelection": {
|
||||
"title": "Elimina",
|
||||
"desc": "Elimina i nodi ed i collegamenti selezionati."
|
||||
},
|
||||
"redo": {
|
||||
"title": "Ripeti",
|
||||
"desc": "Ripeti l'ultima azione del flusso di lavoro."
|
||||
},
|
||||
"selectAll": {
|
||||
"desc": "Seleziona tutti i nodi ed i collegamenti.",
|
||||
"title": "Seleziona tutto"
|
||||
},
|
||||
"undo": {
|
||||
"desc": "Annulla l'ultima azione del flusso di lavoro.",
|
||||
"title": "Annulla"
|
||||
},
|
||||
"title": "Flussi di lavoro"
|
||||
},
|
||||
"viewer": {
|
||||
"nextComparisonMode": {
|
||||
"title": "Modalità di confronto successiva",
|
||||
"desc": "Scorri le modalità di confronto."
|
||||
},
|
||||
"recallPrompts": {
|
||||
"title": "Richiama i Prompt",
|
||||
"desc": "Richiama i prompt positivo e negativo per l'immagine corrente."
|
||||
},
|
||||
"remix": {
|
||||
"title": "Remixa",
|
||||
"desc": "Richiama tutti i metadati, ad eccezione del seme, per l'immagine corrente."
|
||||
},
|
||||
"useSize": {
|
||||
"desc": "Utilizza la dimensione dell'immagine corrente come dimensione del riquadro di delimitazione.",
|
||||
"title": "Usa Dimensioni"
|
||||
},
|
||||
"runPostprocessing": {
|
||||
"title": "Esegui Post-elaborazione",
|
||||
"desc": "Esegue la post-elaborazione selezionata sull'immagine corrente."
|
||||
},
|
||||
"title": "Visualizzatore immagini",
|
||||
"toggleViewer": {
|
||||
"title": "Mostra/Nascondi visualizzatore immagini",
|
||||
"desc": "Mostra o nascondi il visualizzatore di immagini. Disponibile solo nella scheda Tela."
|
||||
},
|
||||
"loadWorkflow": {
|
||||
"title": "Carica Flusso di lavoro",
|
||||
"desc": "Carica il flusso di lavoro salvato dell'immagine corrente (se presente)."
|
||||
},
|
||||
"recallAll": {
|
||||
"title": "Richiama tutti i metadati",
|
||||
"desc": "Richiama tutti i metadati dell'immagine corrente."
|
||||
},
|
||||
"swapImages": {
|
||||
"title": "Scambia le immagini di confronto",
|
||||
"desc": "Scambia le immagini da confrontare."
|
||||
},
|
||||
"recallSeed": {
|
||||
"title": "Richiama il seme",
|
||||
"desc": "Richiama il seme per l'immagine corrente."
|
||||
},
|
||||
"toggleMetadata": {
|
||||
"title": "Mostra/Nascondi metadati",
|
||||
"desc": "Mostra o nasconde la sovrapposizione dei metadati dell'immagine corrente."
|
||||
}
|
||||
},
|
||||
"gallery": {
|
||||
"selectAllOnPage": {
|
||||
"desc": "Seleziona tutte le immagini nella pagina corrente.",
|
||||
"title": "Seleziona tutto nella pagina"
|
||||
},
|
||||
"galleryNavUp": {
|
||||
"desc": "Naviga verso l'alto nella griglia della galleria, selezionando quell'immagine. Se sei in cima alla pagina, andrai alla pagina precedente.",
|
||||
"title": "Naviga verso l'alto"
|
||||
},
|
||||
"galleryNavRight": {
|
||||
"title": "Naviga a destra",
|
||||
"desc": "Naviga a destra nella griglia della galleria, selezionando quell'immagine. Se sei all'ultima immagine della riga, andrai alla riga successiva. Se sei all'ultima immagine della pagina, andrai alla pagina successiva."
|
||||
},
|
||||
"galleryNavLeftAlt": {
|
||||
"desc": "Uguale a Naviga a sinistra, ma seleziona l'immagine da confrontare, aprendo la modalità di confronto se non è già aperta.",
|
||||
"title": "Naviga a sinistra (Confronta immagine)"
|
||||
},
|
||||
"deleteSelection": {
|
||||
"title": "Elimina",
|
||||
"desc": "Elimina tutte le immagini selezionate. Per impostazione predefinita, ti verrà chiesto di confermare l'eliminazione. Se le immagini sono attualmente in uso nell'applicazione, verrai avvisato."
|
||||
},
|
||||
"clearSelection": {
|
||||
"title": "Cancella selezione",
|
||||
"desc": "Cancella la selezione corrente, se presente."
|
||||
},
|
||||
"galleryNavRightAlt": {
|
||||
"desc": "Uguale a Naviga a destra, ma seleziona l'immagine da confrontare, aprendo la modalità di confronto se non è già aperta.",
|
||||
"title": "Naviga a destra (Confronta immagine)"
|
||||
},
|
||||
"galleryNavDownAlt": {
|
||||
"title": "Naviga in basso (Confronta immagine)",
|
||||
"desc": "Uguale a Naviga in basso, ma seleziona l'immagine da confrontare, aprendo la modalità di confronto se non è già aperta."
|
||||
},
|
||||
"title": "Galleria",
|
||||
"galleryNavDown": {
|
||||
"desc": "Naviga verso il basso nella griglia della galleria, selezionando quell'immagine. Se sei in fondo alla pagina, andrai alla pagina successiva.",
|
||||
"title": "Naviga in basso"
|
||||
},
|
||||
"galleryNavLeft": {
|
||||
"title": "Naviga a sinistra",
|
||||
"desc": "Naviga a sinistra nella griglia della galleria, selezionando quell'immagine. Se sei alla prima immagine della riga, andrai alla riga precedente. Se sei alla prima immagine della pagina, andrai alla pagina precedente."
|
||||
},
|
||||
"galleryNavUpAlt": {
|
||||
"desc": "Uguale a Naviga verso l'alto, ma seleziona l'immagine da confrontare, aprendo la modalità di confronto se non è già aperta.",
|
||||
"title": "Naviga verso l'alto (Confronta immagine)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelManager": {
|
||||
"modelManager": "Gestione Modelli",
|
||||
@@ -250,7 +555,8 @@
|
||||
"ipAdapters": "Adattatori IP",
|
||||
"noMatchingModels": "Nessun modello corrispondente",
|
||||
"starterModelsInModelManager": "I modelli iniziali possono essere trovati in Gestione Modelli",
|
||||
"spandrelImageToImage": "Immagine a immagine (Spandrel)"
|
||||
"spandrelImageToImage": "Immagine a immagine (Spandrel)",
|
||||
"learnMoreAboutSupportedModels": "Scopri di più sui modelli che supportiamo"
|
||||
},
|
||||
"parameters": {
|
||||
"images": "Immagini",
|
||||
@@ -285,8 +591,8 @@
|
||||
"cancel": "Annulla"
|
||||
},
|
||||
"symmetry": "Simmetria",
|
||||
"seamlessXAxis": "Piastrella senza giunte Asse X",
|
||||
"seamlessYAxis": "Piastrella senza giunte Asse Y",
|
||||
"seamlessXAxis": "Asse X senza giunte",
|
||||
"seamlessYAxis": "Asse Y senza giunte",
|
||||
"scheduler": "Campionatore",
|
||||
"positivePromptPlaceholder": "Prompt Positivo",
|
||||
"negativePromptPlaceholder": "Prompt Negativo",
|
||||
@@ -312,7 +618,14 @@
|
||||
"ipAdapterNoImageSelected": "Nessuna immagine dell'adattatore IP selezionata",
|
||||
"rgNoPromptsOrIPAdapters": "Nessun prompt o adattatore IP",
|
||||
"rgNoRegion": "Nessuna regione selezionata"
|
||||
}
|
||||
},
|
||||
"fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), altezza riquadro è {{height}}",
|
||||
"fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), larghezza riquadro è {{width}}",
|
||||
"fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), larghezza del riquadro scalato è {{width}}",
|
||||
"fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), altezza del riquadro scalato è {{height}}",
|
||||
"noT5EncoderModelSelected": "Nessun modello di encoder T5 selezionato per la generazione con FLUX",
|
||||
"noCLIPEmbedModelSelected": "Nessun modello CLIP Embed selezionato per la generazione con FLUX",
|
||||
"noFLUXVAEModelSelected": "Nessun modello VAE selezionato per la generazione con FLUX"
|
||||
},
|
||||
"useCpuNoise": "Usa la CPU per generare rumore",
|
||||
"iterations": "Iterazioni",
|
||||
@@ -330,7 +643,8 @@
|
||||
"infillColorValue": "Colore di riempimento",
|
||||
"processImage": "Elabora Immagine",
|
||||
"sendToUpscale": "Invia a Amplia",
|
||||
"postProcessing": "Post-elaborazione (Shift + U)"
|
||||
"postProcessing": "Post-elaborazione (Shift + U)",
|
||||
"guidance": "Guida"
|
||||
},
|
||||
"settings": {
|
||||
"models": "Modelli",
|
||||
@@ -559,7 +873,8 @@
|
||||
"singleFieldType": "{{name}} (Singola)",
|
||||
"imageAccessError": "Impossibile trovare l'immagine {{image_name}}, ripristino ai valori predefiniti",
|
||||
"boardAccessError": "Impossibile trovare la bacheca {{board_id}}, ripristino ai valori predefiniti",
|
||||
"modelAccessError": "Impossibile trovare il modello {{key}}, ripristino ai valori predefiniti"
|
||||
"modelAccessError": "Impossibile trovare il modello {{key}}, ripristino ai valori predefiniti",
|
||||
"saveToGallery": "Salva nella Galleria"
|
||||
},
|
||||
"boards": {
|
||||
"autoAddBoard": "Aggiungi automaticamente bacheca",
|
||||
@@ -632,7 +947,7 @@
|
||||
"batchQueuedDesc_other": "Aggiunte {{count}} sessioni a {{direction}} della coda",
|
||||
"graphQueued": "Grafico in coda",
|
||||
"batch": "Lotto",
|
||||
"clearQueueAlertDialog": "Lo svuotamento della coda annulla immediatamente tutti gli elementi in elaborazione e cancella completamente la coda.",
|
||||
"clearQueueAlertDialog": "Lo svuotamento della coda annulla immediatamente tutti gli elementi in elaborazione e cancella completamente la coda. I filtri in sospeso verranno annullati.",
|
||||
"pending": "In attesa",
|
||||
"completedIn": "Completato in",
|
||||
"resumeFailed": "Problema nel riavvio dell'elaborazione",
|
||||
@@ -671,7 +986,15 @@
|
||||
"prompts_other": "Prompt",
|
||||
"generations_one": "Generazione",
|
||||
"generations_many": "Generazioni",
|
||||
"generations_other": "Generazioni"
|
||||
"generations_other": "Generazioni",
|
||||
"origin": "Origine",
|
||||
"destination": "Destinazione",
|
||||
"upscaling": "Ampliamento",
|
||||
"canvas": "Tela",
|
||||
"workflows": "Flussi di lavoro",
|
||||
"generation": "Generazione",
|
||||
"other": "Altro",
|
||||
"gallery": "Galleria"
|
||||
},
|
||||
"models": {
|
||||
"noMatchingModels": "Nessun modello corrispondente",
|
||||
@@ -1127,7 +1450,8 @@
|
||||
"imageDimensions": "Dimensioni dell'immagine",
|
||||
"parameterSet": "Parametro {{parameter}} impostato",
|
||||
"parsingFailed": "Analisi non riuscita",
|
||||
"recallParameter": "Richiama {{label}}"
|
||||
"recallParameter": "Richiama {{label}}",
|
||||
"canvasV2Metadata": "Tela"
|
||||
},
|
||||
"hrf": {
|
||||
"enableHrf": "Abilita Correzione Alta Risoluzione",
|
||||
@@ -1208,8 +1532,8 @@
|
||||
"autoNegative": "Auto Negativo",
|
||||
"deletePrompt": "Cancella il prompt",
|
||||
"rectangle": "Rettangolo",
|
||||
"addPositivePrompt": "Aggiungi $t(common.positivePrompt)",
|
||||
"addNegativePrompt": "Aggiungi $t(common.negativePrompt)",
|
||||
"addPositivePrompt": "Aggiungi $t(controlLayers.prompt)",
|
||||
"addNegativePrompt": "Aggiungi $t(controlLayers.negativePrompt)",
|
||||
"regionalGuidance": "Guida regionale",
|
||||
"opacity": "Opacità"
|
||||
},
|
||||
|
||||
@@ -83,7 +83,17 @@
|
||||
"tab": "Вкладка",
|
||||
"enabled": "Включено",
|
||||
"disabled": "Отключено",
|
||||
"dontShowMeThese": "Не показывай мне это"
|
||||
"dontShowMeThese": "Не показывай мне это",
|
||||
"apply": "Применить",
|
||||
"loadingImage": "Загрузка изображения",
|
||||
"off": "Выкл",
|
||||
"openInViewer": "Открыть в просмотрщике",
|
||||
"edit": "Редактировать",
|
||||
"view": "Просмотреть",
|
||||
"placeholderSelectAModel": "Выбрать модель",
|
||||
"reset": "Сброс",
|
||||
"none": "Ничего",
|
||||
"new": "Новый"
|
||||
},
|
||||
"gallery": {
|
||||
"galleryImageSize": "Размер изображений",
|
||||
@@ -138,17 +148,123 @@
|
||||
"selectAllOnPage": "Выбрать все на странице",
|
||||
"showArchivedBoards": "Показать архивированные доски",
|
||||
"searchImages": "Поиск по метаданным",
|
||||
"displayBoardSearch": "Отобразить поиск досок",
|
||||
"displaySearch": "Отобразить поиск",
|
||||
"displayBoardSearch": "Поиск доски",
|
||||
"displaySearch": "Поиск изображений",
|
||||
"exitBoardSearch": "Выйти из поиска досок",
|
||||
"go": "Перейти",
|
||||
"exitSearch": "Выйти из поиска",
|
||||
"jump": "Пыгнуть"
|
||||
"exitSearch": "Выйти из поиска изображений",
|
||||
"jump": "Пыгнуть",
|
||||
"move": "Двигать",
|
||||
"gallery": "Галерея",
|
||||
"openViewer": "Открыть просмотрщик",
|
||||
"closeViewer": "Закрыть просмотрщик"
|
||||
},
|
||||
"hotkeys": {
|
||||
"searchHotkeys": "Поиск горячих клавиш",
|
||||
"noHotkeysFound": "Горячие клавиши не найдены",
|
||||
"clearSearch": "Очистить поиск"
|
||||
"clearSearch": "Очистить поиск",
|
||||
"app": {
|
||||
"title": "Приложение",
|
||||
"invoke": {
|
||||
"desc": "Добавить генерацию в конец очереди.",
|
||||
"title": "Сгенерировать"
|
||||
},
|
||||
"clearQueue": {
|
||||
"title": "Очистить очередь",
|
||||
"desc": "Отмена и очистка всех элементов очереди."
|
||||
},
|
||||
"selectCanvasTab": {
|
||||
"title": "Выбрать вкладку Холст",
|
||||
"desc": "Выбирает вкладку Холст."
|
||||
},
|
||||
"selectUpscalingTab": {
|
||||
"title": "Выбрать вкладку Увеличение",
|
||||
"desc": "Выбирает вкладку увеличения."
|
||||
},
|
||||
"selectWorkflowsTab": {
|
||||
"title": "Выбрать вкладку Рабочие Процессы",
|
||||
"desc": "Выбирает вкладку рабочих процессов."
|
||||
},
|
||||
"focusPrompt": {
|
||||
"title": "Сфокусироваться на запросе",
|
||||
"desc": "Перемещает фокус курсора на положительный запрос."
|
||||
},
|
||||
"toggleLeftPanel": {
|
||||
"title": "Переключить левую панель",
|
||||
"desc": "Показывает или скрывает левую панель."
|
||||
},
|
||||
"resetPanelLayout": {
|
||||
"desc": "Верните левую и правую панели к размерам и расположению по умолчанию.",
|
||||
"title": "Сброс расположения панелей"
|
||||
},
|
||||
"invokeFront": {
|
||||
"title": "Сгенерировать (вперед)",
|
||||
"desc": "Добавьте генерацию вперед очереди."
|
||||
},
|
||||
"cancelQueueItem": {
|
||||
"title": "Отмена",
|
||||
"desc": "Отмена текущего обрабатываемого элемента очереди."
|
||||
},
|
||||
"selectModelsTab": {
|
||||
"desc": "Выбирает вкладку моделей.",
|
||||
"title": "Выбрать вкладку Модели"
|
||||
},
|
||||
"selectQueueTab": {
|
||||
"title": "Выбрать вкладку Очередь",
|
||||
"desc": "Выбирает вкладку очереди."
|
||||
},
|
||||
"togglePanels": {
|
||||
"title": "Переключить панели",
|
||||
"desc": "Показать или скрыть одновременно левую и правую панели."
|
||||
},
|
||||
"toggleRightPanel": {
|
||||
"title": "Переключить правую панель",
|
||||
"desc": "Показывает или скрывает правую панель."
|
||||
}
|
||||
},
|
||||
"canvas": {
|
||||
"title": "Холст",
|
||||
"selectBrushTool": {
|
||||
"title": "Инструмент кисть",
|
||||
"desc": "Выбирает кисть."
|
||||
}
|
||||
},
|
||||
"hotkeys": "Горячие клавиши",
|
||||
"workflows": {
|
||||
"undo": {
|
||||
"title": "Отмена",
|
||||
"desc": "Отменить последнее действие в рабочем процессе."
|
||||
},
|
||||
"deleteSelection": {
|
||||
"desc": "Удалить выделенные узлы и ребра."
|
||||
},
|
||||
"redo": {
|
||||
"title": "Вернуть",
|
||||
"desc": "Вернуть последнее действие в рабочем процессе."
|
||||
}
|
||||
},
|
||||
"viewer": {
|
||||
"nextComparisonMode": {
|
||||
"title": "Следующий режим сравнения",
|
||||
"desc": "Циклическое переключение режимов сравнения."
|
||||
},
|
||||
"loadWorkflow": {
|
||||
"desc": "Загрузить сохраненный рабочий процесс текущего изображения (если он есть).",
|
||||
"title": "Загрузить рабочий процесс"
|
||||
},
|
||||
"recallAll": {
|
||||
"desc": "Восстановить все метаданные текущего изображения.",
|
||||
"title": "Восстановить все метаданные"
|
||||
},
|
||||
"swapImages": {
|
||||
"desc": "Поменять местами сравниваемые изображения."
|
||||
},
|
||||
"title": "Просмотрщик изображений",
|
||||
"toggleViewer": {
|
||||
"title": "Открыть/закрыть просмотрщик",
|
||||
"desc": "Показать или скрыть просмотрщик изображений. Доступно только на вкладке «Холст»."
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelManager": {
|
||||
"modelManager": "Менеджер моделей",
|
||||
@@ -254,7 +370,9 @@
|
||||
"noModelsInstalledDesc1": "Установите модели с помощью",
|
||||
"noMatchingModels": "Нет подходящих моделей",
|
||||
"ipAdapters": "IP адаптеры",
|
||||
"starterModelsInModelManager": "Стартовые модели можно найти в Менеджере моделей"
|
||||
"starterModelsInModelManager": "Стартовые модели можно найти в Менеджере моделей",
|
||||
"learnMoreAboutSupportedModels": "Подробнее о поддерживаемых моделях",
|
||||
"t5Encoder": "T5 энкодер"
|
||||
},
|
||||
"parameters": {
|
||||
"images": "Изображения",
|
||||
@@ -289,8 +407,8 @@
|
||||
"symmetry": "Симметрия",
|
||||
"denoisingStrength": "Сила зашумления",
|
||||
"copyImage": "Скопировать изображение",
|
||||
"seamlessXAxis": "Бесшовность по оси X",
|
||||
"seamlessYAxis": "Бесшовность по оси Y",
|
||||
"seamlessXAxis": "Бесшовная ось X",
|
||||
"seamlessYAxis": "Бесшовная ось Y",
|
||||
"scheduler": "Планировщик",
|
||||
"positivePromptPlaceholder": "Запрос",
|
||||
"negativePromptPlaceholder": "Исключающий запрос",
|
||||
@@ -315,7 +433,18 @@
|
||||
"rgNoPromptsOrIPAdapters": "нет текстовых запросов или IP-адаптеров",
|
||||
"ipAdapterIncompatibleBaseModel": "несовместимая базовая модель IP-адаптера",
|
||||
"ipAdapterNoImageSelected": "изображение IP-адаптера не выбрано"
|
||||
}
|
||||
},
|
||||
"fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), ширина bbox {{width}}",
|
||||
"fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), высота bbox {{height}}",
|
||||
"fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), масштабированная высота bbox {{height}}",
|
||||
"fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16) масштабированная ширина bbox {{width}}",
|
||||
"noFLUXVAEModelSelected": "Для генерации FLUX не выбрана модель VAE",
|
||||
"noT5EncoderModelSelected": "Для генерации FLUX не выбрана модель T5 энкодера",
|
||||
"canvasIsFiltering": "Холст фильтруется",
|
||||
"canvasIsTransforming": "Холст трансформируется",
|
||||
"noCLIPEmbedModelSelected": "Для генерации FLUX не выбрана модель CLIP Embed",
|
||||
"canvasIsRasterizing": "Холст растрируется",
|
||||
"canvasIsCompositing": "Холст составляется"
|
||||
},
|
||||
"cfgRescaleMultiplier": "Множитель масштабирования CFG",
|
||||
"patchmatchDownScaleSize": "уменьшить",
|
||||
@@ -336,7 +465,12 @@
|
||||
"infillColorValue": "Цвет заливки",
|
||||
"postProcessing": "Постобработка (Shift + U)",
|
||||
"processImage": "Обработка изображения",
|
||||
"sendToUpscale": "Отправить на увеличение"
|
||||
"sendToUpscale": "Отправить на увеличение",
|
||||
"gaussianBlur": "Размытие по Гауссу",
|
||||
"staged": "Инсценировка",
|
||||
"optimizedImageToImage": "Оптимизированное img2img",
|
||||
"sendToCanvas": "Отправить на холст",
|
||||
"guidance": "Точность"
|
||||
},
|
||||
"settings": {
|
||||
"models": "Модели",
|
||||
@@ -413,7 +547,20 @@
|
||||
"outOfMemoryErrorDesc": "Ваши текущие настройки генерации превышают возможности системы. Пожалуйста, измените настройки и повторите попытку.",
|
||||
"somethingWentWrong": "Что-то пошло не так",
|
||||
"importFailed": "Импорт неудачен",
|
||||
"importSuccessful": "Импорт успешен"
|
||||
"importSuccessful": "Импорт успешен",
|
||||
"problemSavingLayer": "Не удалось сохранить слой",
|
||||
"sentToCanvas": "Отправить на холст",
|
||||
"unableToLoadImage": "Невозможно загрузить изображение",
|
||||
"unableToLoadImageMetadata": "Невозможно загрузить метаданные изображения",
|
||||
"imageSaved": "Изображение сохранено",
|
||||
"stylePresetLoaded": "Предустановка стиля загружена",
|
||||
"imageNotLoadedDesc": "Не удалось найти изображение",
|
||||
"imageSavingFailed": "Не удалось сохранить изображение",
|
||||
"problemCopyingLayer": "Не удалось скопировать слой",
|
||||
"unableToLoadStylePreset": "Невозможно загрузить предустановку стиля",
|
||||
"layerCopiedToClipboard": "Слой скопирован в буфер обмена",
|
||||
"sentToUpscale": "Отправить на увеличение",
|
||||
"layerSavedToAssets": "Слой сохранен в активах"
|
||||
},
|
||||
"accessibility": {
|
||||
"uploadImage": "Загрузить изображение",
|
||||
@@ -565,7 +712,8 @@
|
||||
"noGraph": "Нет графика",
|
||||
"imageAccessError": "Невозможно найти изображение {{image_name}}, сбрасываем на значение по умолчанию",
|
||||
"boardAccessError": "Невозможно найти доску {{board_id}}, сбрасываем на значение по умолчанию",
|
||||
"modelAccessError": "Невозможно найти модель {{key}}, сброс на модель по умолчанию"
|
||||
"modelAccessError": "Невозможно найти модель {{key}}, сброс на модель по умолчанию",
|
||||
"saveToGallery": "Сохранить в галерею"
|
||||
},
|
||||
"boards": {
|
||||
"autoAddBoard": "Авто добавление Доски",
|
||||
@@ -990,6 +1138,25 @@
|
||||
"paragraphs": [
|
||||
"Модель увеличения масштаба масштабирует изображение до выходного размера перед добавлением деталей. Можно использовать любую поддерживаемую модель масштабирования, но некоторые из них специализированы для различных видов изображений, например фотографий или линейных рисунков."
|
||||
]
|
||||
},
|
||||
"fluxDevLicense": {
|
||||
"heading": "Некоммерческая лицензия",
|
||||
"paragraphs": [
|
||||
"Модели FLUX.1 [dev] лицензируются по некоммерческой лицензии FLUX [dev]. Чтобы использовать этот тип модели в коммерческих целях в Invoke, посетите наш веб-сайт, чтобы узнать больше."
|
||||
]
|
||||
},
|
||||
"optimizedDenoising": {
|
||||
"heading": "Оптимизированный img2img",
|
||||
"paragraphs": [
|
||||
"Включите опцию «Оптимизированный img2img», чтобы получить более плавную шкалу Denoise Strength для img2img и перерисовки с моделями Flux. Эта настройка улучшает возможность контролировать степень изменения изображения, но может быть отключена, если вы предпочитаете использовать стандартную шкалу Denoise Strength. Эта настройка все еще находится в стадии настройки и в настоящее время имеет статус бета-версии."
|
||||
]
|
||||
},
|
||||
"paramGuidance": {
|
||||
"paragraphs": [
|
||||
"Контролирует, насколько сильно запрос влияет на процесс генерации.",
|
||||
"Высокие значения точности могут привести к перенасыщению, а высокие или низкие значения точности могут привести к искажению результатов генерации. Точность применима только к моделям FLUX DEV."
|
||||
],
|
||||
"heading": "Точность"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
@@ -1020,7 +1187,8 @@
|
||||
"parsingFailed": "Не удалось выполнить синтаксический анализ",
|
||||
"recallParameter": "Отозвать {{label}}",
|
||||
"allPrompts": "Все запросы",
|
||||
"imageDimensions": "Размеры изображения"
|
||||
"imageDimensions": "Размеры изображения",
|
||||
"canvasV2Metadata": "Холст"
|
||||
},
|
||||
"queue": {
|
||||
"status": "Статус",
|
||||
@@ -1049,7 +1217,7 @@
|
||||
"graphQueued": "График поставлен в очередь",
|
||||
"queue": "Очередь",
|
||||
"batch": "Пакет",
|
||||
"clearQueueAlertDialog": "Очистка очереди немедленно отменяет все элементы обработки и полностью очищает очередь.",
|
||||
"clearQueueAlertDialog": "Очистка очереди немедленно отменяет все элементы обработки и полностью очищает очередь. Ожидающие фильтры будут отменены.",
|
||||
"pending": "В ожидании",
|
||||
"completedIn": "Завершено за",
|
||||
"resumeFailed": "Проблема с возобновлением рендеринга",
|
||||
@@ -1088,7 +1256,15 @@
|
||||
"iterations_many": "Итераций",
|
||||
"generations_one": "Генерация",
|
||||
"generations_few": "Генерации",
|
||||
"generations_many": "Генераций"
|
||||
"generations_many": "Генераций",
|
||||
"other": "Другое",
|
||||
"gallery": "Галерея",
|
||||
"upscaling": "Увеличение",
|
||||
"canvas": "Холст",
|
||||
"generation": "Генерация",
|
||||
"workflows": "Рабочие процессы",
|
||||
"origin": "Источник",
|
||||
"destination": "Назначение"
|
||||
},
|
||||
"sdxl": {
|
||||
"refinerStart": "Запуск доработчика",
|
||||
@@ -1158,7 +1334,10 @@
|
||||
"loadWorkflow": "Рабочий процесс $t(common.load)",
|
||||
"convertGraph": "Конвертировать график",
|
||||
"loadFromGraph": "Загрузка рабочего процесса из графика",
|
||||
"autoLayout": "Автоматическое расположение"
|
||||
"autoLayout": "Автоматическое расположение",
|
||||
"userWorkflows": "Пользовательские рабочие процессы",
|
||||
"projectWorkflows": "Рабочие процессы проекта",
|
||||
"defaultWorkflows": "Стандартные рабочие процессы"
|
||||
},
|
||||
"hrf": {
|
||||
"enableHrf": "Включить исправление высокого разрешения",
|
||||
@@ -1220,7 +1399,39 @@
|
||||
"opacity": "Непрозрачность",
|
||||
"addLayer": "Добавить слой",
|
||||
"moveToFront": "На передний план",
|
||||
"addPositivePrompt": "Добавить $t(common.positivePrompt)"
|
||||
"addPositivePrompt": "Добавить $t(common.positivePrompt)",
|
||||
"regional": "Региональный",
|
||||
"bookmark": "Закладка для быстрого переключения",
|
||||
"fitBboxToLayers": "Подогнать Bbox к слоям",
|
||||
"mergeVisibleOk": "Объединенные видимые слои",
|
||||
"mergeVisibleError": "Ошибка объединения видимых слоев",
|
||||
"clearHistory": "Очистить историю",
|
||||
"mergeVisible": "Объединить видимые",
|
||||
"removeBookmark": "Удалить закладку",
|
||||
"saveLayerToAssets": "Сохранить слой в активы",
|
||||
"clearCaches": "Очистить кэши",
|
||||
"recalculateRects": "Пересчитать прямоугольники",
|
||||
"saveBboxToGallery": "Сохранить Bbox в галерею",
|
||||
"resetCanvas": "Сбросить холст",
|
||||
"canvas": "Холст",
|
||||
"global": "Глобальный",
|
||||
"newGlobalReferenceImageError": "Проблема с созданием глобального эталонного изображения",
|
||||
"newRegionalReferenceImageOk": "Создано региональное эталонное изображение",
|
||||
"newRegionalReferenceImageError": "Проблема создания регионального эталонного изображения",
|
||||
"newControlLayerOk": "Создан слой управления",
|
||||
"newControlLayerError": "Ошибка создания слоя управления",
|
||||
"newRasterLayerOk": "Создан растровый слой",
|
||||
"newRasterLayerError": "Ошибка создания растрового слоя",
|
||||
"newGlobalReferenceImageOk": "Создано глобальное эталонное изображение",
|
||||
"bboxOverlay": "Показать наложение Bbox",
|
||||
"saveCanvasToGallery": "Сохранить холст в галерею",
|
||||
"pullBboxIntoReferenceImageOk": "Bbox перенесен в эталонное изображение",
|
||||
"pullBboxIntoReferenceImageError": "Ошибка переноса BBox в эталонное изображение",
|
||||
"regionIsEmpty": "Выбранный регион пуст",
|
||||
"savedToGalleryOk": "Сохранено в галерею",
|
||||
"savedToGalleryError": "Ошибка сохранения в галерею",
|
||||
"pullBboxIntoLayerOk": "Bbox перенесен в слой",
|
||||
"pullBboxIntoLayerError": "Проблема с переносом BBox в слой"
|
||||
},
|
||||
"ui": {
|
||||
"tabs": {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Box, useGlobalModifiersInit } from '@invoke-ai/ui-library';
|
||||
import { GlobalImageHotkeys } from 'app/components/GlobalImageHotkeys';
|
||||
import type { StudioInitAction } from 'app/hooks/useStudioInitAction';
|
||||
import { useStudioInitAction } from 'app/hooks/useStudioInitAction';
|
||||
import { useSyncQueueStatus } from 'app/hooks/useSyncQueueStatus';
|
||||
@@ -7,7 +8,7 @@ import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/ap
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import type { PartialAppConfig } from 'app/types/invokeai';
|
||||
import ImageUploadOverlay from 'common/components/ImageUploadOverlay';
|
||||
import { useScopeFocusWatcher } from 'common/hooks/interactionScopes';
|
||||
import { useFocusRegionWatcher } from 'common/hooks/focus';
|
||||
import { useClearStorage } from 'common/hooks/useClearStorage';
|
||||
import { useFullscreenDropzone } from 'common/hooks/useFullscreenDropzone';
|
||||
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
|
||||
@@ -77,7 +78,7 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
|
||||
useStudioInitAction(studioInitAction);
|
||||
useStarterModelsToast();
|
||||
useSyncQueueStatus();
|
||||
useScopeFocusWatcher();
|
||||
useFocusRegionWatcher();
|
||||
|
||||
return (
|
||||
<ErrorBoundary onReset={handleReset} FallbackComponent={AppErrorBoundaryFallback}>
|
||||
@@ -104,6 +105,7 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
|
||||
<ClearQueueConfirmationsAlertDialog />
|
||||
<RefreshAfterResetModal />
|
||||
<DeleteBoardModal />
|
||||
<GlobalImageHotkeys />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useImageActions } from 'features/gallery/hooks/useImageActions';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { memo } from 'react';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const GlobalImageHotkeys = memo(() => {
|
||||
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken);
|
||||
|
||||
if (!imageDTO) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <GlobalImageHotkeysInternal imageDTO={imageDTO} />;
|
||||
});
|
||||
|
||||
GlobalImageHotkeys.displayName = 'GlobalImageHotkeys';
|
||||
|
||||
const GlobalImageHotkeysInternal = memo(({ imageDTO }: { imageDTO: ImageDTO }) => {
|
||||
const isGalleryFocused = useIsRegionFocused('gallery');
|
||||
const isViewerFocused = useIsRegionFocused('viewer');
|
||||
const imageActions = useImageActions(imageDTO);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const isUpscalingEnabled = useFeatureStatus('upscaling');
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'loadWorkflow',
|
||||
category: 'viewer',
|
||||
callback: imageActions.loadWorkflow,
|
||||
options: { enabled: isGalleryFocused || isViewerFocused },
|
||||
dependencies: [imageActions.loadWorkflow, isGalleryFocused, isViewerFocused],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'recallAll',
|
||||
category: 'viewer',
|
||||
callback: imageActions.recallAll,
|
||||
options: { enabled: !isStaging && (isGalleryFocused || isViewerFocused) },
|
||||
dependencies: [imageActions.recallAll, isStaging, isGalleryFocused, isViewerFocused],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'recallSeed',
|
||||
category: 'viewer',
|
||||
callback: imageActions.recallSeed,
|
||||
options: { enabled: isGalleryFocused || isViewerFocused },
|
||||
dependencies: [imageActions.recallSeed, isGalleryFocused, isViewerFocused],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'recallPrompts',
|
||||
category: 'viewer',
|
||||
callback: imageActions.recallPrompts,
|
||||
options: { enabled: isGalleryFocused || isViewerFocused },
|
||||
dependencies: [imageActions.recallPrompts, isGalleryFocused, isViewerFocused],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'remix',
|
||||
category: 'viewer',
|
||||
callback: imageActions.remix,
|
||||
options: { enabled: isGalleryFocused || isViewerFocused },
|
||||
dependencies: [imageActions.remix, isGalleryFocused, isViewerFocused],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'useSize',
|
||||
category: 'viewer',
|
||||
callback: imageActions.recallSize,
|
||||
options: { enabled: !isStaging && (isGalleryFocused || isViewerFocused) },
|
||||
dependencies: [imageActions.recallSize, isStaging, isGalleryFocused, isViewerFocused],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'runPostprocessing',
|
||||
category: 'viewer',
|
||||
callback: imageActions.upscale,
|
||||
options: { enabled: isUpscalingEnabled && isViewerFocused },
|
||||
dependencies: [isUpscalingEnabled, imageDTO, isViewerFocused],
|
||||
});
|
||||
return null;
|
||||
});
|
||||
|
||||
GlobalImageHotkeysInternal.displayName = 'GlobalImageHotkeysInternal';
|
||||
@@ -4,6 +4,7 @@ import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaS
|
||||
import {
|
||||
setCfgRescaleMultiplier,
|
||||
setCfgScale,
|
||||
setGuidance,
|
||||
setScheduler,
|
||||
setSteps,
|
||||
vaePrecisionChanged,
|
||||
@@ -13,6 +14,7 @@ import { setDefaultSettings } from 'features/parameters/store/actions';
|
||||
import {
|
||||
isParameterCFGRescaleMultiplier,
|
||||
isParameterCFGScale,
|
||||
isParameterGuidance,
|
||||
isParameterHeight,
|
||||
isParameterPrecision,
|
||||
isParameterScheduler,
|
||||
@@ -49,7 +51,7 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
|
||||
}
|
||||
|
||||
if (isNonRefinerMainModelConfig(modelConfig) && modelConfig.default_settings) {
|
||||
const { vae, vae_precision, cfg_scale, cfg_rescale_multiplier, steps, scheduler, width, height } =
|
||||
const { vae, vae_precision, cfg_scale, cfg_rescale_multiplier, steps, scheduler, width, height, guidance } =
|
||||
modelConfig.default_settings;
|
||||
|
||||
if (vae) {
|
||||
@@ -73,6 +75,12 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
|
||||
}
|
||||
}
|
||||
|
||||
if (guidance) {
|
||||
if (isParameterGuidance(guidance)) {
|
||||
dispatch(setGuidance(guidance));
|
||||
}
|
||||
}
|
||||
|
||||
if (cfg_scale) {
|
||||
if (isParameterCFGScale(cfg_scale)) {
|
||||
dispatch(setCfgScale(cfg_scale));
|
||||
|
||||
182
invokeai/frontend/web/src/common/hooks/focus.ts
Normal file
182
invokeai/frontend/web/src/common/hooks/focus.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import type { Atom } from 'nanostores';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { objectKeys } from 'tsafe';
|
||||
|
||||
/**
|
||||
* We need to manage focus regions to conditionally enable hotkeys:
|
||||
* - Some hotkeys should only be enabled when a specific region is focused.
|
||||
* - Some hotkeys may conflict with other regions, so we need to disable them when a specific region is focused. For
|
||||
* example, `esc` is used to clear the gallery selection, but it is also used to cancel a filter or transform on the
|
||||
* canvas.
|
||||
*
|
||||
* To manage focus regions, we use a system of hooks and stores:
|
||||
* - `useFocusRegion` is a hook that registers an element as part of a focus region. When that element is focused, by
|
||||
* click or any other action, that region is set as the focused region. Optionally, focus can be set on mount. This
|
||||
* is useful for components like the image viewer.
|
||||
* - `useIsRegionFocused` is a hook that returns a boolean indicating if a specific region is focused.
|
||||
* - `useFocusRegionWatcher` is a hook that listens for focus events on the window. When an element is focused, it
|
||||
* checks if it is part of a focus region and sets that region as the focused region.
|
||||
*/
|
||||
|
||||
//
|
||||
|
||||
const log = logger('system');
|
||||
|
||||
/**
|
||||
* The names of the focus regions.
|
||||
*/
|
||||
type FocusRegionName = 'gallery' | 'layers' | 'canvas' | 'workflows' | 'viewer';
|
||||
|
||||
/**
|
||||
* A map of focus regions to the elements that are part of that region.
|
||||
*/
|
||||
const REGION_TARGETS: Record<FocusRegionName, Set<HTMLElement>> = {
|
||||
gallery: new Set<HTMLElement>(),
|
||||
layers: new Set<HTMLElement>(),
|
||||
canvas: new Set<HTMLElement>(),
|
||||
workflows: new Set<HTMLElement>(),
|
||||
viewer: new Set<HTMLElement>(),
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* The currently-focused region or `null` if no region is focused.
|
||||
*/
|
||||
const $focusedRegion = atom<FocusRegionName | null>(null);
|
||||
|
||||
/**
|
||||
* A map of focus regions to atoms that indicate if that region is focused.
|
||||
*/
|
||||
const FOCUS_REGIONS = objectKeys(REGION_TARGETS).reduce(
|
||||
(acc, region) => {
|
||||
acc[`$${region}`] = computed($focusedRegion, (focusedRegion) => focusedRegion === region);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<`$${FocusRegionName}`, Atom<boolean>>
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the focused region, logging a trace level message.
|
||||
*/
|
||||
const setFocus = (region: FocusRegionName | null) => {
|
||||
$focusedRegion.set(region);
|
||||
log.trace(`Focus changed: ${region}`);
|
||||
};
|
||||
|
||||
type UseFocusRegionOptions = {
|
||||
focusOnMount?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers an element as part of a focus region. When that element is focused, by click or any other action, that
|
||||
* region is set as the focused region. Optionally, focus can be set on mount.
|
||||
*
|
||||
* On unmount, if the element is the last element in the region and the region is focused, the focused region is set to
|
||||
* `null`.
|
||||
*
|
||||
* @param region The focus region name.
|
||||
* @param ref The ref of the element to register.
|
||||
* @param options The options.
|
||||
*/
|
||||
export const useFocusRegion = (
|
||||
region: FocusRegionName,
|
||||
ref: RefObject<HTMLElement>,
|
||||
options?: UseFocusRegionOptions
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { focusOnMount = false } = { focusOnMount: false, ...options };
|
||||
|
||||
const element = ref.current;
|
||||
|
||||
REGION_TARGETS[region].add(element);
|
||||
|
||||
if (focusOnMount) {
|
||||
setFocus(region);
|
||||
}
|
||||
|
||||
return () => {
|
||||
REGION_TARGETS[region].delete(element);
|
||||
|
||||
if (REGION_TARGETS[region].size === 0 && $focusedRegion.get() === region) {
|
||||
setFocus(null);
|
||||
}
|
||||
};
|
||||
}, [options, ref, region]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a boolean indicating if a specific region is focused.
|
||||
* @param region The focus region name.
|
||||
*/
|
||||
export const useIsRegionFocused = (region: FocusRegionName) => {
|
||||
return useStore(FOCUS_REGIONS[`$${region}`]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Listens for focus events on the window. When an element is focused, it checks if it is part of a focus region and sets
|
||||
* that region as the focused region. The region corresponding to the deepest element is set.
|
||||
*/
|
||||
const onFocus = (_: FocusEvent) => {
|
||||
const activeElement = document.activeElement;
|
||||
if (!(activeElement instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const regionCandidates: { region: FocusRegionName; element: HTMLElement }[] = [];
|
||||
|
||||
for (const region of objectKeys(REGION_TARGETS)) {
|
||||
for (const element of REGION_TARGETS[region]) {
|
||||
if (element.contains(activeElement)) {
|
||||
regionCandidates.push({ region, element });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (regionCandidates.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by the shallowest element
|
||||
regionCandidates.sort((a, b) => {
|
||||
if (b.element.contains(a.element)) {
|
||||
return -1;
|
||||
}
|
||||
if (a.element.contains(b.element)) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Set the region of the deepest element
|
||||
const focusedRegion = regionCandidates[0]?.region;
|
||||
|
||||
if (!focusedRegion) {
|
||||
log.warn('No focused region found');
|
||||
return;
|
||||
}
|
||||
|
||||
setFocus(focusedRegion);
|
||||
};
|
||||
|
||||
/**
|
||||
* Listens for focus events on the window. When an element is focused, it checks if it is part of a focus region and sets
|
||||
* that region as the focused region. This is a singleton.
|
||||
*/
|
||||
export const useFocusRegionWatcher = () => {
|
||||
useAssertSingleton('useFocusRegionWatcher');
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('focus', onFocus, { capture: true });
|
||||
return () => {
|
||||
window.removeEventListener('focus', onFocus, { capture: true });
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
@@ -1,158 +0,0 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { objectKeys } from 'common/util/objectKeys';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import type { Atom } from 'nanostores';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
const log = logger('system');
|
||||
|
||||
const _INTERACTION_SCOPES = ['gallery', 'canvas', 'stagingArea', 'workflows', 'imageViewer'] as const;
|
||||
|
||||
type InteractionScope = (typeof _INTERACTION_SCOPES)[number];
|
||||
|
||||
export const $activeScopes = atom<Set<InteractionScope>>(new Set());
|
||||
|
||||
type InteractionScopeData = {
|
||||
targets: Set<HTMLElement>;
|
||||
$isActive: Atom<boolean>;
|
||||
};
|
||||
|
||||
export const INTERACTION_SCOPES: Record<InteractionScope, InteractionScopeData> = _INTERACTION_SCOPES.reduce(
|
||||
(acc, region) => {
|
||||
acc[region] = {
|
||||
targets: new Set(),
|
||||
$isActive: computed($activeScopes, (activeScopes) => activeScopes.has(region)),
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as Record<InteractionScope, InteractionScopeData>
|
||||
);
|
||||
|
||||
const formatScopes = (interactionScopes: Set<InteractionScope>) => {
|
||||
if (interactionScopes.size === 0) {
|
||||
return 'none';
|
||||
}
|
||||
return Array.from(interactionScopes).join(', ');
|
||||
};
|
||||
|
||||
export const addScope = (scope: InteractionScope) => {
|
||||
const currentScopes = $activeScopes.get();
|
||||
if (currentScopes.has(scope)) {
|
||||
return;
|
||||
}
|
||||
const newScopes = new Set(currentScopes);
|
||||
newScopes.add(scope);
|
||||
$activeScopes.set(newScopes);
|
||||
log.trace(`Added scope ${scope}: ${formatScopes($activeScopes.get())}`);
|
||||
};
|
||||
|
||||
export const removeScope = (scope: InteractionScope) => {
|
||||
const currentScopes = $activeScopes.get();
|
||||
if (!currentScopes.has(scope)) {
|
||||
return;
|
||||
}
|
||||
const newScopes = new Set(currentScopes);
|
||||
newScopes.delete(scope);
|
||||
$activeScopes.set(newScopes);
|
||||
log.trace(`Removed scope ${scope}: ${formatScopes($activeScopes.get())}`);
|
||||
};
|
||||
|
||||
export const setScopes = (scopes: InteractionScope[]) => {
|
||||
const newScopes = new Set(scopes);
|
||||
$activeScopes.set(newScopes);
|
||||
log.trace(`Set scopes: ${formatScopes($activeScopes.get())}`);
|
||||
};
|
||||
|
||||
export const useScopeOnFocus = (scope: InteractionScope, ref: RefObject<HTMLElement>) => {
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
INTERACTION_SCOPES[scope].targets.add(element);
|
||||
|
||||
return () => {
|
||||
INTERACTION_SCOPES[scope].targets.delete(element);
|
||||
};
|
||||
}, [ref, scope]);
|
||||
};
|
||||
|
||||
type UseScopeOnMountOptions = {
|
||||
mount?: boolean;
|
||||
unmount?: boolean;
|
||||
};
|
||||
|
||||
const defaultUseScopeOnMountOptions: UseScopeOnMountOptions = {
|
||||
mount: true,
|
||||
unmount: true,
|
||||
};
|
||||
|
||||
export const useScopeOnMount = (scope: InteractionScope, options?: UseScopeOnMountOptions) => {
|
||||
useEffect(() => {
|
||||
const { mount, unmount } = { ...defaultUseScopeOnMountOptions, ...options };
|
||||
|
||||
if (mount) {
|
||||
addScope(scope);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (unmount) {
|
||||
removeScope(scope);
|
||||
}
|
||||
};
|
||||
}, [options, scope]);
|
||||
};
|
||||
|
||||
export const useScopeImperativeApi = (scope: InteractionScope) => {
|
||||
const api = useMemo(() => {
|
||||
return {
|
||||
add: () => {
|
||||
addScope(scope);
|
||||
},
|
||||
remove: () => {
|
||||
removeScope(scope);
|
||||
},
|
||||
};
|
||||
}, [scope]);
|
||||
|
||||
return api;
|
||||
};
|
||||
|
||||
const handleFocusEvent = (_event: FocusEvent) => {
|
||||
const activeElement = document.activeElement;
|
||||
if (!(activeElement instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newActiveScopes = new Set<InteractionScope>();
|
||||
|
||||
for (const scope of objectKeys(INTERACTION_SCOPES)) {
|
||||
for (const element of INTERACTION_SCOPES[scope].targets) {
|
||||
if (element.contains(activeElement)) {
|
||||
newActiveScopes.add(scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const oldActiveScopes = $activeScopes.get();
|
||||
if (!isEqual(oldActiveScopes, newActiveScopes)) {
|
||||
$activeScopes.set(newActiveScopes);
|
||||
log.trace(`Scopes changed: ${formatScopes($activeScopes.get())}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const useScopeFocusWatcher = () => {
|
||||
useAssertSingleton('useScopeFocusWatcher');
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('focus', handleFocusEvent, true);
|
||||
return () => {
|
||||
window.removeEventListener('focus', handleFocusEvent, true);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { addScope, removeScope, setScopes } from 'common/hooks/interactionScopes';
|
||||
import { useClearQueue } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
|
||||
import { useInvoke } from 'features/queue/hooks/useInvoke';
|
||||
@@ -71,8 +70,6 @@ export const useGlobalHotkeys = () => {
|
||||
category: 'app',
|
||||
callback: () => {
|
||||
dispatch(setActiveTab('canvas'));
|
||||
addScope('canvas');
|
||||
removeScope('workflows');
|
||||
},
|
||||
dependencies: [dispatch],
|
||||
});
|
||||
@@ -82,8 +79,6 @@ export const useGlobalHotkeys = () => {
|
||||
category: 'app',
|
||||
callback: () => {
|
||||
dispatch(setActiveTab('upscaling'));
|
||||
removeScope('canvas');
|
||||
removeScope('workflows');
|
||||
},
|
||||
dependencies: [dispatch],
|
||||
});
|
||||
@@ -93,8 +88,6 @@ export const useGlobalHotkeys = () => {
|
||||
category: 'app',
|
||||
callback: () => {
|
||||
dispatch(setActiveTab('workflows'));
|
||||
removeScope('canvas');
|
||||
addScope('workflows');
|
||||
},
|
||||
dependencies: [dispatch],
|
||||
});
|
||||
@@ -104,7 +97,6 @@ export const useGlobalHotkeys = () => {
|
||||
category: 'app',
|
||||
callback: () => {
|
||||
dispatch(setActiveTab('models'));
|
||||
setScopes([]);
|
||||
},
|
||||
options: {
|
||||
enabled: isModelManagerEnabled,
|
||||
@@ -117,7 +109,6 @@ export const useGlobalHotkeys = () => {
|
||||
category: 'app',
|
||||
callback: () => {
|
||||
dispatch(setActiveTab('queue'));
|
||||
setScopes([]);
|
||||
},
|
||||
dependencies: [dispatch, isModelManagerEnabled],
|
||||
});
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import { Divider, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useFocusRegion } from 'common/hooks/focus';
|
||||
import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons';
|
||||
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
|
||||
import { EntityListSelectedEntityActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectHasEntities } from 'features/controlLayers/store/selectors';
|
||||
import { memo } from 'react';
|
||||
import { memo, useRef } from 'react';
|
||||
|
||||
export const CanvasLayersPanelContent = memo(() => {
|
||||
const hasEntities = useAppSelector(selectHasEntities);
|
||||
const layersPanelFocusRef = useRef<HTMLDivElement>(null);
|
||||
useFocusRegion('layers', layersPanelFocusRef);
|
||||
|
||||
return (
|
||||
<CanvasManagerProviderGate>
|
||||
<Flex flexDir="column" gap={2} w="full" h="full">
|
||||
<EntityListSelectedEntityActionBar />
|
||||
<Divider py={0} />
|
||||
{!hasEntities && <CanvasAddEntityButtons />}
|
||||
{hasEntities && <CanvasEntityList />}
|
||||
</Flex>
|
||||
</CanvasManagerProviderGate>
|
||||
<Flex ref={layersPanelFocusRef} flexDir="column" gap={2} w="full" h="full">
|
||||
<EntityListSelectedEntityActionBar />
|
||||
<Divider py={0} />
|
||||
{!hasEntities && <CanvasAddEntityButtons />}
|
||||
{hasEntities && <CanvasEntityList />}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ContextMenu, Flex, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import { useFocusRegion } from 'common/hooks/focus';
|
||||
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
|
||||
import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
|
||||
import { CanvasAlertsSendingToGallery } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo';
|
||||
@@ -35,7 +35,7 @@ export const CanvasMainPanelContent = memo(() => {
|
||||
);
|
||||
}, []);
|
||||
|
||||
useScopeOnFocus('canvas', ref);
|
||||
useFocusRegion('canvas', ref);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useDndContext } from '@dnd-kit/core';
|
||||
import { Box, Button, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import {
|
||||
$canvasRightPanelTabIndex,
|
||||
selectCanvasRightPanelGalleryTab,
|
||||
@@ -18,9 +18,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const CanvasRightPanel = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const tabIndex = useStore($canvasRightPanelTabIndex);
|
||||
useScopeOnFocus('gallery', ref);
|
||||
const imageViewer = useImageViewer();
|
||||
const onClickViewerToggleButton = useCallback(() => {
|
||||
if ($canvasRightPanelTabIndex.get() !== 1) {
|
||||
@@ -46,7 +44,9 @@ export const CanvasRightPanel = memo(() => {
|
||||
</TabList>
|
||||
<TabPanels w="full" h="full">
|
||||
<TabPanel w="full" h="full" p={0} pt={3}>
|
||||
<CanvasLayersPanelContent />
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasLayersPanelContent />
|
||||
</CanvasManagerProviderGate>
|
||||
</TabPanel>
|
||||
<TabPanel w="full" h="full" p={0} pt={3}>
|
||||
<GalleryPanelContent />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button, ButtonGroup, Flex, FormControl, FormLabel, Heading, Spacer, Switch } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { FilterSettings } from 'features/controlLayers/components/Filters/FilterSettings';
|
||||
import { FilterTypeSelect } from 'features/controlLayers/components/Filters/FilterTypeSelect';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
@@ -14,118 +15,142 @@ import {
|
||||
} from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import type { FilterConfig } from 'features/controlLayers/store/filters';
|
||||
import { IMAGE_FILTERS } from 'features/controlLayers/store/filters';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowsCounterClockwiseBold, PiCheckBold, PiShootingStarBold, PiXBold } from 'react-icons/pi';
|
||||
|
||||
const FilterBox = memo(({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const config = useStore(adapter.filterer.$filterConfig);
|
||||
const isProcessing = useStore(adapter.filterer.$isProcessing);
|
||||
const hasProcessed = useStore(adapter.filterer.$hasProcessed);
|
||||
const autoProcessFilter = useAppSelector(selectAutoProcessFilter);
|
||||
const isolatedFilteringPreview = useAppSelector(selectIsolatedFilteringPreview);
|
||||
const onChangeIsolatedPreview = useCallback(() => {
|
||||
dispatch(settingsIsolatedFilteringPreviewToggled());
|
||||
}, [dispatch]);
|
||||
const FilterContent = memo(
|
||||
({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useFocusRegion('canvas', ref, { focusOnMount: true });
|
||||
|
||||
const onChangeFilterConfig = useCallback(
|
||||
(filterConfig: FilterConfig) => {
|
||||
adapter.filterer.$filterConfig.set(filterConfig);
|
||||
},
|
||||
[adapter.filterer.$filterConfig]
|
||||
);
|
||||
const config = useStore(adapter.filterer.$filterConfig);
|
||||
const isCanvasFocused = useIsRegionFocused('canvas');
|
||||
const isProcessing = useStore(adapter.filterer.$isProcessing);
|
||||
const hasProcessed = useStore(adapter.filterer.$hasProcessed);
|
||||
const autoProcessFilter = useAppSelector(selectAutoProcessFilter);
|
||||
const isolatedFilteringPreview = useAppSelector(selectIsolatedFilteringPreview);
|
||||
const onChangeIsolatedPreview = useCallback(() => {
|
||||
dispatch(settingsIsolatedFilteringPreviewToggled());
|
||||
}, [dispatch]);
|
||||
|
||||
const onChangeFilterType = useCallback(
|
||||
(filterType: FilterConfig['type']) => {
|
||||
adapter.filterer.$filterConfig.set(IMAGE_FILTERS[filterType].buildDefaults());
|
||||
},
|
||||
[adapter.filterer.$filterConfig]
|
||||
);
|
||||
const onChangeFilterConfig = useCallback(
|
||||
(filterConfig: FilterConfig) => {
|
||||
adapter.filterer.$filterConfig.set(filterConfig);
|
||||
},
|
||||
[adapter.filterer.$filterConfig]
|
||||
);
|
||||
|
||||
const onChangeAutoProcessFilter = useCallback(() => {
|
||||
dispatch(settingsAutoProcessFilterToggled());
|
||||
}, [dispatch]);
|
||||
const onChangeFilterType = useCallback(
|
||||
(filterType: FilterConfig['type']) => {
|
||||
adapter.filterer.$filterConfig.set(IMAGE_FILTERS[filterType].buildDefaults());
|
||||
},
|
||||
[adapter.filterer.$filterConfig]
|
||||
);
|
||||
|
||||
const isValid = useMemo(() => {
|
||||
return IMAGE_FILTERS[config.type].validateConfig?.(config as never) ?? true;
|
||||
}, [config]);
|
||||
const onChangeAutoProcessFilter = useCallback(() => {
|
||||
dispatch(settingsAutoProcessFilterToggled());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
bg="base.800"
|
||||
borderRadius="base"
|
||||
p={4}
|
||||
flexDir="column"
|
||||
gap={4}
|
||||
w={420}
|
||||
h="auto"
|
||||
shadow="dark-lg"
|
||||
transitionProperty="height"
|
||||
transitionDuration="normal"
|
||||
>
|
||||
<Flex w="full" gap={4}>
|
||||
<Heading size="md" color="base.300" userSelect="none">
|
||||
{t('controlLayers.filter.filter')}
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<FormControl w="min-content">
|
||||
<FormLabel m={0}>{t('controlLayers.filter.autoProcess')}</FormLabel>
|
||||
<Switch size="sm" isChecked={autoProcessFilter} onChange={onChangeAutoProcessFilter} />
|
||||
</FormControl>
|
||||
<FormControl w="min-content">
|
||||
<FormLabel m={0}>{t('controlLayers.settings.isolatedPreview')}</FormLabel>
|
||||
<Switch size="sm" isChecked={isolatedFilteringPreview} onChange={onChangeIsolatedPreview} />
|
||||
</FormControl>
|
||||
const isValid = useMemo(() => {
|
||||
return IMAGE_FILTERS[config.type].validateConfig?.(config as never) ?? true;
|
||||
}, [config]);
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'applyFilter',
|
||||
category: 'canvas',
|
||||
callback: adapter.filterer.apply,
|
||||
options: { enabled: !isProcessing && isCanvasFocused },
|
||||
dependencies: [adapter.filterer, isProcessing, isCanvasFocused],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'cancelFilter',
|
||||
category: 'canvas',
|
||||
callback: adapter.filterer.cancel,
|
||||
options: { enabled: !isProcessing && isCanvasFocused },
|
||||
dependencies: [adapter.filterer, isProcessing, isCanvasFocused],
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex
|
||||
ref={ref}
|
||||
bg="base.800"
|
||||
borderRadius="base"
|
||||
p={4}
|
||||
flexDir="column"
|
||||
gap={4}
|
||||
w={420}
|
||||
h="auto"
|
||||
shadow="dark-lg"
|
||||
transitionProperty="height"
|
||||
transitionDuration="normal"
|
||||
>
|
||||
<Flex w="full" gap={4}>
|
||||
<Heading size="md" color="base.300" userSelect="none">
|
||||
{t('controlLayers.filter.filter')}
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<FormControl w="min-content">
|
||||
<FormLabel m={0}>{t('controlLayers.filter.autoProcess')}</FormLabel>
|
||||
<Switch size="sm" isChecked={autoProcessFilter} onChange={onChangeAutoProcessFilter} />
|
||||
</FormControl>
|
||||
<FormControl w="min-content">
|
||||
<FormLabel m={0}>{t('controlLayers.settings.isolatedPreview')}</FormLabel>
|
||||
<Switch size="sm" isChecked={isolatedFilteringPreview} onChange={onChangeIsolatedPreview} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
<FilterTypeSelect filterType={config.type} onChange={onChangeFilterType} />
|
||||
<FilterSettings filterConfig={config} onChange={onChangeFilterConfig} />
|
||||
<ButtonGroup isAttached={false} size="sm" w="full">
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiShootingStarBold />}
|
||||
onClick={adapter.filterer.processImmediate}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.filter.process')}
|
||||
isDisabled={!isValid || autoProcessFilter}
|
||||
>
|
||||
{t('controlLayers.filter.process')}
|
||||
</Button>
|
||||
<Spacer />
|
||||
<Button
|
||||
leftIcon={<PiArrowsCounterClockwiseBold />}
|
||||
onClick={adapter.filterer.reset}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.filter.reset')}
|
||||
variant="ghost"
|
||||
>
|
||||
{t('controlLayers.filter.reset')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiCheckBold />}
|
||||
onClick={adapter.filterer.apply}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.filter.apply')}
|
||||
isDisabled={!isValid || !hasProcessed}
|
||||
>
|
||||
{t('controlLayers.filter.apply')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiXBold />}
|
||||
onClick={adapter.filterer.cancel}
|
||||
loadingText={t('controlLayers.filter.cancel')}
|
||||
>
|
||||
{t('controlLayers.filter.cancel')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
<FilterTypeSelect filterType={config.type} onChange={onChangeFilterType} />
|
||||
<FilterSettings filterConfig={config} onChange={onChangeFilterConfig} />
|
||||
<ButtonGroup isAttached={false} size="sm" w="full">
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiShootingStarBold />}
|
||||
onClick={adapter.filterer.processImmediate}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.filter.process')}
|
||||
isDisabled={!isValid || autoProcessFilter}
|
||||
>
|
||||
{t('controlLayers.filter.process')}
|
||||
</Button>
|
||||
<Spacer />
|
||||
<Button
|
||||
leftIcon={<PiArrowsCounterClockwiseBold />}
|
||||
onClick={adapter.filterer.reset}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.filter.reset')}
|
||||
variant="ghost"
|
||||
>
|
||||
{t('controlLayers.filter.reset')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiCheckBold />}
|
||||
onClick={adapter.filterer.apply}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.filter.apply')}
|
||||
isDisabled={!isValid || !hasProcessed}
|
||||
>
|
||||
{t('controlLayers.filter.apply')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiXBold />}
|
||||
onClick={adapter.filterer.cancel}
|
||||
loadingText={t('controlLayers.filter.cancel')}
|
||||
>
|
||||
{t('controlLayers.filter.cancel')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
FilterBox.displayName = 'FilterBox';
|
||||
FilterContent.displayName = 'FilterContent';
|
||||
|
||||
export const Filter = () => {
|
||||
const canvasManager = useCanvasManager();
|
||||
@@ -134,7 +159,7 @@ export const Filter = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <FilterBox adapter={adapter} />;
|
||||
return <FilterContent adapter={adapter} />;
|
||||
};
|
||||
|
||||
Filter.displayName = 'Filter';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ButtonGroup } from '@invoke-ai/ui-library';
|
||||
import { useScopeOnMount } from 'common/hooks/interactionScopes';
|
||||
import { StagingAreaToolbarAcceptButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton';
|
||||
import { StagingAreaToolbarDiscardAllButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton';
|
||||
import { StagingAreaToolbarDiscardSelectedButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton';
|
||||
@@ -11,8 +10,6 @@ import { StagingAreaToolbarToggleShowResultsButton } from 'features/controlLayer
|
||||
import { memo } from 'react';
|
||||
|
||||
export const StagingAreaToolbar = memo(() => {
|
||||
useScopeOnMount('stagingArea');
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup borderRadius="base" shadow="dark-lg">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
|
||||
import {
|
||||
@@ -25,7 +25,7 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
|
||||
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||
const imageCount = useAppSelector(selectImageCount);
|
||||
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
|
||||
const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
|
||||
const isCanvasFocused = useIsRegionFocused('canvas');
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -50,9 +50,9 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
|
||||
acceptSelected,
|
||||
{
|
||||
preventDefault: true,
|
||||
enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
|
||||
enabled: isCanvasFocused && shouldShowStagedImage && imageCount > 1,
|
||||
},
|
||||
[isCanvasActive, shouldShowStagedImage, imageCount]
|
||||
[isCanvasFocused, shouldShowStagedImage, imageCount]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import {
|
||||
selectImageCount,
|
||||
@@ -17,7 +17,7 @@ export const StagingAreaToolbarNextButton = memo(() => {
|
||||
const canvasManager = useCanvasManager();
|
||||
const imageCount = useAppSelector(selectImageCount);
|
||||
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
|
||||
const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
|
||||
const isCanvasFocused = useIsRegionFocused('canvas');
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -30,9 +30,9 @@ export const StagingAreaToolbarNextButton = memo(() => {
|
||||
selectNext,
|
||||
{
|
||||
preventDefault: true,
|
||||
enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
|
||||
enabled: isCanvasFocused && shouldShowStagedImage && imageCount > 1,
|
||||
},
|
||||
[isCanvasActive, shouldShowStagedImage, imageCount]
|
||||
[isCanvasFocused, shouldShowStagedImage, imageCount]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import {
|
||||
selectImageCount,
|
||||
@@ -17,7 +17,7 @@ export const StagingAreaToolbarPrevButton = memo(() => {
|
||||
const canvasManager = useCanvasManager();
|
||||
const imageCount = useAppSelector(selectImageCount);
|
||||
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
|
||||
const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
|
||||
const isCanvasFocused = useIsRegionFocused('canvas');
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -30,9 +30,9 @@ export const StagingAreaToolbarPrevButton = memo(() => {
|
||||
selectPrev,
|
||||
{
|
||||
preventDefault: true,
|
||||
enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
|
||||
enabled: isCanvasFocused && shouldShowStagedImage && imageCount > 1,
|
||||
},
|
||||
[isCanvasActive, shouldShowStagedImage, imageCount]
|
||||
[isCanvasFocused, shouldShowStagedImage, imageCount]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
@@ -11,50 +10,50 @@ import { PiArrowsOutBold } from 'react-icons/pi';
|
||||
export const CanvasToolbarResetViewButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const canvasManager = useCanvasManager();
|
||||
const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
|
||||
const isCanvasFocused = useIsRegionFocused('canvas');
|
||||
const imageViewer = useImageViewer();
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'fitLayersToCanvas',
|
||||
category: 'canvas',
|
||||
callback: canvasManager.stage.fitLayersToStage,
|
||||
options: { enabled: isCanvasActive && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasActive, imageViewer.isOpen],
|
||||
options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasFocused, imageViewer.isOpen],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'fitBboxToCanvas',
|
||||
category: 'canvas',
|
||||
callback: canvasManager.stage.fitBboxToStage,
|
||||
options: { enabled: isCanvasActive && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasActive, imageViewer.isOpen],
|
||||
options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasFocused, imageViewer.isOpen],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'setZoomTo100Percent',
|
||||
category: 'canvas',
|
||||
callback: () => canvasManager.stage.setScale(1),
|
||||
options: { enabled: isCanvasActive && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasActive, imageViewer.isOpen],
|
||||
options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasFocused, imageViewer.isOpen],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'setZoomTo200Percent',
|
||||
category: 'canvas',
|
||||
callback: () => canvasManager.stage.setScale(2),
|
||||
options: { enabled: isCanvasActive && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasActive, imageViewer.isOpen],
|
||||
options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasFocused, imageViewer.isOpen],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'setZoomTo400Percent',
|
||||
category: 'canvas',
|
||||
callback: () => canvasManager.stage.setScale(4),
|
||||
options: { enabled: isCanvasActive && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasActive, imageViewer.isOpen],
|
||||
options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasFocused, imageViewer.isOpen],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'setZoomTo800Percent',
|
||||
category: 'canvas',
|
||||
callback: () => canvasManager.stage.setScale(8),
|
||||
options: { enabled: isCanvasActive && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasActive, imageViewer.isOpen],
|
||||
options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasFocused, imageViewer.isOpen],
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,27 +1,49 @@
|
||||
import { Button, ButtonGroup, Flex, FormControl, FormLabel, Heading, Spacer, Switch } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types';
|
||||
import {
|
||||
selectIsolatedTransformingPreview,
|
||||
settingsIsolatedTransformingPreviewToggled,
|
||||
} from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowsCounterClockwiseBold, PiArrowsOutBold, PiCheckBold, PiXBold } from 'react-icons/pi';
|
||||
|
||||
const TransformBox = memo(({ adapter }: { adapter: CanvasEntityAdapter }) => {
|
||||
const TransformContent = memo(({ adapter }: { adapter: CanvasEntityAdapter }) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useFocusRegion('canvas', ref, { focusOnMount: true });
|
||||
const isCanvasFocused = useIsRegionFocused('canvas');
|
||||
const isProcessing = useStore(adapter.transformer.$isProcessing);
|
||||
const isolatedTransformingPreview = useAppSelector(selectIsolatedTransformingPreview);
|
||||
const onChangeIsolatedPreview = useCallback(() => {
|
||||
dispatch(settingsIsolatedTransformingPreviewToggled());
|
||||
}, [dispatch]);
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'applyTransform',
|
||||
category: 'canvas',
|
||||
callback: adapter.transformer.applyTransform,
|
||||
options: { enabled: !isProcessing && isCanvasFocused },
|
||||
dependencies: [adapter.transformer, isProcessing, isCanvasFocused],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'cancelTransform',
|
||||
category: 'canvas',
|
||||
callback: adapter.transformer.stopTransform,
|
||||
options: { enabled: !isProcessing && isCanvasFocused },
|
||||
dependencies: [adapter.transformer, isProcessing, isCanvasFocused],
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex
|
||||
ref={ref}
|
||||
bg="base.800"
|
||||
borderRadius="base"
|
||||
p={4}
|
||||
@@ -86,7 +108,7 @@ const TransformBox = memo(({ adapter }: { adapter: CanvasEntityAdapter }) => {
|
||||
);
|
||||
});
|
||||
|
||||
TransformBox.displayName = 'Transform';
|
||||
TransformContent.displayName = 'TransformContent';
|
||||
|
||||
export const Transform = () => {
|
||||
const canvasManager = useCanvasManager();
|
||||
@@ -96,5 +118,5 @@ export const Transform = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <TransformBox adapter={adapter} />;
|
||||
return <TransformContent adapter={adapter} />;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Button, Collapse, Divider, Flex, IconButton, useDisclosure } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import { useFocusRegion } from 'common/hooks/focus';
|
||||
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
|
||||
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
|
||||
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
|
||||
@@ -26,8 +26,8 @@ const GalleryPanelContent = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
|
||||
const imperativePanelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useScopeOnFocus('gallery', ref);
|
||||
const galleryPanelFocusRef = useRef<HTMLDivElement>(null);
|
||||
useFocusRegion('gallery', galleryPanelFocusRef);
|
||||
|
||||
const boardsListPanelOptions = useMemo<UsePanelOptions>(
|
||||
() => ({
|
||||
@@ -50,7 +50,7 @@ const GalleryPanelContent = () => {
|
||||
}, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
|
||||
|
||||
return (
|
||||
<Flex ref={ref} position="relative" flexDirection="column" h="full" w="full" tabIndex={-1}>
|
||||
<Flex ref={galleryPanelFocusRef} position="relative" flexDirection="column" h="full" w="full" tabIndex={-1}>
|
||||
<Flex alignItems="center" w="full">
|
||||
<Flex w="25%">
|
||||
<Button
|
||||
|
||||
@@ -11,7 +11,7 @@ import { PiFlowArrowBold } from 'react-icons/pi';
|
||||
export const ImageMenuItemLoadWorkflow = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({});
|
||||
const [getAndLoadEmbeddedWorkflow, { isLoading }] = useGetAndLoadEmbeddedWorkflow();
|
||||
const hasTemplates = useStore($hasTemplates);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
@@ -20,7 +20,7 @@ export const ImageMenuItemLoadWorkflow = memo(() => {
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
icon={getAndLoadEmbeddedWorkflowResult.isLoading ? <SpinnerIcon /> : <PiFlowArrowBold />}
|
||||
icon={isLoading ? <SpinnerIcon /> : <PiFlowArrowBold />}
|
||||
onClickCapture={onClick}
|
||||
isDisabled={!imageDTO.has_workflow || !hasTemplates}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { SpinnerIcon } from 'features/gallery/components/ImageContextMenu/SpinnerIcon';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { useImageActions } from 'features/gallery/hooks/useImageActions';
|
||||
import { memo } from 'react';
|
||||
@@ -16,53 +15,24 @@ export const ImageMenuItemMetadataRecallActions = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
|
||||
const {
|
||||
recallAll,
|
||||
remix,
|
||||
recallSeed,
|
||||
recallPrompts,
|
||||
hasMetadata,
|
||||
hasSeed,
|
||||
hasPrompts,
|
||||
isLoadingMetadata,
|
||||
createAsPreset,
|
||||
} = useImageActions(imageDTO?.image_name);
|
||||
const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, createAsPreset } =
|
||||
useImageActions(imageDTO);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiArrowsCounterClockwiseBold />}
|
||||
onClickCapture={remix}
|
||||
isDisabled={isLoadingMetadata || !hasMetadata}
|
||||
>
|
||||
<MenuItem icon={<PiArrowsCounterClockwiseBold />} onClickCapture={remix} isDisabled={!hasMetadata}>
|
||||
{t('parameters.remixImage')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiQuotesBold />}
|
||||
onClickCapture={recallPrompts}
|
||||
isDisabled={isLoadingMetadata || !hasPrompts}
|
||||
>
|
||||
<MenuItem icon={<PiQuotesBold />} onClickCapture={recallPrompts} isDisabled={!hasPrompts}>
|
||||
{t('parameters.usePrompt')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiPlantBold />}
|
||||
onClickCapture={recallSeed}
|
||||
isDisabled={isLoadingMetadata || !hasSeed}
|
||||
>
|
||||
<MenuItem icon={<PiPlantBold />} onClickCapture={recallSeed} isDisabled={!hasSeed}>
|
||||
{t('parameters.useSeed')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiAsteriskBold />}
|
||||
onClickCapture={recallAll}
|
||||
isDisabled={isLoadingMetadata || !hasMetadata}
|
||||
>
|
||||
<MenuItem icon={<PiAsteriskBold />} onClickCapture={recallAll} isDisabled={!hasMetadata}>
|
||||
{t('parameters.useAll')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiPaintBrushBold />}
|
||||
onClickCapture={createAsPreset}
|
||||
isDisabled={isLoadingMetadata || !hasPrompts}
|
||||
>
|
||||
<MenuItem icon={<PiPaintBrushBold />} onClickCapture={createAsPreset} isDisabled={!hasPrompts}>
|
||||
{t('stylePresets.useForTemplate')}
|
||||
</MenuItem>
|
||||
</>
|
||||
|
||||
@@ -1,32 +1,18 @@
|
||||
import { Tag, TagCloseButton, TagLabel } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { $activeScopes } from 'common/hooks/interactionScopes';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
|
||||
import { selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { $isRightPanelOpen } from 'features/ui/store/uiSlice';
|
||||
import { computed } from 'nanostores';
|
||||
import { useCallback } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
const $isSelectAllEnabled = computed([$activeScopes, $isRightPanelOpen], (activeScopes, isGalleryPanelOpen) => {
|
||||
return activeScopes.has('gallery') && !activeScopes.has('workflows') && isGalleryPanelOpen;
|
||||
});
|
||||
|
||||
export const GallerySelectionCountTag = () => {
|
||||
export const GallerySelectionCountTag = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { selection } = useAppSelector((s) => s.gallery);
|
||||
const { t } = useTranslation();
|
||||
const { imageDTOs } = useGalleryImages();
|
||||
const isSelectAllEnabled = useStore($isSelectAllEnabled);
|
||||
|
||||
const onClearSelection = useCallback(() => {
|
||||
const firstImage = selection[0];
|
||||
if (firstImage) {
|
||||
dispatch(selectionChanged([firstImage]));
|
||||
}
|
||||
}, [dispatch, selection]);
|
||||
const isGalleryFocused = useIsRegionFocused('gallery');
|
||||
|
||||
const onSelectPage = useCallback(() => {
|
||||
dispatch(selectionChanged([...selection, ...imageDTOs]));
|
||||
@@ -36,22 +22,40 @@ export const GallerySelectionCountTag = () => {
|
||||
id: 'selectAllOnPage',
|
||||
category: 'gallery',
|
||||
callback: onSelectPage,
|
||||
options: { preventDefault: true, enabled: isSelectAllEnabled },
|
||||
dependencies: [onSelectPage, isSelectAllEnabled],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'clearSelection',
|
||||
category: 'gallery',
|
||||
callback: onClearSelection,
|
||||
options: { enabled: selection.length > 0 && isSelectAllEnabled },
|
||||
dependencies: [onClearSelection, selection, isSelectAllEnabled],
|
||||
options: { preventDefault: true, enabled: isGalleryFocused },
|
||||
dependencies: [onSelectPage, isGalleryFocused],
|
||||
});
|
||||
|
||||
if (selection.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <GallerySelectionCountTagContent selection={selection} />;
|
||||
});
|
||||
|
||||
GallerySelectionCountTag.displayName = 'GallerySelectionCountTag';
|
||||
|
||||
const GallerySelectionCountTagContent = memo(({ selection }: { selection: ImageDTO[] }) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const isGalleryFocused = useIsRegionFocused('gallery');
|
||||
|
||||
const onClearSelection = useCallback(() => {
|
||||
const firstImage = selection[0];
|
||||
if (firstImage) {
|
||||
dispatch(selectionChanged([firstImage]));
|
||||
}
|
||||
}, [dispatch, selection]);
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'clearSelection',
|
||||
category: 'gallery',
|
||||
callback: onClearSelection,
|
||||
options: { enabled: selection.length > 0 && isGalleryFocused },
|
||||
dependencies: [onClearSelection, selection, isGalleryFocused],
|
||||
});
|
||||
|
||||
return (
|
||||
<Tag
|
||||
position="absolute"
|
||||
@@ -72,4 +76,6 @@ export const GallerySelectionCountTag = () => {
|
||||
<TagCloseButton onClick={onClearSelection} />
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
GallerySelectionCountTagContent.displayName = 'GallerySelectionCountTagContent';
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
import { ButtonGroup, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
|
||||
import { useImageActions } from 'features/gallery/hooks/useImageActions';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { parseAndRecallImageDimensions } from 'features/metadata/util/handlers';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
|
||||
import { PostProcessingPopover } from 'features/parameters/components/PostProcessing/PostProcessingPopover';
|
||||
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
|
||||
import { size } from 'lodash-es';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
PiArrowsCounterClockwiseBold,
|
||||
@@ -30,109 +22,31 @@ import {
|
||||
PiRulerBold,
|
||||
} from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import { $isConnected, $progressImage } from 'services/events/stores';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
const CurrentImageButtons = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isConnected = useStore($isConnected);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
|
||||
const progressImage = useStore($progressImage);
|
||||
const shouldDisableToolbarButtons = useMemo(() => {
|
||||
return Boolean(progressImage) || !lastSelectedImage;
|
||||
}, [lastSelectedImage, progressImage]);
|
||||
const templates = useStore($templates);
|
||||
const isUpscalingEnabled = useFeatureStatus('upscaling');
|
||||
const isQueueMutationInProgress = useIsQueueMutationInProgress();
|
||||
const { t } = useTranslation();
|
||||
const isImageViewerActive = useStore(INTERACTION_SCOPES.imageViewer.$isActive);
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken);
|
||||
|
||||
const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, isLoadingMetadata } =
|
||||
useImageActions(lastSelectedImage?.image_name);
|
||||
if (!imageDTO) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({});
|
||||
return <CurrentImageButtonsContent imageDTO={imageDTO} />;
|
||||
};
|
||||
|
||||
const handleLoadWorkflow = useCallback(() => {
|
||||
if (!lastSelectedImage || !lastSelectedImage.has_workflow) {
|
||||
return;
|
||||
}
|
||||
getAndLoadEmbeddedWorkflow(lastSelectedImage.image_name);
|
||||
}, [getAndLoadEmbeddedWorkflow, lastSelectedImage]);
|
||||
export default memo(CurrentImageButtons);
|
||||
|
||||
const handleUseSize = useCallback(() => {
|
||||
if (isStaging) {
|
||||
return;
|
||||
}
|
||||
parseAndRecallImageDimensions(lastSelectedImage);
|
||||
}, [isStaging, lastSelectedImage]);
|
||||
const handleClickUpscale = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
dispatch(adHocPostProcessingRequested({ imageDTO }));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
dispatch(imagesToDeleteSelected([imageDTO]));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'loadWorkflow',
|
||||
category: 'viewer',
|
||||
callback: handleLoadWorkflow,
|
||||
options: { enabled: isImageViewerActive },
|
||||
dependencies: [handleLoadWorkflow, isImageViewerActive],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'recallAll',
|
||||
category: 'viewer',
|
||||
callback: recallAll,
|
||||
options: { enabled: isImageViewerActive },
|
||||
dependencies: [recallAll, isImageViewerActive],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'recallSeed',
|
||||
category: 'viewer',
|
||||
callback: recallSeed,
|
||||
options: { enabled: isImageViewerActive },
|
||||
dependencies: [recallSeed, isImageViewerActive],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'recallPrompts',
|
||||
category: 'viewer',
|
||||
callback: recallPrompts,
|
||||
options: { enabled: isImageViewerActive },
|
||||
dependencies: [recallPrompts, isImageViewerActive],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'remix',
|
||||
category: 'viewer',
|
||||
callback: remix,
|
||||
options: { enabled: isImageViewerActive },
|
||||
dependencies: [remix, isImageViewerActive],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'useSize',
|
||||
category: 'viewer',
|
||||
callback: handleUseSize,
|
||||
options: { enabled: isImageViewerActive },
|
||||
dependencies: [handleUseSize, isImageViewerActive],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'runPostprocessing',
|
||||
category: 'viewer',
|
||||
callback: handleClickUpscale,
|
||||
options: { enabled: Boolean(isUpscalingEnabled && isImageViewerActive && isConnected) },
|
||||
dependencies: [isUpscalingEnabled, imageDTO, shouldDisableToolbarButtons, isConnected, isImageViewerActive],
|
||||
});
|
||||
const CurrentImageButtonsContent = memo(({ imageDTO }: { imageDTO: ImageDTO }) => {
|
||||
const { t } = useTranslation();
|
||||
const hasTemplates = useStore($hasTemplates);
|
||||
const imageActions = useImageActions(imageDTO);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const isUpscalingEnabled = useFeatureStatus('upscaling');
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
|
||||
<ButtonGroup>
|
||||
<Menu isLazy>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
@@ -145,68 +59,62 @@ const CurrentImageButtons = () => {
|
||||
</Menu>
|
||||
</ButtonGroup>
|
||||
|
||||
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
|
||||
<ButtonGroup>
|
||||
<IconButton
|
||||
icon={<PiFlowArrowBold />}
|
||||
tooltip={`${t('nodes.loadWorkflow')} (W)`}
|
||||
aria-label={`${t('nodes.loadWorkflow')} (W)`}
|
||||
isDisabled={!imageDTO?.has_workflow || !size(templates)}
|
||||
onClick={handleLoadWorkflow}
|
||||
isLoading={getAndLoadEmbeddedWorkflowResult.isLoading}
|
||||
isDisabled={!imageActions.hasWorkflow || !hasTemplates}
|
||||
onClick={imageActions.loadWorkflow}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiArrowsCounterClockwiseBold />}
|
||||
tooltip={`${t('parameters.remixImage')} (R)`}
|
||||
aria-label={`${t('parameters.remixImage')} (R)`}
|
||||
isDisabled={!hasMetadata}
|
||||
onClick={remix}
|
||||
isDisabled={!imageActions.hasMetadata}
|
||||
onClick={imageActions.remix}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiQuotesBold />}
|
||||
tooltip={`${t('parameters.usePrompt')} (P)`}
|
||||
aria-label={`${t('parameters.usePrompt')} (P)`}
|
||||
isDisabled={!hasPrompts}
|
||||
onClick={recallPrompts}
|
||||
isDisabled={!imageActions.hasPrompts}
|
||||
onClick={imageActions.recallPrompts}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiPlantBold />}
|
||||
tooltip={`${t('parameters.useSeed')} (S)`}
|
||||
aria-label={`${t('parameters.useSeed')} (S)`}
|
||||
isDisabled={!hasSeed}
|
||||
onClick={recallSeed}
|
||||
isDisabled={!imageActions.hasSeed}
|
||||
onClick={imageActions.recallSeed}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiRulerBold />}
|
||||
tooltip={`${t('parameters.useSize')} (D)`}
|
||||
aria-label={`${t('parameters.useSize')} (D)`}
|
||||
onClick={handleUseSize}
|
||||
onClick={imageActions.recallSize}
|
||||
isDisabled={isStaging}
|
||||
/>
|
||||
<IconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
icon={<PiAsteriskBold />}
|
||||
tooltip={`${t('parameters.useAll')} (A)`}
|
||||
aria-label={`${t('parameters.useAll')} (A)`}
|
||||
isDisabled={!hasMetadata}
|
||||
onClick={recallAll}
|
||||
isDisabled={!imageActions.hasMetadata}
|
||||
onClick={imageActions.recallAll}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
|
||||
{isUpscalingEnabled && (
|
||||
<ButtonGroup isDisabled={isQueueMutationInProgress}>
|
||||
{isUpscalingEnabled && <PostProcessingPopover imageDTO={imageDTO} />}
|
||||
<ButtonGroup>
|
||||
<PostProcessingPopover imageDTO={imageDTO} />
|
||||
</ButtonGroup>
|
||||
)}
|
||||
|
||||
<ButtonGroup>
|
||||
<DeleteImageButton onClick={handleDelete} />
|
||||
<DeleteImageButton onClick={imageActions.delete} />
|
||||
</ButtonGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default memo(CurrentImageButtons);
|
||||
CurrentImageButtonsContent.displayName = 'CurrentImageButtonsContent';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Flex, IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useScopeOnFocus, useScopeOnMount } from 'common/hooks/interactionScopes';
|
||||
import { useFocusRegion } from 'common/hooks/focus';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { CanvasAlertsSendingToCanvas } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo';
|
||||
import { CompareToolbar } from 'features/gallery/components/ImageViewer/CompareToolbar';
|
||||
@@ -10,7 +10,7 @@ import { ImageComparisonDroppable } from 'features/gallery/components/ImageViewe
|
||||
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar';
|
||||
import { selectHasImageToCompare } from 'features/gallery/store/gallerySelectors';
|
||||
import type { ReactNode } from 'react';
|
||||
import { memo, useEffect, useRef } from 'react';
|
||||
import { memo, useRef } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiXBold } from 'react-icons/pi';
|
||||
@@ -22,17 +22,16 @@ type Props = {
|
||||
closeButton?: ReactNode;
|
||||
};
|
||||
|
||||
const useFocusRegionOptions = {
|
||||
focusOnMount: true,
|
||||
};
|
||||
|
||||
export const ImageViewer = memo(({ closeButton }: Props) => {
|
||||
useAssertSingleton('ImageViewer');
|
||||
const hasImageToCompare = useAppSelector(selectHasImageToCompare);
|
||||
const [containerRef, containerDims] = useMeasure<HTMLDivElement>();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useScopeOnFocus('imageViewer', ref);
|
||||
useScopeOnMount('imageViewer');
|
||||
|
||||
useEffect(() => {
|
||||
ref?.current?.focus();
|
||||
}, []);
|
||||
useFocusRegion('viewer', ref, useFocusRegionOptions);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { $activeScopes, INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { $canvasRightPanelTab } from 'features/controlLayers/store/ephemeral';
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
@@ -9,21 +9,9 @@ import { useGalleryPagination } from 'features/gallery/hooks/useGalleryPaginatio
|
||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { $isRightPanelOpen } from 'features/ui/store/uiSlice';
|
||||
import { computed } from 'nanostores';
|
||||
import { useMemo } from 'react';
|
||||
import { useListImagesQuery } from 'services/api/endpoints/images';
|
||||
|
||||
const $leftRightHotkeysEnabled = computed($activeScopes, (activeScopes) => {
|
||||
// The left and right hotkeys can be used when the gallery is focused and the canvas is not focused, OR when the image viewer is focused.
|
||||
return !activeScopes.has('canvas') || activeScopes.has('imageViewer');
|
||||
});
|
||||
|
||||
const $upDownHotkeysEnabled = computed([$activeScopes, $isRightPanelOpen], (activeScopes, isGalleryPanelOpen) => {
|
||||
// The up and down hotkeys can be used when the gallery is focused and the canvas is not focused, and the gallery panel is open.
|
||||
return !activeScopes.has('canvas') && isGalleryPanelOpen;
|
||||
});
|
||||
|
||||
/**
|
||||
* Registers gallery hotkeys. This hook is a singleton.
|
||||
*/
|
||||
@@ -34,11 +22,11 @@ export const useGalleryHotkeys = () => {
|
||||
const selection = useAppSelector((s) => s.gallery.selection);
|
||||
const queryArgs = useAppSelector(selectListImagesQueryArgs);
|
||||
const queryResult = useListImagesQuery(queryArgs);
|
||||
const leftRightHotkeysEnabled = useStore($leftRightHotkeysEnabled);
|
||||
const upDownHotkeysEnabled = useStore($upDownHotkeysEnabled);
|
||||
const canvasRightPanelTab = useStore($canvasRightPanelTab);
|
||||
const appTab = useAppSelector(selectActiveTab);
|
||||
const isWorkflowsScopeActive = useStore(INTERACTION_SCOPES.workflows.$isActive);
|
||||
const isWorkflowsFocused = useIsRegionFocused('workflows');
|
||||
const isGalleryFocused = useIsRegionFocused('gallery');
|
||||
const isImageViewerFocused = useIsRegionFocused('viewer');
|
||||
|
||||
// When we are on the canvas tab, we need to disable the delete hotkey when the user is focused on the layers tab in
|
||||
// the right hand panel, because the same hotkey is used to delete layers.
|
||||
@@ -70,14 +58,15 @@ export const useGalleryHotkeys = () => {
|
||||
}
|
||||
handleLeftImage(false);
|
||||
},
|
||||
options: { preventDefault: true, enabled: leftRightHotkeysEnabled },
|
||||
options: { preventDefault: true, enabled: isGalleryFocused || isImageViewerFocused },
|
||||
dependencies: [
|
||||
handleLeftImage,
|
||||
isOnFirstImageOfView,
|
||||
goPrev,
|
||||
isPrevEnabled,
|
||||
queryResult.isFetching,
|
||||
leftRightHotkeysEnabled,
|
||||
isGalleryFocused,
|
||||
isImageViewerFocused,
|
||||
],
|
||||
});
|
||||
|
||||
@@ -93,14 +82,15 @@ export const useGalleryHotkeys = () => {
|
||||
handleRightImage(false);
|
||||
}
|
||||
},
|
||||
options: { preventDefault: true, enabled: leftRightHotkeysEnabled },
|
||||
options: { preventDefault: true, enabled: isGalleryFocused || isImageViewerFocused },
|
||||
dependencies: [
|
||||
isOnLastImageOfView,
|
||||
goNext,
|
||||
isNextEnabled,
|
||||
queryResult.isFetching,
|
||||
handleRightImage,
|
||||
leftRightHotkeysEnabled,
|
||||
isGalleryFocused,
|
||||
isImageViewerFocused,
|
||||
],
|
||||
});
|
||||
|
||||
@@ -114,8 +104,8 @@ export const useGalleryHotkeys = () => {
|
||||
}
|
||||
handleUpImage(false);
|
||||
},
|
||||
options: { preventDefault: true, enabled: upDownHotkeysEnabled },
|
||||
dependencies: [handleUpImage, isOnFirstRow, goPrev, isPrevEnabled, queryResult.isFetching, upDownHotkeysEnabled],
|
||||
options: { preventDefault: true, enabled: isGalleryFocused },
|
||||
dependencies: [handleUpImage, isOnFirstRow, goPrev, isPrevEnabled, queryResult.isFetching, isGalleryFocused],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
@@ -128,8 +118,8 @@ export const useGalleryHotkeys = () => {
|
||||
}
|
||||
handleDownImage(false);
|
||||
},
|
||||
options: { preventDefault: true, enabled: upDownHotkeysEnabled },
|
||||
dependencies: [isOnLastRow, goNext, isNextEnabled, queryResult.isFetching, handleDownImage, upDownHotkeysEnabled],
|
||||
options: { preventDefault: true, enabled: isGalleryFocused },
|
||||
dependencies: [isOnLastRow, goNext, isNextEnabled, queryResult.isFetching, handleDownImage, isGalleryFocused],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
@@ -142,14 +132,15 @@ export const useGalleryHotkeys = () => {
|
||||
}
|
||||
handleLeftImage(true);
|
||||
},
|
||||
options: { preventDefault: true, enabled: leftRightHotkeysEnabled },
|
||||
options: { preventDefault: true, enabled: isGalleryFocused || isImageViewerFocused },
|
||||
dependencies: [
|
||||
handleLeftImage,
|
||||
isOnFirstImageOfView,
|
||||
goPrev,
|
||||
isPrevEnabled,
|
||||
queryResult.isFetching,
|
||||
leftRightHotkeysEnabled,
|
||||
isGalleryFocused,
|
||||
isImageViewerFocused,
|
||||
],
|
||||
});
|
||||
|
||||
@@ -165,14 +156,15 @@ export const useGalleryHotkeys = () => {
|
||||
handleRightImage(true);
|
||||
}
|
||||
},
|
||||
options: { preventDefault: true, enabled: leftRightHotkeysEnabled },
|
||||
options: { preventDefault: true, enabled: isGalleryFocused || isImageViewerFocused },
|
||||
dependencies: [
|
||||
isOnLastImageOfView,
|
||||
goNext,
|
||||
isNextEnabled,
|
||||
queryResult.isFetching,
|
||||
handleRightImage,
|
||||
leftRightHotkeysEnabled,
|
||||
isGalleryFocused,
|
||||
isImageViewerFocused,
|
||||
],
|
||||
});
|
||||
|
||||
@@ -186,8 +178,8 @@ export const useGalleryHotkeys = () => {
|
||||
}
|
||||
handleUpImage(true);
|
||||
},
|
||||
options: { preventDefault: true, enabled: upDownHotkeysEnabled },
|
||||
dependencies: [handleUpImage, isOnFirstRow, goPrev, isPrevEnabled, queryResult.isFetching, upDownHotkeysEnabled],
|
||||
options: { preventDefault: true, enabled: isGalleryFocused },
|
||||
dependencies: [handleUpImage, isOnFirstRow, goPrev, isPrevEnabled, queryResult.isFetching, isGalleryFocused],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
@@ -200,8 +192,8 @@ export const useGalleryHotkeys = () => {
|
||||
}
|
||||
handleDownImage(true);
|
||||
},
|
||||
options: { preventDefault: true, enabled: upDownHotkeysEnabled },
|
||||
dependencies: [isOnLastRow, goNext, isNextEnabled, queryResult.isFetching, handleDownImage, upDownHotkeysEnabled],
|
||||
options: { preventDefault: true, enabled: isGalleryFocused },
|
||||
dependencies: [isOnLastRow, goNext, isNextEnabled, queryResult.isFetching, handleDownImage, isGalleryFocused],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
@@ -213,7 +205,9 @@ export const useGalleryHotkeys = () => {
|
||||
}
|
||||
dispatch(imagesToDeleteSelected(selection));
|
||||
},
|
||||
options: { enabled: leftRightHotkeysEnabled && isDeleteEnabledByTab && !isWorkflowsScopeActive },
|
||||
dependencies: [leftRightHotkeysEnabled, isDeleteEnabledByTab, selection, isWorkflowsScopeActive],
|
||||
options: {
|
||||
enabled: (isGalleryFocused || isImageViewerFocused) && isDeleteEnabledByTab && !isWorkflowsFocused,
|
||||
},
|
||||
dependencies: [isWorkflowsFocused, isDeleteEnabledByTab, selection, isWorkflowsFocused],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { handlers, parseAndRecallAllMetadata, parseAndRecallPrompts } from 'features/metadata/util/handlers';
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
import {
|
||||
handlers,
|
||||
parseAndRecallAllMetadata,
|
||||
parseAndRecallImageDimensions,
|
||||
parseAndRecallPrompts,
|
||||
} from 'features/metadata/util/handlers';
|
||||
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
|
||||
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
|
||||
import {
|
||||
activeStylePresetIdChanged,
|
||||
@@ -9,22 +17,23 @@ import {
|
||||
} from 'features/stylePresets/store/stylePresetSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const useImageActions = (image_name?: string) => {
|
||||
export const useImageActions = (imageDTO: ImageDTO) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const activeTabName = useAppSelector(selectActiveTab);
|
||||
const { metadata, isLoading: isLoadingMetadata } = useDebouncedMetadata(image_name);
|
||||
const { metadata } = useDebouncedMetadata(imageDTO.image_name);
|
||||
const [hasMetadata, setHasMetadata] = useState(false);
|
||||
const [hasSeed, setHasSeed] = useState(false);
|
||||
const [hasPrompts, setHasPrompts] = useState(false);
|
||||
const { data: imageDTO } = useGetImageDTOQuery(image_name ?? skipToken);
|
||||
const hasTemplates = useStore($hasTemplates);
|
||||
|
||||
useEffect(() => {
|
||||
const parseMetadata = async () => {
|
||||
@@ -68,66 +77,107 @@ export const useImageActions = (image_name?: string) => {
|
||||
}, [dispatch, activeStylePresetId, t]);
|
||||
|
||||
const recallAll = useCallback(() => {
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
parseAndRecallAllMetadata(metadata, activeTabName === 'canvas', isStaging ? ['width', 'height'] : []);
|
||||
clearStylePreset();
|
||||
}, [metadata, activeTabName, isStaging, clearStylePreset]);
|
||||
|
||||
const remix = useCallback(() => {
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
// Recalls all metadata parameters except seed
|
||||
parseAndRecallAllMetadata(metadata, activeTabName === 'canvas', ['seed']);
|
||||
clearStylePreset();
|
||||
}, [activeTabName, metadata, clearStylePreset]);
|
||||
|
||||
const recallSeed = useCallback(() => {
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
handlers.seed.parse(metadata).then((seed) => {
|
||||
handlers.seed.recall && handlers.seed.recall(seed, true);
|
||||
});
|
||||
}, [metadata]);
|
||||
|
||||
const recallPrompts = useCallback(() => {
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
parseAndRecallPrompts(metadata);
|
||||
clearStylePreset();
|
||||
}, [metadata, clearStylePreset]);
|
||||
|
||||
const createAsPreset = useCallback(async () => {
|
||||
if (image_name && metadata && imageDTO) {
|
||||
let positivePrompt;
|
||||
let negativePrompt;
|
||||
|
||||
try {
|
||||
positivePrompt = await handlers.positivePrompt.parse(metadata);
|
||||
} catch (error) {
|
||||
positivePrompt = '';
|
||||
}
|
||||
try {
|
||||
negativePrompt = await handlers.negativePrompt.parse(metadata);
|
||||
} catch (error) {
|
||||
negativePrompt = '';
|
||||
}
|
||||
|
||||
$stylePresetModalState.set({
|
||||
prefilledFormData: {
|
||||
name: '',
|
||||
positivePrompt,
|
||||
negativePrompt,
|
||||
imageUrl: imageDTO.image_url,
|
||||
type: 'user',
|
||||
},
|
||||
updatingStylePresetId: null,
|
||||
isModalOpen: true,
|
||||
});
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
}, [image_name, metadata, imageDTO]);
|
||||
let positivePrompt;
|
||||
let negativePrompt;
|
||||
|
||||
try {
|
||||
positivePrompt = await handlers.positivePrompt.parse(metadata);
|
||||
} catch (error) {
|
||||
positivePrompt = '';
|
||||
}
|
||||
try {
|
||||
negativePrompt = await handlers.negativePrompt.parse(metadata);
|
||||
} catch (error) {
|
||||
negativePrompt = '';
|
||||
}
|
||||
|
||||
$stylePresetModalState.set({
|
||||
prefilledFormData: {
|
||||
name: '',
|
||||
positivePrompt,
|
||||
negativePrompt,
|
||||
imageUrl: imageDTO.image_url,
|
||||
type: 'user',
|
||||
},
|
||||
updatingStylePresetId: null,
|
||||
isModalOpen: true,
|
||||
});
|
||||
}, [metadata, imageDTO]);
|
||||
|
||||
const [getAndLoadEmbeddedWorkflow] = useGetAndLoadEmbeddedWorkflow();
|
||||
|
||||
const loadWorkflow = useCallback(() => {
|
||||
if (!imageDTO.has_workflow || !hasTemplates) {
|
||||
return;
|
||||
}
|
||||
getAndLoadEmbeddedWorkflow(imageDTO.image_name);
|
||||
}, [getAndLoadEmbeddedWorkflow, hasTemplates, imageDTO.has_workflow, imageDTO.image_name]);
|
||||
|
||||
const recallSize = useCallback(() => {
|
||||
if (isStaging) {
|
||||
return;
|
||||
}
|
||||
parseAndRecallImageDimensions(imageDTO);
|
||||
}, [imageDTO, isStaging]);
|
||||
|
||||
const upscale = useCallback(() => {
|
||||
dispatch(adHocPostProcessingRequested({ imageDTO }));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
const _delete = useCallback(() => {
|
||||
dispatch(imagesToDeleteSelected([imageDTO]));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
return {
|
||||
hasMetadata,
|
||||
hasSeed,
|
||||
hasPrompts,
|
||||
recallAll,
|
||||
remix,
|
||||
recallSeed,
|
||||
recallPrompts,
|
||||
hasMetadata,
|
||||
hasSeed,
|
||||
hasPrompts,
|
||||
isLoadingMetadata,
|
||||
createAsPreset,
|
||||
loadWorkflow,
|
||||
hasWorkflow: imageDTO.has_workflow,
|
||||
recallSize,
|
||||
upscale,
|
||||
delete: _delete,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { MainModelConfig } from 'services/api/types';
|
||||
|
||||
const initialStatesSelector = createMemoizedSelector(selectConfigSlice, (config) => {
|
||||
const { steps, guidance, scheduler, cfgRescaleMultiplier, vaePrecision, width, height } = config.sd;
|
||||
const { guidance: fluxGuidance } = config.flux;
|
||||
|
||||
return {
|
||||
initialSteps: steps.initial,
|
||||
@@ -16,6 +17,7 @@ const initialStatesSelector = createMemoizedSelector(selectConfigSlice, (config)
|
||||
initialVaePrecision: vaePrecision,
|
||||
initialWidth: width.initial,
|
||||
initialHeight: height.initial,
|
||||
initialGuidance: fluxGuidance.initial,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -28,6 +30,7 @@ export const useMainModelDefaultSettings = (modelConfig: MainModelConfig) => {
|
||||
initialVaePrecision,
|
||||
initialWidth,
|
||||
initialHeight,
|
||||
initialGuidance,
|
||||
} = useAppSelector(initialStatesSelector);
|
||||
|
||||
const defaultSettingsDefaults = useMemo(() => {
|
||||
@@ -64,6 +67,10 @@ export const useMainModelDefaultSettings = (modelConfig: MainModelConfig) => {
|
||||
isEnabled: !isNil(modelConfig?.default_settings?.height),
|
||||
value: modelConfig?.default_settings?.height || initialHeight,
|
||||
},
|
||||
guidance: {
|
||||
isEnabled: !isNil(modelConfig?.default_settings?.guidance),
|
||||
value: modelConfig?.default_settings?.guidance || initialGuidance,
|
||||
},
|
||||
};
|
||||
}, [
|
||||
modelConfig,
|
||||
@@ -74,6 +81,7 @@ export const useMainModelDefaultSettings = (modelConfig: MainModelConfig) => {
|
||||
initialCfgRescaleMultiplier,
|
||||
initialWidth,
|
||||
initialHeight,
|
||||
initialGuidance,
|
||||
]);
|
||||
|
||||
return defaultSettingsDefaults;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createSelector, createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import type { ModelType } from 'services/api/types';
|
||||
|
||||
export type FilterableModelType = Exclude<ModelType, 'onnx' | 'clip_vision'> | 'refiner';
|
||||
export type FilterableModelType = Exclude<ModelType, 'onnx'> | 'refiner';
|
||||
|
||||
type ModelManagerState = {
|
||||
_version: 1;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
useCLIPEmbedModels,
|
||||
useCLIPVisionModels,
|
||||
useControlNetModels,
|
||||
useEmbeddingModels,
|
||||
useIPAdapterModels,
|
||||
@@ -73,6 +74,12 @@ const ModelList = () => {
|
||||
[ipAdapterModels, searchTerm, filteredModelType]
|
||||
);
|
||||
|
||||
const [clipVisionModels, { isLoading: isLoadingCLIPVisionModels }] = useCLIPVisionModels();
|
||||
const filteredCLIPVisionModels = useMemo(
|
||||
() => modelsFilter(clipVisionModels, searchTerm, filteredModelType),
|
||||
[clipVisionModels, searchTerm, filteredModelType]
|
||||
);
|
||||
|
||||
const [vaeModels, { isLoading: isLoadingVAEModels }] = useVAEModels();
|
||||
const filteredVAEModels = useMemo(
|
||||
() => modelsFilter(vaeModels, searchTerm, filteredModelType),
|
||||
@@ -107,6 +114,7 @@ const ModelList = () => {
|
||||
filteredControlNetModels.length +
|
||||
filteredT2IAdapterModels.length +
|
||||
filteredIPAdapterModels.length +
|
||||
filteredCLIPVisionModels.length +
|
||||
filteredVAEModels.length +
|
||||
filteredSpandrelImageToImageModels.length +
|
||||
t5EncoderModels.length +
|
||||
@@ -116,6 +124,7 @@ const ModelList = () => {
|
||||
filteredControlNetModels.length,
|
||||
filteredEmbeddingModels.length,
|
||||
filteredIPAdapterModels.length,
|
||||
filteredCLIPVisionModels.length,
|
||||
filteredLoRAModels.length,
|
||||
filteredMainModels.length,
|
||||
filteredRefinerModels.length,
|
||||
@@ -171,6 +180,11 @@ const ModelList = () => {
|
||||
{!isLoadingIPAdapterModels && filteredIPAdapterModels.length > 0 && (
|
||||
<ModelListWrapper title={t('common.ipAdapter')} modelList={filteredIPAdapterModels} key="ip-adapters" />
|
||||
)}
|
||||
{/* CLIP Vision List */}
|
||||
{isLoadingCLIPVisionModels && <FetchingModelsLoader loadingMessage="Loading CLIP Vision Models..." />}
|
||||
{!isLoadingCLIPVisionModels && filteredCLIPVisionModels.length > 0 && (
|
||||
<ModelListWrapper title="CLIP Vision" modelList={filteredCLIPVisionModels} key="clip-vision" />
|
||||
)}
|
||||
{/* T2I Adapters List */}
|
||||
{isLoadingT2IAdapterModels && <FetchingModelsLoader loadingMessage="Loading T2I Adapters..." />}
|
||||
{!isLoadingT2IAdapterModels && filteredT2IAdapterModels.length > 0 && (
|
||||
|
||||
@@ -22,6 +22,7 @@ export const ModelTypeFilter = memo(() => {
|
||||
t5_encoder: t('modelManager.t5Encoder'),
|
||||
clip_embed: t('modelManager.clipEmbed'),
|
||||
ip_adapter: t('common.ipAdapter'),
|
||||
clip_vision: 'CLIP Vision',
|
||||
spandrel_image_to_image: t('modelManager.spandrelImageToImage'),
|
||||
}),
|
||||
[t]
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle';
|
||||
import { selectGuidanceConfig } from 'features/system/store/configSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import type { UseControllerProps } from 'react-hook-form';
|
||||
import { useController } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { MainModelDefaultSettingsFormData } from './MainModelDefaultSettings';
|
||||
|
||||
type DefaultGuidanceType = MainModelDefaultSettingsFormData['guidance'];
|
||||
|
||||
export const DefaultGuidance = memo((props: UseControllerProps<MainModelDefaultSettingsFormData>) => {
|
||||
const { field } = useController(props);
|
||||
|
||||
const config = useAppSelector(selectGuidanceConfig);
|
||||
const { t } = useTranslation();
|
||||
const marks = useMemo(
|
||||
() => [
|
||||
config.sliderMin,
|
||||
Math.floor(config.sliderMax - (config.sliderMax - config.sliderMin) / 2),
|
||||
config.sliderMax,
|
||||
],
|
||||
[config.sliderMax, config.sliderMin]
|
||||
);
|
||||
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
const updatedValue = {
|
||||
...(field.value as DefaultGuidanceType),
|
||||
value: v,
|
||||
};
|
||||
field.onChange(updatedValue);
|
||||
},
|
||||
[field]
|
||||
);
|
||||
|
||||
const value = useMemo(() => {
|
||||
return (field.value as DefaultGuidanceType).value;
|
||||
}, [field.value]);
|
||||
|
||||
const isDisabled = useMemo(() => {
|
||||
return !(field.value as DefaultGuidanceType).isEnabled;
|
||||
}, [field.value]);
|
||||
|
||||
return (
|
||||
<FormControl flexDir="column" gap={2} alignItems="flex-start">
|
||||
<Flex justifyContent="space-between" w="full">
|
||||
<InformationalPopover feature="paramGuidance">
|
||||
<FormLabel>{t('parameters.guidance')}</FormLabel>
|
||||
</InformationalPopover>
|
||||
<SettingToggle control={props.control} name="guidance" />
|
||||
</Flex>
|
||||
|
||||
<Flex w="full" gap={4}>
|
||||
<CompositeSlider
|
||||
value={value}
|
||||
min={config.sliderMin}
|
||||
max={config.sliderMax}
|
||||
step={config.coarseStep}
|
||||
fineStep={config.fineStep}
|
||||
onChange={onChange}
|
||||
marks={marks}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
<CompositeNumberInput
|
||||
value={value}
|
||||
min={config.numberInputMin}
|
||||
max={config.numberInputMax}
|
||||
step={config.coarseStep}
|
||||
fineStep={config.fineStep}
|
||||
onChange={onChange}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
|
||||
DefaultGuidance.displayName = 'DefaultGuidance';
|
||||
@@ -17,6 +17,7 @@ import type { MainModelConfig } from 'services/api/types';
|
||||
|
||||
import { DefaultCfgRescaleMultiplier } from './DefaultCfgRescaleMultiplier';
|
||||
import { DefaultCfgScale } from './DefaultCfgScale';
|
||||
import { DefaultGuidance } from './DefaultGuidance';
|
||||
import { DefaultScheduler } from './DefaultScheduler';
|
||||
import { DefaultSteps } from './DefaultSteps';
|
||||
import { DefaultVae } from './DefaultVae';
|
||||
@@ -36,6 +37,7 @@ export type MainModelDefaultSettingsFormData = {
|
||||
cfgRescaleMultiplier: FormField<number>;
|
||||
width: FormField<number>;
|
||||
height: FormField<number>;
|
||||
guidance: FormField<number>;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@@ -46,6 +48,10 @@ export const MainModelDefaultSettings = memo(({ modelConfig }: Props) => {
|
||||
const selectedModelKey = useAppSelector(selectSelectedModelKey);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isFlux = useMemo(() => {
|
||||
return modelConfig.base === 'flux';
|
||||
}, [modelConfig]);
|
||||
|
||||
const defaultSettingsDefaults = useMainModelDefaultSettings(modelConfig);
|
||||
const optimalDimension = useMemo(() => {
|
||||
const modelBase = modelConfig?.base;
|
||||
@@ -72,6 +78,7 @@ export const MainModelDefaultSettings = memo(({ modelConfig }: Props) => {
|
||||
scheduler: data.scheduler.isEnabled ? data.scheduler.value : null,
|
||||
width: data.width.isEnabled ? data.width.value : null,
|
||||
height: data.height.isEnabled ? data.height.value : null,
|
||||
guidance: data.guidance.isEnabled ? data.guidance.value : null,
|
||||
};
|
||||
|
||||
updateModel({
|
||||
@@ -118,11 +125,12 @@ export const MainModelDefaultSettings = memo(({ modelConfig }: Props) => {
|
||||
|
||||
<SimpleGrid columns={2} gap={8}>
|
||||
<DefaultVae control={control} name="vae" />
|
||||
<DefaultVaePrecision control={control} name="vaePrecision" />
|
||||
<DefaultScheduler control={control} name="scheduler" />
|
||||
{!isFlux && <DefaultVaePrecision control={control} name="vaePrecision" />}
|
||||
{!isFlux && <DefaultScheduler control={control} name="scheduler" />}
|
||||
<DefaultSteps control={control} name="steps" />
|
||||
<DefaultCfgScale control={control} name="cfgScale" />
|
||||
<DefaultCfgRescaleMultiplier control={control} name="cfgRescaleMultiplier" />
|
||||
{isFlux && <DefaultGuidance control={control} name="guidance" />}
|
||||
{!isFlux && <DefaultCfgScale control={control} name="cfgScale" />}
|
||||
{!isFlux && <DefaultCfgRescaleMultiplier control={control} name="cfgRescaleMultiplier" />}
|
||||
<DefaultWidth control={control} optimalDimension={optimalDimension} />
|
||||
<DefaultHeight control={control} optimalDimension={optimalDimension} />
|
||||
</SimpleGrid>
|
||||
|
||||
@@ -120,14 +120,18 @@ export const ModelEdit = memo(({ modelConfig }: Props) => {
|
||||
<Textarea {...form.register('description')} minH={32} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
<Heading as="h3" fontSize="md" mt="4">
|
||||
{t('modelManager.modelSettings')}
|
||||
</Heading>
|
||||
{modelConfig.type !== 'clip_vision' && (
|
||||
<Heading as="h3" fontSize="md" mt="4">
|
||||
{t('modelManager.modelSettings')}
|
||||
</Heading>
|
||||
)}
|
||||
<SimpleGrid columns={2} gap={4}>
|
||||
<FormControl flexDir="column" alignItems="flex-start" gap={1}>
|
||||
<FormLabel>{t('modelManager.baseModel')}</FormLabel>
|
||||
<BaseModelSelect control={form.control} />
|
||||
</FormControl>
|
||||
{modelConfig.type !== 'clip_vision' && (
|
||||
<FormControl flexDir="column" alignItems="flex-start" gap={1}>
|
||||
<FormLabel>{t('modelManager.baseModel')}</FormLabel>
|
||||
<BaseModelSelect control={form.control} />
|
||||
</FormControl>
|
||||
)}
|
||||
{modelConfig.type === 'main' && (
|
||||
<FormControl flexDir="column" alignItems="flex-start" gap={1}>
|
||||
<FormLabel>{t('modelManager.variant')}</FormLabel>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ModelConvertButton } from 'features/modelManagerV2/subpanels/ModelPanel
|
||||
import { ModelEditButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelEditButton';
|
||||
import { ModelHeader } from 'features/modelManagerV2/subpanels/ModelPanel/ModelHeader';
|
||||
import { TriggerPhrases } from 'features/modelManagerV2/subpanels/ModelPanel/TriggerPhrases';
|
||||
import { memo } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { AnyModelConfig } from 'services/api/types';
|
||||
|
||||
@@ -17,6 +17,20 @@ type Props = {
|
||||
|
||||
export const ModelView = memo(({ modelConfig }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const withSettings = useMemo(() => {
|
||||
if (modelConfig.type === 'main' && modelConfig.base !== 'sdxl-refiner') {
|
||||
return true;
|
||||
}
|
||||
if (modelConfig.type === 'controlnet' || modelConfig.type === 't2i_adapter') {
|
||||
return true;
|
||||
}
|
||||
if (modelConfig.type === 'main' || modelConfig.type === 'lora') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [modelConfig.base, modelConfig.type]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<ModelHeader modelConfig={modelConfig}>
|
||||
@@ -50,15 +64,19 @@ export const ModelView = memo(({ modelConfig }: Props) => {
|
||||
)}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
<Box layerStyle="second" borderRadius="base" p={4}>
|
||||
{modelConfig.type === 'main' && modelConfig.base !== 'sdxl-refiner' && (
|
||||
<MainModelDefaultSettings modelConfig={modelConfig} />
|
||||
)}
|
||||
{(modelConfig.type === 'controlnet' || modelConfig.type === 't2i_adapter') && (
|
||||
<ControlNetOrT2IAdapterDefaultSettings modelConfig={modelConfig} />
|
||||
)}
|
||||
{(modelConfig.type === 'main' || modelConfig.type === 'lora') && <TriggerPhrases modelConfig={modelConfig} />}
|
||||
</Box>
|
||||
{withSettings && (
|
||||
<Box layerStyle="second" borderRadius="base" p={4}>
|
||||
{modelConfig.type === 'main' && modelConfig.base !== 'sdxl-refiner' && (
|
||||
<MainModelDefaultSettings modelConfig={modelConfig} />
|
||||
)}
|
||||
{(modelConfig.type === 'controlnet' || modelConfig.type === 't2i_adapter') && (
|
||||
<ControlNetOrT2IAdapterDefaultSettings modelConfig={modelConfig} />
|
||||
)}
|
||||
{(modelConfig.type === 'main' || modelConfig.type === 'lora') && (
|
||||
<TriggerPhrases modelConfig={modelConfig} />
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'reactflow/dist/style.css';
|
||||
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import { useFocusRegion } from 'common/hooks/focus';
|
||||
import { AddNodeCmdk } from 'features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk';
|
||||
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
|
||||
import WorkflowEditorSettings from 'features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings';
|
||||
@@ -21,7 +21,7 @@ const NodeEditor = () => {
|
||||
const { data, isLoading } = useGetOpenAPISchemaQuery();
|
||||
const { t } = useTranslation();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useScopeOnFocus('workflows', ref);
|
||||
useFocusRegion('workflows', ref);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useGlobalMenuClose, useToken } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { INTERACTION_SCOPES, useScopeImperativeApi, useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useConnection } from 'features/nodes/hooks/useConnection';
|
||||
import { useCopyPaste } from 'features/nodes/hooks/useCopyPaste';
|
||||
import { useSyncExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||
@@ -89,9 +89,8 @@ export const Flow = memo(() => {
|
||||
const cancelConnection = useReactFlowStore(selectCancelConnection);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const store = useAppStore();
|
||||
const isWorkflowsActive = useStore(INTERACTION_SCOPES.workflows.$isActive);
|
||||
const workflowsScopeApi = useScopeImperativeApi('workflows');
|
||||
useScopeOnFocus('workflows', flowWrapper);
|
||||
const isWorkflowsFocused = useIsRegionFocused('workflows');
|
||||
useFocusRegion('workflows', flowWrapper);
|
||||
|
||||
useWorkflowWatcher();
|
||||
useSyncExecutionState();
|
||||
@@ -129,8 +128,7 @@ export const Flow = memo(() => {
|
||||
const { onCloseGlobal } = useGlobalMenuClose();
|
||||
const handlePaneClick = useCallback(() => {
|
||||
onCloseGlobal();
|
||||
workflowsScopeApi.add();
|
||||
}, [onCloseGlobal, workflowsScopeApi]);
|
||||
}, [onCloseGlobal]);
|
||||
|
||||
const onInit: OnInit = useCallback((flow) => {
|
||||
$flow.set(flow);
|
||||
@@ -245,8 +243,8 @@ export const Flow = memo(() => {
|
||||
id: 'selectAll',
|
||||
category: 'workflows',
|
||||
callback: selectAll,
|
||||
options: { enabled: isWorkflowsActive, preventDefault: true },
|
||||
dependencies: [selectAll, isWorkflowsActive],
|
||||
options: { enabled: isWorkflowsFocused, preventDefault: true },
|
||||
dependencies: [selectAll, isWorkflowsFocused],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
@@ -319,8 +317,8 @@ export const Flow = memo(() => {
|
||||
id: 'deleteSelection',
|
||||
category: 'workflows',
|
||||
callback: deleteSelection,
|
||||
options: { preventDefault: true, enabled: isWorkflowsActive },
|
||||
dependencies: [deleteSelection, isWorkflowsActive],
|
||||
options: { preventDefault: true, enabled: isWorkflowsFocused },
|
||||
dependencies: [deleteSelection, isWorkflowsFocused],
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,7 +8,8 @@ export const addFLUXLoRAs = (
|
||||
state: RootState,
|
||||
g: Graph,
|
||||
denoise: Invocation<'flux_denoise'>,
|
||||
modelLoader: Invocation<'flux_model_loader'>
|
||||
modelLoader: Invocation<'flux_model_loader'>,
|
||||
fluxTextEncoder: Invocation<'flux_text_encoder'>
|
||||
): void => {
|
||||
const enabledLoRAs = state.loras.loras.filter((l) => l.isEnabled && l.model.base === 'flux');
|
||||
const loraCount = enabledLoRAs.length;
|
||||
@@ -20,7 +21,7 @@ export const addFLUXLoRAs = (
|
||||
const loraMetadata: S['LoRAMetadataField'][] = [];
|
||||
|
||||
// We will collect LoRAs into a single collection node, then pass them to the LoRA collection loader, which applies
|
||||
// each LoRA to the UNet and CLIP.
|
||||
// each LoRA to the transformer and text encoders.
|
||||
const loraCollector = g.addNode({
|
||||
id: getPrefixedId('lora_collector'),
|
||||
type: 'collect',
|
||||
@@ -33,10 +34,12 @@ export const addFLUXLoRAs = (
|
||||
g.addEdge(loraCollector, 'collection', loraCollectionLoader, 'loras');
|
||||
// Use model loader as transformer input
|
||||
g.addEdge(modelLoader, 'transformer', loraCollectionLoader, 'transformer');
|
||||
// Reroute transformer connections through the LoRA collection loader
|
||||
g.addEdge(modelLoader, 'clip', loraCollectionLoader, 'clip');
|
||||
// Reroute model connections through the LoRA collection loader
|
||||
g.deleteEdgesTo(denoise, ['transformer']);
|
||||
|
||||
g.deleteEdgesTo(fluxTextEncoder, ['clip']);
|
||||
g.addEdge(loraCollectionLoader, 'transformer', denoise, 'transformer');
|
||||
g.addEdge(loraCollectionLoader, 'clip', fluxTextEncoder, 'clip');
|
||||
|
||||
for (const lora of enabledLoRAs) {
|
||||
const { weight } = lora;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSe
|
||||
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
|
||||
import { addFLUXLoRAs } from 'features/nodes/util/graph/generation/addFLUXLoRAs';
|
||||
import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage';
|
||||
import { addInpaint } from 'features/nodes/util/graph/generation/addInpaint';
|
||||
import { addNSFWChecker } from 'features/nodes/util/graph/generation/addNSFWChecker';
|
||||
@@ -18,8 +19,6 @@ import type { Invocation } from 'services/api/types';
|
||||
import { isNonRefinerMainModelConfig } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
import { addFLUXLoRAs } from './addFLUXLoRAs';
|
||||
|
||||
const log = logger('system');
|
||||
|
||||
export const buildFLUXGraph = async (
|
||||
@@ -96,12 +95,12 @@ export const buildFLUXGraph = async (
|
||||
g.addEdge(modelLoader, 'transformer', noise, 'transformer');
|
||||
g.addEdge(modelLoader, 'vae', l2i, 'vae');
|
||||
|
||||
addFLUXLoRAs(state, g, noise, modelLoader);
|
||||
|
||||
g.addEdge(modelLoader, 'clip', posCond, 'clip');
|
||||
g.addEdge(modelLoader, 't5_encoder', posCond, 't5_encoder');
|
||||
g.addEdge(modelLoader, 'max_seq_len', posCond, 't5_max_seq_len');
|
||||
|
||||
addFLUXLoRAs(state, g, noise, modelLoader, posCond);
|
||||
|
||||
g.addEdge(posCond, 'conditioning', noise, 'positive_text_conditioning');
|
||||
|
||||
g.addEdge(noise, 'latents', l2i, 'latents');
|
||||
|
||||
@@ -59,6 +59,8 @@ export const isParameterCFGScale = (val: unknown): val is ParameterCFGScale =>
|
||||
// #region Guidance parameter
|
||||
const zParameterGuidance = z.number().min(1);
|
||||
export type ParameterGuidance = z.infer<typeof zParameterGuidance>;
|
||||
export const isParameterGuidance = (val: unknown): val is ParameterGuidance =>
|
||||
zParameterGuidance.safeParse(val).success;
|
||||
// #endregion
|
||||
|
||||
// #region CFG Rescale Multiplier
|
||||
|
||||
@@ -115,6 +115,10 @@ export const useHotkeyData = (): HotkeysData => {
|
||||
addHotkey('canvas', 'redo', ['mod+shift+z', 'mod+y']);
|
||||
addHotkey('canvas', 'nextEntity', ['alt+]']);
|
||||
addHotkey('canvas', 'prevEntity', ['alt+[']);
|
||||
addHotkey('canvas', 'applyFilter', ['enter']);
|
||||
addHotkey('canvas', 'cancelFilter', ['esc']);
|
||||
addHotkey('canvas', 'applyTransform', ['enter']);
|
||||
addHotkey('canvas', 'cancelTransform', ['esc']);
|
||||
|
||||
// Workflows
|
||||
addHotkey('workflows', 'addNode', ['shift+a', 'space']);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import { CanvasMainPanelContent } from 'features/controlLayers/components/CanvasMainPanelContent';
|
||||
import { CanvasRightPanel } from 'features/controlLayers/components/CanvasRightPanel';
|
||||
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
|
||||
@@ -40,9 +39,6 @@ const onLeftPanelCollapse = (isCollapsed: boolean) => $isLeftPanelOpen.set(!isCo
|
||||
const onRightPanelCollapse = (isCollapsed: boolean) => $isRightPanelOpen.set(!isCollapsed);
|
||||
|
||||
export const AppContent = memo(() => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useScopeOnFocus('gallery', ref);
|
||||
|
||||
const imperativePanelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||
|
||||
const withLeftPanel = useAppSelector(selectWithLeftPanel);
|
||||
@@ -119,7 +115,7 @@ export const AppContent = memo(() => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex ref={ref} id="invoke-app-tabs" w="full" h="full" gap={4} p={4}>
|
||||
<Flex id="invoke-app-tabs" w="full" h="full" gap={4} p={4}>
|
||||
<VerticalNavBar />
|
||||
<Flex position="relative" w="full" h="full" gap={4} minW={0}>
|
||||
<PanelGroup
|
||||
|
||||
@@ -10,19 +10,10 @@ type UseGetAndLoadEmbeddedWorkflowOptions = {
|
||||
onError?: () => void;
|
||||
};
|
||||
|
||||
type UseGetAndLoadEmbeddedWorkflowReturn = {
|
||||
getAndLoadEmbeddedWorkflow: (imageName: string) => Promise<void>;
|
||||
getAndLoadEmbeddedWorkflowResult: ReturnType<typeof useLazyGetImageWorkflowQuery>[1];
|
||||
};
|
||||
|
||||
type UseGetAndLoadEmbeddedWorkflow = (
|
||||
options?: UseGetAndLoadEmbeddedWorkflowOptions
|
||||
) => UseGetAndLoadEmbeddedWorkflowReturn;
|
||||
|
||||
export const useGetAndLoadEmbeddedWorkflow: UseGetAndLoadEmbeddedWorkflow = (options) => {
|
||||
export const useGetAndLoadEmbeddedWorkflow = (options?: UseGetAndLoadEmbeddedWorkflowOptions) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const [_getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult] = useLazyGetImageWorkflowQuery();
|
||||
const [_getAndLoadEmbeddedWorkflow, result] = useLazyGetImageWorkflowQuery();
|
||||
const getAndLoadEmbeddedWorkflow = useCallback(
|
||||
async (imageName: string) => {
|
||||
try {
|
||||
@@ -50,5 +41,5 @@ export const useGetAndLoadEmbeddedWorkflow: UseGetAndLoadEmbeddedWorkflow = (opt
|
||||
[_getAndLoadEmbeddedWorkflow, dispatch, options, t]
|
||||
);
|
||||
|
||||
return { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult };
|
||||
return [getAndLoadEmbeddedWorkflow, result] as const;
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import type { AnyModelConfig } from 'services/api/types';
|
||||
import {
|
||||
isCLIPEmbedModelConfig,
|
||||
isCLIPVisionModelConfig,
|
||||
isControlNetModelConfig,
|
||||
isControlNetOrT2IAdapterModelConfig,
|
||||
isFluxMainModelModelConfig,
|
||||
@@ -58,6 +59,7 @@ export const useIPAdapterModels = buildModelsHook(isIPAdapterModelConfig);
|
||||
export const useEmbeddingModels = buildModelsHook(isTIModelConfig);
|
||||
export const useVAEModels = buildModelsHook(isVAEModelConfig);
|
||||
export const useFluxVAEModels = buildModelsHook(isFluxVAEModelConfig);
|
||||
export const useCLIPVisionModels = buildModelsHook(isCLIPVisionModelConfig);
|
||||
|
||||
// const buildModelsSelector =
|
||||
// <T extends AnyModelConfig>(typeGuard: (config: AnyModelConfig) => config is T): Selector<RootState, T[]> =>
|
||||
|
||||
@@ -5707,6 +5707,12 @@ export type components = {
|
||||
* @default null
|
||||
*/
|
||||
transformer?: components["schemas"]["TransformerField"] | null;
|
||||
/**
|
||||
* CLIP
|
||||
* @description CLIP (tokenizer, text encoder, LoRAs) and skipped layer count
|
||||
* @default null
|
||||
*/
|
||||
clip?: components["schemas"]["CLIPField"] | null;
|
||||
/**
|
||||
* type
|
||||
* @default flux_lora_collection_loader
|
||||
@@ -6391,7 +6397,7 @@ export type components = {
|
||||
};
|
||||
/**
|
||||
* FLUX LoRA
|
||||
* @description Apply a LoRA model to a FLUX transformer.
|
||||
* @description Apply a LoRA model to a FLUX transformer and/or text encoder.
|
||||
*/
|
||||
FluxLoRALoaderInvocation: {
|
||||
/**
|
||||
@@ -6428,7 +6434,13 @@ export type components = {
|
||||
* @description Transformer
|
||||
* @default null
|
||||
*/
|
||||
transformer?: components["schemas"]["TransformerField"];
|
||||
transformer?: components["schemas"]["TransformerField"] | null;
|
||||
/**
|
||||
* CLIP
|
||||
* @description CLIP (tokenizer, text encoder, LoRAs) and skipped layer count
|
||||
* @default null
|
||||
*/
|
||||
clip?: components["schemas"]["CLIPField"] | null;
|
||||
/**
|
||||
* type
|
||||
* @default flux_lora_loader
|
||||
@@ -6448,6 +6460,12 @@ export type components = {
|
||||
* @default null
|
||||
*/
|
||||
transformer: components["schemas"]["TransformerField"] | null;
|
||||
/**
|
||||
* CLIP
|
||||
* @description CLIP (tokenizer, text encoder, LoRAs) and skipped layer count
|
||||
* @default null
|
||||
*/
|
||||
clip: components["schemas"]["CLIPField"] | null;
|
||||
/**
|
||||
* type
|
||||
* @default flux_lora_loader_output
|
||||
@@ -11019,6 +11037,11 @@ export type components = {
|
||||
* @description Default height for this model
|
||||
*/
|
||||
height?: number | null;
|
||||
/**
|
||||
* Guidance
|
||||
* @description Default Guidance for this model
|
||||
*/
|
||||
guidance?: number | null;
|
||||
};
|
||||
/**
|
||||
* Main Model
|
||||
|
||||
@@ -99,6 +99,10 @@ export const isIPAdapterModelConfig = (config: AnyModelConfig): config is IPAdap
|
||||
return config.type === 'ip_adapter';
|
||||
};
|
||||
|
||||
export const isCLIPVisionModelConfig = (config: AnyModelConfig): config is CLIPVisionDiffusersConfig => {
|
||||
return config.type === 'clip_vision';
|
||||
};
|
||||
|
||||
export const isT2IAdapterModelConfig = (config: AnyModelConfig): config is T2IAdapterModelConfig => {
|
||||
return config.type === 't2i_adapter';
|
||||
};
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "5.0.0"
|
||||
__version__ = "5.0.2"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ from invokeai.backend.lora.conversions.flux_diffusers_lora_conversion_utils impo
|
||||
is_state_dict_likely_in_flux_diffusers_format,
|
||||
lora_model_from_flux_diffusers_state_dict,
|
||||
)
|
||||
from invokeai.backend.lora.conversions.flux_lora_constants import FLUX_LORA_TRANSFORMER_PREFIX
|
||||
from tests.backend.lora.conversions.lora_state_dicts.flux_lora_diffusers_format import (
|
||||
state_dict_keys as flux_diffusers_state_dict_keys,
|
||||
)
|
||||
@@ -50,6 +51,7 @@ def test_lora_model_from_flux_diffusers_state_dict():
|
||||
concatenated_weights = ["to_k", "to_v", "proj_mlp", "add_k_proj", "add_v_proj"]
|
||||
expected_lora_layers = {k for k in expected_lora_layers if not any(w in k for w in concatenated_weights)}
|
||||
assert len(model.layers) == len(expected_lora_layers)
|
||||
assert all(k.startswith(FLUX_LORA_TRANSFORMER_PREFIX) for k in model.layers.keys())
|
||||
|
||||
|
||||
def test_lora_model_from_flux_diffusers_state_dict_extra_keys_error():
|
||||
|
||||
@@ -5,23 +5,28 @@ import torch
|
||||
from invokeai.backend.flux.model import Flux
|
||||
from invokeai.backend.flux.util import params
|
||||
from invokeai.backend.lora.conversions.flux_kohya_lora_conversion_utils import (
|
||||
convert_flux_kohya_state_dict_to_invoke_format,
|
||||
_convert_flux_transformer_kohya_state_dict_to_invoke_format,
|
||||
is_state_dict_likely_in_flux_kohya_format,
|
||||
lora_model_from_flux_kohya_state_dict,
|
||||
)
|
||||
from invokeai.backend.lora.conversions.flux_lora_constants import FLUX_LORA_CLIP_PREFIX, FLUX_LORA_TRANSFORMER_PREFIX
|
||||
from tests.backend.lora.conversions.lora_state_dicts.flux_lora_diffusers_format import (
|
||||
state_dict_keys as flux_diffusers_state_dict_keys,
|
||||
)
|
||||
from tests.backend.lora.conversions.lora_state_dicts.flux_lora_kohya_format import (
|
||||
state_dict_keys as flux_kohya_state_dict_keys,
|
||||
)
|
||||
from tests.backend.lora.conversions.lora_state_dicts.flux_lora_kohya_with_te1_format import (
|
||||
state_dict_keys as flux_kohya_te1_state_dict_keys,
|
||||
)
|
||||
from tests.backend.lora.conversions.lora_state_dicts.utils import keys_to_mock_state_dict
|
||||
|
||||
|
||||
def test_is_state_dict_likely_in_flux_kohya_format_true():
|
||||
@pytest.mark.parametrize("sd_keys", [flux_kohya_state_dict_keys, flux_kohya_te1_state_dict_keys])
|
||||
def test_is_state_dict_likely_in_flux_kohya_format_true(sd_keys: list[str]):
|
||||
"""Test that is_state_dict_likely_in_flux_kohya_format() can identify a state dict in the Kohya FLUX LoRA format."""
|
||||
# Construct a state dict that is in the Kohya FLUX LoRA format.
|
||||
state_dict = keys_to_mock_state_dict(flux_kohya_state_dict_keys)
|
||||
state_dict = keys_to_mock_state_dict(sd_keys)
|
||||
|
||||
assert is_state_dict_likely_in_flux_kohya_format(state_dict)
|
||||
|
||||
@@ -34,11 +39,11 @@ def test_is_state_dict_likely_in_flux_kohya_format_false():
|
||||
assert not is_state_dict_likely_in_flux_kohya_format(state_dict)
|
||||
|
||||
|
||||
def test_convert_flux_kohya_state_dict_to_invoke_format():
|
||||
def test_convert_flux_transformer_kohya_state_dict_to_invoke_format():
|
||||
# Construct state_dict from state_dict_keys.
|
||||
state_dict = keys_to_mock_state_dict(flux_kohya_state_dict_keys)
|
||||
|
||||
converted_state_dict = convert_flux_kohya_state_dict_to_invoke_format(state_dict)
|
||||
converted_state_dict = _convert_flux_transformer_kohya_state_dict_to_invoke_format(state_dict)
|
||||
|
||||
# Extract the prefixes from the converted state dict (i.e. without the .lora_up.weight, .lora_down.weight, and
|
||||
# .alpha suffixes).
|
||||
@@ -65,29 +70,33 @@ def test_convert_flux_kohya_state_dict_to_invoke_format():
|
||||
raise AssertionError(f"Could not find a match for the converted key prefix: {converted_key_prefix}")
|
||||
|
||||
|
||||
def test_convert_flux_kohya_state_dict_to_invoke_format_error():
|
||||
"""Test that an error is raised by convert_flux_kohya_state_dict_to_invoke_format() if the input state_dict contains
|
||||
unexpected keys.
|
||||
def test_convert_flux_transformer_kohya_state_dict_to_invoke_format_error():
|
||||
"""Test that an error is raised by _convert_flux_transformer_kohya_state_dict_to_invoke_format() if the input
|
||||
state_dict contains unexpected keys.
|
||||
"""
|
||||
state_dict = {
|
||||
"unexpected_key.lora_up.weight": torch.empty(1),
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
convert_flux_kohya_state_dict_to_invoke_format(state_dict)
|
||||
_convert_flux_transformer_kohya_state_dict_to_invoke_format(state_dict)
|
||||
|
||||
|
||||
def test_lora_model_from_flux_kohya_state_dict():
|
||||
@pytest.mark.parametrize("sd_keys", [flux_kohya_state_dict_keys, flux_kohya_te1_state_dict_keys])
|
||||
def test_lora_model_from_flux_kohya_state_dict(sd_keys: list[str]):
|
||||
"""Test that a LoRAModelRaw can be created from a state dict in the Kohya FLUX LoRA format."""
|
||||
# Construct a state dict that is in the Kohya FLUX LoRA format.
|
||||
state_dict = keys_to_mock_state_dict(flux_kohya_state_dict_keys)
|
||||
state_dict = keys_to_mock_state_dict(sd_keys)
|
||||
|
||||
lora_model = lora_model_from_flux_kohya_state_dict(state_dict)
|
||||
|
||||
# Prepare expected layer keys.
|
||||
expected_layer_keys: set[str] = set()
|
||||
for k in flux_kohya_state_dict_keys:
|
||||
k = k.replace("lora_unet_", "")
|
||||
for k in sd_keys:
|
||||
# Replace prefixes.
|
||||
k = k.replace("lora_unet_", FLUX_LORA_TRANSFORMER_PREFIX)
|
||||
k = k.replace("lora_te1_", FLUX_LORA_CLIP_PREFIX)
|
||||
# Remove suffixes.
|
||||
k = k.replace(".lora_up.weight", "")
|
||||
k = k.replace(".lora_down.weight", "")
|
||||
k = k.replace(".alpha", "")
|
||||
|
||||
Reference in New Issue
Block a user