mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-20 00:18:05 -05:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccc55069d1 | ||
|
|
61ff9ee3a7 | ||
|
|
111408c046 | ||
|
|
d7619d465e | ||
|
|
8ad4f6e56d | ||
|
|
bf4899526f | ||
|
|
6435d265c6 | ||
|
|
3163ef454d | ||
|
|
7ea636df70 | ||
|
|
1869824803 | ||
|
|
66fc8af8a6 | ||
|
|
48cb6b12f0 | ||
|
|
68e30a9864 | ||
|
|
f65dc2c081 | ||
|
|
0cd77443a7 | ||
|
|
185ed86424 | ||
|
|
fed817ab83 | ||
|
|
e0b45db69a | ||
|
|
2beac1fb04 |
@@ -157,6 +157,12 @@ def overridden_redoc() -> HTMLResponse:
|
||||
|
||||
web_root_path = Path(list(web_dir.__path__)[0])
|
||||
|
||||
if app_config.unsafe_disable_picklescan:
|
||||
logger.warning(
|
||||
"The unsafe_disable_picklescan option is enabled. This disables malware scanning while installing and"
|
||||
"loading models, which may allow malicious code to be executed. Use at your own risk."
|
||||
)
|
||||
|
||||
try:
|
||||
app.mount("/", NoCacheStaticFiles(directory=Path(web_root_path, "dist"), html=True), name="ui")
|
||||
except RuntimeError:
|
||||
|
||||
@@ -107,6 +107,7 @@ class InvokeAIAppConfig(BaseSettings):
|
||||
hashing_algorithm: Model hashing algorthim for model installs. 'blake3_multi' is best for SSDs. 'blake3_single' is best for spinning disk HDDs. 'random' disables hashing, instead assigning a UUID to models. Useful when using a memory db to reduce model installation time, or if you don't care about storing stable hashes for models. Alternatively, any other hashlib algorithm is accepted, though these are not nearly as performant as blake3.<br>Valid values: `blake3_multi`, `blake3_single`, `random`, `md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`, `blake2b`, `blake2s`, `sha3_224`, `sha3_256`, `sha3_384`, `sha3_512`, `shake_128`, `shake_256`
|
||||
remote_api_tokens: List of regular expression and token pairs used when downloading models from URLs. The download URL is tested against the regex, and if it matches, the token is provided in as a Bearer token.
|
||||
scan_models_on_startup: Scan the models directory on startup, registering orphaned models. This is typically only used in conjunction with `use_memory_db` for testing purposes.
|
||||
unsafe_disable_picklescan: UNSAFE. Disable the picklescan security check during model installation. Recommended only for development and testing purposes. This will allow arbitrary code execution during model installation, so should never be used in production.
|
||||
"""
|
||||
|
||||
_root: Optional[Path] = PrivateAttr(default=None)
|
||||
@@ -196,6 +197,7 @@ class InvokeAIAppConfig(BaseSettings):
|
||||
hashing_algorithm: HASHING_ALGORITHMS = Field(default="blake3_single", description="Model hashing algorthim for model installs. 'blake3_multi' is best for SSDs. 'blake3_single' is best for spinning disk HDDs. 'random' disables hashing, instead assigning a UUID to models. Useful when using a memory db to reduce model installation time, or if you don't care about storing stable hashes for models. Alternatively, any other hashlib algorithm is accepted, though these are not nearly as performant as blake3.")
|
||||
remote_api_tokens: Optional[list[URLRegexTokenPair]] = Field(default=None, description="List of regular expression and token pairs used when downloading models from URLs. The download URL is tested against the regex, and if it matches, the token is provided in as a Bearer token.")
|
||||
scan_models_on_startup: bool = Field(default=False, description="Scan the models directory on startup, registering orphaned models. This is typically only used in conjunction with `use_memory_db` for testing purposes.")
|
||||
unsafe_disable_picklescan: bool = Field(default=False, description="UNSAFE. Disable the picklescan security check during model installation. Recommended only for development and testing purposes. This will allow arbitrary code execution during model installation, so should never be used in production.")
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
@@ -87,9 +87,21 @@ class ModelLoadService(ModelLoadServiceBase):
|
||||
def torch_load_file(checkpoint: Path) -> AnyModel:
|
||||
scan_result = scan_file_path(checkpoint)
|
||||
if scan_result.infected_files != 0:
|
||||
raise Exception(f"The model at {checkpoint} is potentially infected by malware. Aborting load.")
|
||||
if self._app_config.unsafe_disable_picklescan:
|
||||
self._logger.warning(
|
||||
f"Model at {checkpoint} is potentially infected by malware, but picklescan is disabled. "
|
||||
"Proceeding with caution."
|
||||
)
|
||||
else:
|
||||
raise Exception(f"The model at {checkpoint} is potentially infected by malware. Aborting load.")
|
||||
if scan_result.scan_err:
|
||||
raise Exception(f"Error scanning model at {checkpoint} for malware. Aborting load.")
|
||||
if self._app_config.unsafe_disable_picklescan:
|
||||
self._logger.warning(
|
||||
f"Error scanning model at {checkpoint} for malware, but picklescan is disabled. "
|
||||
"Proceeding with caution."
|
||||
)
|
||||
else:
|
||||
raise Exception(f"Error scanning model at {checkpoint} for malware. Aborting load.")
|
||||
|
||||
result = torch_load(checkpoint, map_location="cpu")
|
||||
return result
|
||||
|
||||
@@ -9,6 +9,7 @@ import spandrel
|
||||
import torch
|
||||
|
||||
import invokeai.backend.util.logging as logger
|
||||
from invokeai.app.services.config.config_default import get_config
|
||||
from invokeai.app.util.misc import uuid_string
|
||||
from invokeai.backend.flux.controlnet.state_dict_utils import (
|
||||
is_state_dict_instantx_controlnet,
|
||||
@@ -493,9 +494,21 @@ class ModelProbe(object):
|
||||
# scan model
|
||||
scan_result = pscan.scan_file_path(checkpoint)
|
||||
if scan_result.infected_files != 0:
|
||||
raise Exception(f"The model {model_name} is potentially infected by malware. Aborting import.")
|
||||
if get_config().unsafe_disable_picklescan:
|
||||
logger.warning(
|
||||
f"The model {model_name} is potentially infected by malware, but picklescan is disabled. "
|
||||
"Proceeding with caution."
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(f"The model {model_name} is potentially infected by malware. Aborting import.")
|
||||
if scan_result.scan_err:
|
||||
raise Exception(f"Error scanning model {model_name} for malware. Aborting import.")
|
||||
if get_config().unsafe_disable_picklescan:
|
||||
logger.warning(
|
||||
f"Error scanning the model at {model_name} for malware, but picklescan is disabled. "
|
||||
"Proceeding with caution."
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(f"Error scanning the model at {model_name} for malware. Aborting import.")
|
||||
|
||||
|
||||
# Probing utilities
|
||||
|
||||
@@ -6,13 +6,17 @@ import torch
|
||||
from picklescan.scanner import scan_file_path
|
||||
from safetensors import safe_open
|
||||
|
||||
from invokeai.app.services.config.config_default import get_config
|
||||
from invokeai.backend.model_hash.model_hash import HASHING_ALGORITHMS, ModelHash
|
||||
from invokeai.backend.model_manager.taxonomy import ModelRepoVariant
|
||||
from invokeai.backend.quantization.gguf.loaders import gguf_sd_loader
|
||||
from invokeai.backend.util.logging import InvokeAILogger
|
||||
from invokeai.backend.util.silence_warnings import SilenceWarnings
|
||||
|
||||
StateDict: TypeAlias = dict[str | int, Any] # When are the keys int?
|
||||
|
||||
logger = InvokeAILogger.get_logger()
|
||||
|
||||
|
||||
class ModelOnDisk:
|
||||
"""A utility class representing a model stored on disk."""
|
||||
@@ -79,8 +83,24 @@ class ModelOnDisk:
|
||||
with SilenceWarnings():
|
||||
if path.suffix.endswith((".ckpt", ".pt", ".pth", ".bin")):
|
||||
scan_result = scan_file_path(path)
|
||||
if scan_result.infected_files != 0 or scan_result.scan_err:
|
||||
raise RuntimeError(f"The model {path.stem} is potentially infected by malware. Aborting import.")
|
||||
if scan_result.infected_files != 0:
|
||||
if get_config().unsafe_disable_picklescan:
|
||||
logger.warning(
|
||||
f"The model {path.stem} is potentially infected by malware, but picklescan is disabled. "
|
||||
"Proceeding with caution."
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"The model {path.stem} is potentially infected by malware. Aborting import."
|
||||
)
|
||||
if scan_result.scan_err:
|
||||
if get_config().unsafe_disable_picklescan:
|
||||
logger.warning(
|
||||
f"Error scanning the model at {path.stem} for malware, but picklescan is disabled. "
|
||||
"Proceeding with caution."
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(f"Error scanning the model at {path.stem} for malware. Aborting import.")
|
||||
checkpoint = torch.load(path, map_location="cpu")
|
||||
assert isinstance(checkpoint, dict)
|
||||
elif path.suffix.endswith(".gguf"):
|
||||
|
||||
@@ -149,13 +149,29 @@ flux_kontext = StarterModel(
|
||||
dependencies=[t5_base_encoder, flux_vae, clip_l_encoder],
|
||||
)
|
||||
flux_kontext_quantized = StarterModel(
|
||||
name="FLUX.1 Kontext dev (Quantized)",
|
||||
name="FLUX.1 Kontext dev (quantized)",
|
||||
base=BaseModelType.Flux,
|
||||
source="https://huggingface.co/unsloth/FLUX.1-Kontext-dev-GGUF/resolve/main/flux1-kontext-dev-Q4_K_M.gguf",
|
||||
description="FLUX.1 Kontext dev quantized (q4_k_m). Total size with dependencies: ~14GB",
|
||||
type=ModelType.Main,
|
||||
dependencies=[t5_8b_quantized_encoder, flux_vae, clip_l_encoder],
|
||||
)
|
||||
flux_krea = StarterModel(
|
||||
name="FLUX.1 Krea dev",
|
||||
base=BaseModelType.Flux,
|
||||
source="https://huggingface.co/InvokeAI/FLUX.1-Krea-dev/resolve/main/flux1-krea-dev.safetensors",
|
||||
description="FLUX.1 Krea dev. Total size with dependencies: ~33GB",
|
||||
type=ModelType.Main,
|
||||
dependencies=[t5_8b_quantized_encoder, flux_vae, clip_l_encoder],
|
||||
)
|
||||
flux_krea_quantized = StarterModel(
|
||||
name="FLUX.1 Krea dev (quantized)",
|
||||
base=BaseModelType.Flux,
|
||||
source="https://huggingface.co/InvokeAI/FLUX.1-Krea-dev-GGUF/resolve/main/flux1-krea-dev-Q4_K_M.gguf",
|
||||
description="FLUX.1 Krea dev quantized (q4_k_m). Total size with dependencies: ~14GB",
|
||||
type=ModelType.Main,
|
||||
dependencies=[t5_8b_quantized_encoder, flux_vae, clip_l_encoder],
|
||||
)
|
||||
sd35_medium = StarterModel(
|
||||
name="SD3.5 Medium",
|
||||
base=BaseModelType.StableDiffusion3,
|
||||
@@ -580,13 +596,14 @@ t2i_sketch_sdxl = StarterModel(
|
||||
)
|
||||
# endregion
|
||||
# region SpandrelImageToImage
|
||||
realesrgan_anime = StarterModel(
|
||||
name="RealESRGAN_x4plus_anime_6B",
|
||||
animesharp_v4_rcan = StarterModel(
|
||||
name="2x-AnimeSharpV4_RCAN",
|
||||
base=BaseModelType.Any,
|
||||
source="https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth",
|
||||
description="A Real-ESRGAN 4x upscaling model (optimized for anime images).",
|
||||
source="https://github.com/Kim2091/Kim2091-Models/releases/download/2x-AnimeSharpV4/2x-AnimeSharpV4_RCAN.safetensors",
|
||||
description="A 2x upscaling model (optimized for anime images).",
|
||||
type=ModelType.SpandrelImageToImage,
|
||||
)
|
||||
|
||||
realesrgan_x4 = StarterModel(
|
||||
name="RealESRGAN_x4plus",
|
||||
base=BaseModelType.Any,
|
||||
@@ -732,7 +749,7 @@ STARTER_MODELS: list[StarterModel] = [
|
||||
t2i_lineart_sdxl,
|
||||
t2i_sketch_sdxl,
|
||||
realesrgan_x4,
|
||||
realesrgan_anime,
|
||||
animesharp_v4_rcan,
|
||||
realesrgan_x2,
|
||||
swinir,
|
||||
t5_base_encoder,
|
||||
@@ -743,6 +760,8 @@ STARTER_MODELS: list[StarterModel] = [
|
||||
llava_onevision,
|
||||
flux_fill,
|
||||
cogview4,
|
||||
flux_krea,
|
||||
flux_krea_quantized,
|
||||
]
|
||||
|
||||
sd1_bundle: list[StarterModel] = [
|
||||
@@ -794,6 +813,7 @@ flux_bundle: list[StarterModel] = [
|
||||
flux_redux,
|
||||
flux_fill,
|
||||
flux_kontext_quantized,
|
||||
flux_krea_quantized,
|
||||
]
|
||||
|
||||
STARTER_BUNDLES: dict[str, StarterModelBundle] = {
|
||||
|
||||
@@ -8,8 +8,12 @@ import picklescan.scanner as pscan
|
||||
import safetensors
|
||||
import torch
|
||||
|
||||
from invokeai.app.services.config.config_default import get_config
|
||||
from invokeai.backend.model_manager.taxonomy import ClipVariantType
|
||||
from invokeai.backend.quantization.gguf.loaders import gguf_sd_loader
|
||||
from invokeai.backend.util.logging import InvokeAILogger
|
||||
|
||||
logger = InvokeAILogger.get_logger()
|
||||
|
||||
|
||||
def _fast_safetensors_reader(path: str) -> Dict[str, torch.Tensor]:
|
||||
@@ -59,9 +63,21 @@ def read_checkpoint_meta(path: Union[str, Path], scan: bool = True) -> Dict[str,
|
||||
if scan:
|
||||
scan_result = pscan.scan_file_path(path)
|
||||
if scan_result.infected_files != 0:
|
||||
raise Exception(f"The model at {path} is potentially infected by malware. Aborting import.")
|
||||
if get_config().unsafe_disable_picklescan:
|
||||
logger.warning(
|
||||
f"The model {path} is potentially infected by malware, but picklescan is disabled. "
|
||||
"Proceeding with caution."
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(f"The model {path} is potentially infected by malware. Aborting import.")
|
||||
if scan_result.scan_err:
|
||||
raise Exception(f"Error scanning model at {path} for malware. Aborting import.")
|
||||
if get_config().unsafe_disable_picklescan:
|
||||
logger.warning(
|
||||
f"Error scanning the model at {path} for malware, but picklescan is disabled. "
|
||||
"Proceeding with caution."
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(f"Error scanning the model at {path} for malware. Aborting import.")
|
||||
|
||||
checkpoint = torch.load(path, map_location=torch.device("meta"))
|
||||
return checkpoint
|
||||
|
||||
@@ -1470,7 +1470,6 @@
|
||||
"ui": {
|
||||
"tabs": {
|
||||
"queue": "Warteschlange",
|
||||
"generation": "Erzeugung",
|
||||
"gallery": "Galerie",
|
||||
"models": "Modelle",
|
||||
"upscaling": "Hochskalierung",
|
||||
|
||||
@@ -610,6 +610,10 @@
|
||||
"title": "Toggle Non-Raster Layers",
|
||||
"desc": "Show or hide all non-raster layer categories (Control Layers, Inpaint Masks, Regional Guidance)."
|
||||
},
|
||||
"fitBboxToLayers": {
|
||||
"title": "Fit Bbox To Layers",
|
||||
"desc": "Automatically adjust the generation bounding box to fit visible layers"
|
||||
},
|
||||
"fitBboxToMasks": {
|
||||
"title": "Fit Bbox To Masks",
|
||||
"desc": "Automatically adjust the generation bounding box to fit visible inpaint masks"
|
||||
|
||||
@@ -399,7 +399,6 @@
|
||||
"ui": {
|
||||
"tabs": {
|
||||
"canvas": "Lienzo",
|
||||
"generation": "Generación",
|
||||
"queue": "Cola",
|
||||
"workflows": "Flujos de trabajo",
|
||||
"models": "Modelos",
|
||||
|
||||
@@ -1820,7 +1820,6 @@
|
||||
"upscaling": "Agrandissement",
|
||||
"gallery": "Galerie",
|
||||
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)",
|
||||
"generation": "Génération",
|
||||
"workflows": "Workflows",
|
||||
"workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)",
|
||||
"models": "Modèles",
|
||||
|
||||
@@ -1173,8 +1173,8 @@
|
||||
"layeringStrategy": "Strategia livelli",
|
||||
"longestPath": "Percorso più lungo",
|
||||
"layoutDirection": "Direzione schema",
|
||||
"layoutDirectionRight": "Orizzontale",
|
||||
"layoutDirectionDown": "Verticale",
|
||||
"layoutDirectionRight": "A destra",
|
||||
"layoutDirectionDown": "In basso",
|
||||
"alignment": "Allineamento nodi",
|
||||
"alignmentUL": "In alto a sinistra",
|
||||
"alignmentDL": "In basso a sinistra",
|
||||
@@ -1728,7 +1728,7 @@
|
||||
"structure": {
|
||||
"heading": "Struttura",
|
||||
"paragraphs": [
|
||||
"La struttura determina quanto l'immagine finale rispecchierà il layout dell'originale. Un valore struttura basso permette cambiamenti significativi, mentre un valore struttura alto conserva la composizione e lo schema originali."
|
||||
"La struttura determina quanto l'immagine finale rispecchierà lo schema dell'originale. Un valore struttura basso permette cambiamenti significativi, mentre un valore struttura alto conserva la composizione e lo schema originali."
|
||||
]
|
||||
},
|
||||
"fluxDevLicense": {
|
||||
@@ -2495,11 +2495,12 @@
|
||||
"off": "Spento"
|
||||
},
|
||||
"invertMask": "Inverti maschera",
|
||||
"fitBboxToMasks": "Adatta il riquadro di delimitazione alle maschere"
|
||||
"fitBboxToMasks": "Adatta il riquadro di delimitazione alle maschere",
|
||||
"maxRefImages": "Max Immagini di rif.to",
|
||||
"useAsReferenceImage": "Usa come immagine di riferimento"
|
||||
},
|
||||
"ui": {
|
||||
"tabs": {
|
||||
"generation": "Generazione",
|
||||
"canvas": "Tela",
|
||||
"workflows": "Flussi di lavoro",
|
||||
"workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)",
|
||||
@@ -2508,7 +2509,8 @@
|
||||
"queue": "Coda",
|
||||
"upscaling": "Amplia",
|
||||
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)",
|
||||
"gallery": "Galleria"
|
||||
"gallery": "Galleria",
|
||||
"generate": "Genera"
|
||||
},
|
||||
"launchpad": {
|
||||
"workflowsTitle": "Approfondisci i flussi di lavoro.",
|
||||
@@ -2556,8 +2558,43 @@
|
||||
"helpText": {
|
||||
"promptAdvice": "Durante l'ampliamento, utilizza un prompt che descriva il mezzo e lo stile. Evita di descrivere dettagli specifici del contenuto dell'immagine.",
|
||||
"styleAdvice": "L'ampliamento funziona meglio con lo stile generale dell'immagine."
|
||||
},
|
||||
"creativityAndStructure": {
|
||||
"title": "Creatività e struttura predefinite",
|
||||
"conservative": "Conservativo",
|
||||
"balanced": "Bilanciato",
|
||||
"creative": "Creativo",
|
||||
"artistic": "Artistico"
|
||||
}
|
||||
},
|
||||
"createNewWorkflowFromScratch": "Crea un nuovo flusso di lavoro da zero",
|
||||
"browseAndLoadWorkflows": "Sfoglia e carica i flussi di lavoro esistenti",
|
||||
"addStyleRef": {
|
||||
"title": "Aggiungi un riferimento di stile",
|
||||
"description": "Aggiungi un'immagine per trasferirne l'aspetto."
|
||||
},
|
||||
"editImage": {
|
||||
"title": "Modifica immagine",
|
||||
"description": "Aggiungi un'immagine da perfezionare."
|
||||
},
|
||||
"generateFromText": {
|
||||
"title": "Genera da testo",
|
||||
"description": "Inserisci un prompt e genera."
|
||||
},
|
||||
"useALayoutImage": {
|
||||
"description": "Aggiungi un'immagine per controllare la composizione.",
|
||||
"title": "Usa una immagine guida"
|
||||
},
|
||||
"generate": {
|
||||
"canvasCalloutTitle": "Vuoi avere più controllo, modificare e affinare le tue immagini?",
|
||||
"canvasCalloutLink": "Per ulteriori funzionalità, vai su Tela."
|
||||
}
|
||||
},
|
||||
"panels": {
|
||||
"launchpad": "Rampa di lancio",
|
||||
"workflowEditor": "Editor del flusso di lavoro",
|
||||
"imageViewer": "Visualizzatore immagini",
|
||||
"canvas": "Tela"
|
||||
}
|
||||
},
|
||||
"upscaling": {
|
||||
@@ -2648,10 +2685,8 @@
|
||||
"watchRecentReleaseVideos": "Guarda i video su questa versione",
|
||||
"watchUiUpdatesOverview": "Guarda le novità dell'interfaccia",
|
||||
"items": [
|
||||
"Nuova impostazione per inviare tutte le generazioni della Tela direttamente alla Galleria.",
|
||||
"Nuove funzionalità Inverti maschera (Maiusc+V) e Adatta il Riquadro di delimitazione alla maschera (Maiusc+B).",
|
||||
"Supporto esteso per miniature e configurazioni dei modelli.",
|
||||
"Vari altri aggiornamenti e correzioni per la qualità della vita"
|
||||
"Lo stato dello studio viene salvato sul server, consentendoti di continuare a lavorare su qualsiasi dispositivo.",
|
||||
"Supporto per più immagini di riferimento per FLUX Kontext (solo modello locale)."
|
||||
]
|
||||
},
|
||||
"system": {
|
||||
|
||||
@@ -1783,7 +1783,6 @@
|
||||
"workflows": "ワークフロー",
|
||||
"models": "モデル",
|
||||
"gallery": "ギャラリー",
|
||||
"generation": "生成",
|
||||
"workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)",
|
||||
"modelsTab": "$t(ui.tabs.models) $t(common.tab)",
|
||||
"upscaling": "アップスケーリング",
|
||||
|
||||
@@ -1931,7 +1931,6 @@
|
||||
},
|
||||
"ui": {
|
||||
"tabs": {
|
||||
"generation": "Генерация",
|
||||
"canvas": "Холст",
|
||||
"workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)",
|
||||
"models": "Модели",
|
||||
|
||||
@@ -2238,7 +2238,9 @@
|
||||
"switchOnFinish": "Khi Kết Thúc"
|
||||
},
|
||||
"fitBboxToMasks": "Xếp Vừa Hộp Giới Hạn Vào Lớp Phủ",
|
||||
"invertMask": "Đảo Ngược Lớp Phủ"
|
||||
"invertMask": "Đảo Ngược Lớp Phủ",
|
||||
"maxRefImages": "Ảnh Mẫu Tối Đa",
|
||||
"useAsReferenceImage": "Dùng Làm Ảnh Mẫu"
|
||||
},
|
||||
"stylePresets": {
|
||||
"negativePrompt": "Lệnh Tiêu Cực",
|
||||
@@ -2414,14 +2416,14 @@
|
||||
"tabs": {
|
||||
"gallery": "Thư Viện Ảnh",
|
||||
"models": "Models",
|
||||
"generation": "Generation (Máy Tạo Sinh)",
|
||||
"upscaling": "Upscale (Nâng Cấp Chất Lượng Hình Ảnh)",
|
||||
"canvas": "Canvas (Vùng Ảnh)",
|
||||
"upscalingTab": "$t(common.tab) $t(ui.tabs.upscaling)",
|
||||
"modelsTab": "$t(common.tab) $t(ui.tabs.models)",
|
||||
"queue": "Queue (Hàng Đợi)",
|
||||
"workflows": "Workflow (Luồng Làm Việc)",
|
||||
"workflowsTab": "$t(common.tab) $t(ui.tabs.workflows)"
|
||||
"workflowsTab": "$t(common.tab) $t(ui.tabs.workflows)",
|
||||
"generate": "Tạo Sinh"
|
||||
},
|
||||
"launchpad": {
|
||||
"workflowsTitle": "Đi sâu hơn với Workflow.",
|
||||
@@ -2469,8 +2471,43 @@
|
||||
"promptAdvice": "Khi upscale, dùng lệnh để mô tả phương thức và phong cách. Tránh mô tả các chi tiết cụ thể trong ảnh.",
|
||||
"styleAdvice": "Upscale thích hợp nhất cho phong cách chung của ảnh."
|
||||
},
|
||||
"scale": "Kích Thước"
|
||||
"scale": "Kích Thước",
|
||||
"creativityAndStructure": {
|
||||
"title": "Độ Sáng Tạo & Cấu Trúc Mặc Định",
|
||||
"conservative": "Bảo toàn",
|
||||
"balanced": "Cân bằng",
|
||||
"creative": "Sáng tạo",
|
||||
"artistic": "Thẩm mỹ"
|
||||
}
|
||||
},
|
||||
"createNewWorkflowFromScratch": "Tạo workflow mới từ đầu",
|
||||
"browseAndLoadWorkflows": "Duyệt và tải workflow có sẵn",
|
||||
"addStyleRef": {
|
||||
"title": "Thêm Phong Cách Mẫu",
|
||||
"description": "Thêm ảnh để chuyển đổi diện mạo của nó."
|
||||
},
|
||||
"editImage": {
|
||||
"title": "Biên Tập Ảnh",
|
||||
"description": "Thêm ảnh để chỉnh sửa."
|
||||
},
|
||||
"generateFromText": {
|
||||
"title": "Tạo Sinh Từ Chữ",
|
||||
"description": "Nhập lệnh vào và Kích Hoạt."
|
||||
},
|
||||
"useALayoutImage": {
|
||||
"title": "Dùng Bố Cục Ảnh",
|
||||
"description": "Thêm ảnh để điều khiển bố cục."
|
||||
},
|
||||
"generate": {
|
||||
"canvasCalloutTitle": "Đang tìm cách để điều khiển, chỉnh sửa, và làm lại ảnh?",
|
||||
"canvasCalloutLink": "Vào Canvas cho nhiều tính năng hơn."
|
||||
}
|
||||
},
|
||||
"panels": {
|
||||
"launchpad": "Launchpad",
|
||||
"workflowEditor": "Trình Biên Tập Workflow",
|
||||
"imageViewer": "Trình Xem Ảnh",
|
||||
"canvas": "Canvas"
|
||||
}
|
||||
},
|
||||
"workflows": {
|
||||
@@ -2642,10 +2679,8 @@
|
||||
"watchRecentReleaseVideos": "Xem Video Phát Hành Mới Nhất",
|
||||
"watchUiUpdatesOverview": "Xem Tổng Quan Về Những Cập Nhật Cho Giao Diện Người Dùng",
|
||||
"items": [
|
||||
"Thiết lập mới để gửi các sản phẩm tạo sinh từ Canvas trực tiếp đến Thư Viện Ảnh.",
|
||||
"Chức năng mới Đảo Ngược Lớp Phủ (Shift+V) và khả năng Xếp Vừa Hộp Giới Hạn Vào Lớp Phủ (Shift+B).",
|
||||
"Mở rộng hỗ trợ cho Ảnh Minh Hoạ và thiết lập model.",
|
||||
"Nhiều bản cập nhật và sửa lỗi chất lượng"
|
||||
"Trạng thái Studio được lưu vào server, giúp bạn tiếp tục công việc ở mọi thiết bị.",
|
||||
"Hỗ trợ nhiều ảnh mẫu cho FLUX KONTEXT (chỉ cho model trên máy)."
|
||||
]
|
||||
},
|
||||
"upsell": {
|
||||
|
||||
@@ -1772,7 +1772,6 @@
|
||||
},
|
||||
"ui": {
|
||||
"tabs": {
|
||||
"generation": "生成",
|
||||
"queue": "队列",
|
||||
"canvas": "画布",
|
||||
"upscaling": "放大中",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { canvasReset } from 'features/controlLayers/store/actions';
|
||||
import { inpaintMaskAdded } from 'features/controlLayers/store/canvasSlice';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { allEntitiesDeleted, inpaintMaskAdded } from 'features/controlLayers/store/canvasSlice';
|
||||
import { $canvasManager } from 'features/controlLayers/store/ephemeral';
|
||||
import { paramsReset } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowsCounterClockwiseBold } from 'react-icons/pi';
|
||||
@@ -11,9 +11,10 @@ import { PiArrowsCounterClockwiseBold } from 'react-icons/pi';
|
||||
export const SessionMenuItems = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
|
||||
const resetCanvasLayers = useCallback(() => {
|
||||
dispatch(canvasReset());
|
||||
dispatch(allEntitiesDeleted());
|
||||
dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
|
||||
$canvasManager.get()?.stage.fitBboxToStage();
|
||||
}, [dispatch]);
|
||||
@@ -22,12 +23,16 @@ export const SessionMenuItems = memo(() => {
|
||||
}, [dispatch]);
|
||||
return (
|
||||
<>
|
||||
<MenuItem icon={<PiArrowsCounterClockwiseBold />} onClick={resetCanvasLayers}>
|
||||
{t('controlLayers.resetCanvasLayers')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiArrowsCounterClockwiseBold />} onClick={resetGenerationSettings}>
|
||||
{t('controlLayers.resetGenerationSettings')}
|
||||
</MenuItem>
|
||||
{tab === 'canvas' && (
|
||||
<MenuItem icon={<PiArrowsCounterClockwiseBold />} onClick={resetCanvasLayers}>
|
||||
{t('controlLayers.resetCanvasLayers')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{(tab === 'canvas' || tab === 'generate') && (
|
||||
<MenuItem icon={<PiArrowsCounterClockwiseBold />} onClick={resetGenerationSettings}>
|
||||
{t('controlLayers.resetGenerationSettings')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
|
||||
import { bboxSizeOptimized, bboxSizeRecalled } from 'features/controlLayers/store/canvasSlice';
|
||||
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { sizeOptimized, sizeRecalled } from 'features/controlLayers/store/paramsSlice';
|
||||
import type { ImageWithDims } from 'features/controlLayers/store/types';
|
||||
import type { setGlobalReferenceImageDndTarget, setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { DndImage } from 'features/dnd/DndImage';
|
||||
import { DndImageIcon } from 'features/dnd/DndImageIcon';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
||||
import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import { $isConnected } from 'services/events/stores';
|
||||
@@ -29,7 +34,10 @@ export const RefImageImage = memo(
|
||||
dndTargetData,
|
||||
}: Props<T>) => {
|
||||
const { t } = useTranslation();
|
||||
const store = useAppStore();
|
||||
const isConnected = useStore($isConnected);
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const isStaging = useCanvasIsStaging();
|
||||
const { currentData: imageDTO, isError } = useGetImageDTOQuery(image?.image_name ?? skipToken);
|
||||
const handleResetControlImage = useCallback(() => {
|
||||
onChangeImage(null);
|
||||
@@ -48,6 +56,20 @@ export const RefImageImage = memo(
|
||||
[onChangeImage]
|
||||
);
|
||||
|
||||
const recallSizeAndOptimize = useCallback(() => {
|
||||
if (!imageDTO || (tab === 'canvas' && isStaging)) {
|
||||
return;
|
||||
}
|
||||
const { width, height } = imageDTO;
|
||||
if (tab === 'canvas') {
|
||||
store.dispatch(bboxSizeRecalled({ width, height }));
|
||||
store.dispatch(bboxSizeOptimized());
|
||||
} else if (tab === 'generate') {
|
||||
store.dispatch(sizeRecalled({ width, height }));
|
||||
store.dispatch(sizeOptimized());
|
||||
}
|
||||
}, [imageDTO, isStaging, store, tab]);
|
||||
|
||||
return (
|
||||
<Flex position="relative" w="full" h="full" alignItems="center" data-error={!imageDTO && !image?.image_name}>
|
||||
{!imageDTO && (
|
||||
@@ -69,6 +91,14 @@ export const RefImageImage = memo(
|
||||
tooltip={t('common.reset')}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex position="absolute" flexDir="column" bottom={2} insetInlineEnd={2} gap={1}>
|
||||
<DndImageIcon
|
||||
onClick={recallSizeAndOptimize}
|
||||
icon={<PiRulerBold size={16} />}
|
||||
tooltip={t('parameters.useSize')}
|
||||
isDisabled={!imageDTO || (tab === 'canvas' && isStaging)}
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
<DndDropTarget dndTarget={dndTarget} dndTargetData={dndTargetData} label={t('gallery.drop')} />
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiResizeBold } from 'react-icons/pi';
|
||||
@@ -9,9 +11,23 @@ export const CanvasToolbarFitBboxToLayersButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const canvasManager = useCanvasManager();
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const isCanvasFocused = useIsRegionFocused('canvas');
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
canvasManager.tool.tools.bbox.fitToLayers();
|
||||
}, [canvasManager.tool.tools.bbox]);
|
||||
canvasManager.stage.fitLayersToStage();
|
||||
}, [canvasManager.tool.tools.bbox, canvasManager.stage]);
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'fitBboxToLayers',
|
||||
category: 'canvas',
|
||||
callback: () => {
|
||||
canvasManager.tool.tools.bbox.fitToLayers();
|
||||
canvasManager.stage.fitLayersToStage();
|
||||
},
|
||||
options: { enabled: isCanvasFocused && !isBusy, preventDefault: true },
|
||||
dependencies: [isCanvasFocused, isBusy],
|
||||
});
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
|
||||
@@ -1091,6 +1091,15 @@ const slice = createSlice({
|
||||
|
||||
syncScaledSize(state);
|
||||
},
|
||||
bboxSizeRecalled: (state, action: PayloadAction<{ width: number; height: number }>) => {
|
||||
const { width, height } = action.payload;
|
||||
const gridSize = getGridSize(state.bbox.modelBase);
|
||||
state.bbox.rect.width = Math.max(roundDownToMultiple(width, gridSize), 64);
|
||||
state.bbox.rect.height = Math.max(roundDownToMultiple(height, gridSize), 64);
|
||||
state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height;
|
||||
state.bbox.aspectRatio.id = 'Free';
|
||||
state.bbox.aspectRatio.isLocked = true;
|
||||
},
|
||||
bboxAspectRatioLockToggled: (state) => {
|
||||
state.bbox.aspectRatio.isLocked = !state.bbox.aspectRatio.isLocked;
|
||||
syncScaledSize(state);
|
||||
@@ -1619,6 +1628,7 @@ export const {
|
||||
entityArrangedToBack,
|
||||
entityOpacityChanged,
|
||||
entitiesReordered,
|
||||
allEntitiesDeleted,
|
||||
allEntitiesOfTypeIsHiddenToggled,
|
||||
allNonRasterLayersIsHiddenToggled,
|
||||
// bbox
|
||||
@@ -1626,6 +1636,7 @@ export const {
|
||||
bboxScaledWidthChanged,
|
||||
bboxScaledHeightChanged,
|
||||
bboxScaleMethodChanged,
|
||||
bboxSizeRecalled,
|
||||
bboxWidthChanged,
|
||||
bboxHeightChanged,
|
||||
bboxAspectRatioLockToggled,
|
||||
|
||||
@@ -241,6 +241,15 @@ const slice = createSlice({
|
||||
},
|
||||
|
||||
//#region Dimensions
|
||||
sizeRecalled: (state, action: PayloadAction<{ width: number; height: number }>) => {
|
||||
const { width, height } = action.payload;
|
||||
const gridSize = getGridSize(state.model?.base);
|
||||
state.dimensions.rect.width = Math.max(roundDownToMultiple(width, gridSize), 64);
|
||||
state.dimensions.rect.height = Math.max(roundDownToMultiple(height, gridSize), 64);
|
||||
state.dimensions.aspectRatio.value = state.dimensions.rect.width / state.dimensions.rect.height;
|
||||
state.dimensions.aspectRatio.id = 'Free';
|
||||
state.dimensions.aspectRatio.isLocked = true;
|
||||
},
|
||||
widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => {
|
||||
const { width, updateAspectRatio, clamp } = action.payload;
|
||||
const gridSize = getGridSize(state.model?.base);
|
||||
@@ -369,14 +378,16 @@ const slice = createSlice({
|
||||
const resetState = (state: ParamsState): ParamsState => {
|
||||
// When a new session is requested, we need to keep the current model selections, plus dependent state
|
||||
// like VAE precision. Everything else gets reset to default.
|
||||
const oldState = deepClone(state);
|
||||
const newState = getInitialParamsState();
|
||||
newState.model = state.model;
|
||||
newState.vae = state.vae;
|
||||
newState.fluxVAE = state.fluxVAE;
|
||||
newState.vaePrecision = state.vaePrecision;
|
||||
newState.t5EncoderModel = state.t5EncoderModel;
|
||||
newState.clipEmbedModel = state.clipEmbedModel;
|
||||
newState.refinerModel = state.refinerModel;
|
||||
newState.dimensions = oldState.dimensions;
|
||||
newState.model = oldState.model;
|
||||
newState.vae = oldState.vae;
|
||||
newState.fluxVAE = oldState.fluxVAE;
|
||||
newState.vaePrecision = oldState.vaePrecision;
|
||||
newState.t5EncoderModel = oldState.t5EncoderModel;
|
||||
newState.clipEmbedModel = oldState.clipEmbedModel;
|
||||
newState.refinerModel = oldState.refinerModel;
|
||||
return newState;
|
||||
};
|
||||
|
||||
@@ -427,6 +438,7 @@ export const {
|
||||
modelChanged,
|
||||
|
||||
// Dimensions
|
||||
sizeRecalled,
|
||||
widthChanged,
|
||||
heightChanged,
|
||||
aspectRatioLockToggled,
|
||||
|
||||
@@ -27,6 +27,7 @@ export const DndImageIcon = memo((props: Props) => {
|
||||
return (
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
tooltip={tooltip}
|
||||
aria-label={tooltip}
|
||||
icon={icon}
|
||||
variant="link"
|
||||
|
||||
@@ -53,6 +53,7 @@ export const BoardEditableTitle = memo(({ board, isSelected }: Props) => {
|
||||
color={isSelected ? 'base.100' : 'base.300'}
|
||||
onDoubleClick={editable.startEditing}
|
||||
cursor="text"
|
||||
noOfLines={1}
|
||||
>
|
||||
{editable.value}
|
||||
</Text>
|
||||
|
||||
@@ -37,6 +37,7 @@ export const BoardTooltip = ({ board }: Props) => {
|
||||
/>
|
||||
)}
|
||||
<Flex flexDir="column" alignItems="center">
|
||||
{board && <Text fontWeight="semibold">{board.board_name}</Text>}
|
||||
<Text noOfLines={1}>
|
||||
{t('boards.imagesWithCount', { count: imagesTotal })}, {t('boards.assetsWithCount', { count: assetsTotal })}
|
||||
</Text>
|
||||
|
||||
@@ -75,6 +75,7 @@ export const GalleryPanel = memo(() => {
|
||||
variant="ghost"
|
||||
onClick={collapsibleApi.toggle}
|
||||
leftIcon={isCollapsed ? <PiCaretDownBold /> : <PiCaretUpBold />}
|
||||
noOfLines={1}
|
||||
>
|
||||
{boardName}
|
||||
</Button>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||
import { toast } from 'features/toast/toast';
|
||||
@@ -17,6 +18,7 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
|
||||
const store = useAppStore();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const isBusy = useCanvasIsBusySafe();
|
||||
const isStaging = useCanvasIsStaging();
|
||||
|
||||
const onClickNewCanvasWithRasterLayerFromImage = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
@@ -97,27 +99,31 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
|
||||
<SubMenuButtonContent label={t('controlLayers.newCanvasFromImage')} />
|
||||
</MenuButton>
|
||||
<MenuList {...subMenu.menuListProps}>
|
||||
<MenuItem icon={<PiFileBold />} onClickCapture={onClickNewCanvasWithRasterLayerFromImage} isDisabled={isBusy}>
|
||||
<MenuItem
|
||||
icon={<PiFileBold />}
|
||||
onClickCapture={onClickNewCanvasWithRasterLayerFromImage}
|
||||
isDisabled={isStaging || isBusy}
|
||||
>
|
||||
{t('controlLayers.asRasterLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<PiFileBold />}
|
||||
onClickCapture={onClickNewCanvasWithRasterLayerFromImageWithResize}
|
||||
isDisabled={isBusy}
|
||||
isDisabled={isStaging || isBusy}
|
||||
>
|
||||
{t('controlLayers.asRasterLayerResize')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<PiFileBold />}
|
||||
onClickCapture={onClickNewCanvasWithControlLayerFromImage}
|
||||
isDisabled={isBusy}
|
||||
isDisabled={isStaging || isBusy}
|
||||
>
|
||||
{t('controlLayers.asControlLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<PiFileBold />}
|
||||
onClickCapture={onClickNewCanvasWithControlLayerFromImageWithResize}
|
||||
isDisabled={isBusy}
|
||||
isDisabled={isStaging || isBusy}
|
||||
>
|
||||
{t('controlLayers.asControlLayerResize')}
|
||||
</MenuItem>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
@@ -13,13 +14,17 @@ export const useEditImage = (imageDTO?: ImageDTO | null) => {
|
||||
|
||||
const { getState, dispatch } = useAppStore();
|
||||
const canvasManager = useCanvasManagerSafe();
|
||||
const isStaging = useCanvasIsStaging();
|
||||
|
||||
const isEnabled = useMemo(() => {
|
||||
if (!imageDTO) {
|
||||
return false;
|
||||
}
|
||||
if (isStaging) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [imageDTO]);
|
||||
}, [imageDTO, isStaging]);
|
||||
|
||||
const edit = useCallback(async () => {
|
||||
if (!imageDTO) {
|
||||
|
||||
@@ -4,8 +4,7 @@ import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'
|
||||
import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
|
||||
import { isFluxKontextAspectRatioID, isFluxKontextReferenceImageConfig } from 'features/controlLayers/store/types';
|
||||
import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators';
|
||||
import type { ImageField } from 'features/nodes/types/common';
|
||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||
import { zImageField, zModelIdentifierField } from 'features/nodes/types/common';
|
||||
import { Graph } from 'features/nodes/util/graph/generation/Graph';
|
||||
import {
|
||||
getOriginalAndScaledSizesForTextToImage,
|
||||
@@ -39,34 +38,63 @@ export const buildFluxKontextGraph = (arg: GraphBuilderArg): GraphBuilderReturn
|
||||
const validRefImages = refImages.entities
|
||||
.filter((entity) => entity.isEnabled)
|
||||
.filter((entity) => isFluxKontextReferenceImageConfig(entity.config))
|
||||
.filter((entity) => getGlobalReferenceImageWarnings(entity, model).length === 0)
|
||||
.toReversed(); // sends them in order they are displayed in the list
|
||||
|
||||
let input_image: ImageField | undefined = undefined;
|
||||
|
||||
if (validRefImages[0]) {
|
||||
assert(validRefImages.length === 1, 'Flux Kontext can have at most one reference image');
|
||||
|
||||
assert(validRefImages[0].config.image, 'Image is required for reference image');
|
||||
input_image = {
|
||||
image_name: validRefImages[0].config.image.image_name,
|
||||
};
|
||||
}
|
||||
.filter((entity) => getGlobalReferenceImageWarnings(entity, model).length === 0);
|
||||
|
||||
const g = new Graph(getPrefixedId('flux_kontext_txt2img_graph'));
|
||||
const positivePrompt = g.addNode({
|
||||
id: getPrefixedId('positive_prompt'),
|
||||
type: 'string',
|
||||
});
|
||||
const fluxKontextImage = g.addNode({
|
||||
// @ts-expect-error: These nodes are not available in the OSS application
|
||||
type: input_image ? 'flux_kontext_edit_image' : 'flux_kontext_generate_image',
|
||||
model: zModelIdentifierField.parse(model),
|
||||
aspect_ratio: aspectRatio.id,
|
||||
input_image,
|
||||
prompt_upsampling: true,
|
||||
...selectCanvasOutputFields(state),
|
||||
});
|
||||
|
||||
let fluxKontextImage;
|
||||
|
||||
if (validRefImages.length > 0) {
|
||||
if (validRefImages.length === 1) {
|
||||
// Single reference image - use it directly
|
||||
const firstImage = validRefImages[0]?.config.image;
|
||||
assert(firstImage, 'First image should exist when validRefImages.length > 0');
|
||||
|
||||
fluxKontextImage = g.addNode({
|
||||
// @ts-expect-error: These nodes are not available in the OSS application
|
||||
type: 'flux_kontext_edit_image',
|
||||
model: zModelIdentifierField.parse(model),
|
||||
aspect_ratio: aspectRatio.id,
|
||||
prompt_upsampling: true,
|
||||
input_image: {
|
||||
image_name: firstImage.image_name,
|
||||
},
|
||||
...selectCanvasOutputFields(state),
|
||||
});
|
||||
} else {
|
||||
// Multiple reference images - use concatenation
|
||||
const kontextConcatenator = g.addNode({
|
||||
id: getPrefixedId('flux_kontext_image_prep'),
|
||||
type: 'flux_kontext_image_prep',
|
||||
images: validRefImages.map(({ config }) => zImageField.parse(config.image)),
|
||||
});
|
||||
|
||||
fluxKontextImage = g.addNode({
|
||||
// @ts-expect-error: These nodes are not available in the OSS application
|
||||
type: 'flux_kontext_edit_image',
|
||||
model: zModelIdentifierField.parse(model),
|
||||
aspect_ratio: aspectRatio.id,
|
||||
prompt_upsampling: true,
|
||||
|
||||
...selectCanvasOutputFields(state),
|
||||
});
|
||||
// @ts-expect-error: These nodes are not available in the OSS application
|
||||
g.addEdge(kontextConcatenator, 'image', fluxKontextImage, 'input_image');
|
||||
}
|
||||
} else {
|
||||
fluxKontextImage = g.addNode({
|
||||
// @ts-expect-error: These nodes are not available in the OSS application
|
||||
type: 'flux_kontext_generate_image',
|
||||
model: zModelIdentifierField.parse(model),
|
||||
aspect_ratio: aspectRatio.id,
|
||||
prompt_upsampling: true,
|
||||
...selectCanvasOutputFields(state),
|
||||
});
|
||||
}
|
||||
|
||||
g.addEdge(
|
||||
positivePrompt,
|
||||
@@ -83,6 +111,10 @@ export const buildFluxKontextGraph = (arg: GraphBuilderArg): GraphBuilderReturn
|
||||
height: originalSize.height,
|
||||
});
|
||||
|
||||
if (validRefImages.length > 0) {
|
||||
g.upsertMetadata({ ref_images: [validRefImages] }, 'merge');
|
||||
}
|
||||
|
||||
g.setMetadataReceivingNode(fluxKontextImage);
|
||||
|
||||
return {
|
||||
|
||||
@@ -278,12 +278,6 @@ const getReasonsWhyCannotEnqueueGenerateTab = (arg: {
|
||||
}
|
||||
|
||||
const enabledRefImages = refImages.entities.filter(({ isEnabled }) => isEnabled);
|
||||
const referenceImageCount = enabledRefImages.length;
|
||||
|
||||
// FLUX Kontext via BFL API only supports 1x Reference Image at a time.
|
||||
if (model?.base === 'flux-kontext' && referenceImageCount > 1) {
|
||||
reasons.push({ content: i18n.t('parameters.invoke.fluxKontextMultipleReferenceImages') });
|
||||
}
|
||||
|
||||
enabledRefImages.forEach((entity, i) => {
|
||||
const layerNumber = i + 1;
|
||||
@@ -633,12 +627,6 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
|
||||
});
|
||||
|
||||
const enabledRefImages = refImages.entities.filter(({ isEnabled }) => isEnabled);
|
||||
const referenceImageCount = enabledRefImages.length;
|
||||
|
||||
// FLUX Kontext via BFL API only supports 1x Reference Image at a time.
|
||||
if (model?.base === 'flux-kontext' && referenceImageCount > 1) {
|
||||
reasons.push({ content: i18n.t('parameters.invoke.fluxKontextMultipleReferenceImages') });
|
||||
}
|
||||
|
||||
enabledRefImages.forEach((entity, i) => {
|
||||
const layerNumber = i + 1;
|
||||
|
||||
@@ -103,6 +103,7 @@ export const useHotkeyData = (): HotkeysData => {
|
||||
addHotkey('canvas', 'setFillToWhite', ['d']);
|
||||
addHotkey('canvas', 'fitLayersToCanvas', ['mod+0']);
|
||||
addHotkey('canvas', 'fitBboxToCanvas', ['mod+shift+0']);
|
||||
addHotkey('canvas', 'fitBboxToLayers', ['shift+n']);
|
||||
addHotkey('canvas', 'setZoomTo100Percent', ['mod+1']);
|
||||
addHotkey('canvas', 'setZoomTo200Percent', ['mod+2']);
|
||||
addHotkey('canvas', 'setZoomTo400Percent', ['mod+3']);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { refImageAdded } from 'features/controlLayers/store/refImagesSlice';
|
||||
import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import { addGlobalReferenceImageDndTarget, newCanvasFromImageDndTarget } from 'features/dnd/dnd';
|
||||
import { addGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { LaunchpadButton } from 'features/ui/layouts/LaunchpadButton';
|
||||
import { memo, useMemo } from 'react';
|
||||
@@ -39,13 +39,13 @@ export const LaunchpadAddStyleReference = memo((props: { extraAction?: () => voi
|
||||
<Icon as={PiUserCircleGearBold} boxSize={8} color="base.500" />
|
||||
<Flex flexDir="column" alignItems="flex-start" gap={2}>
|
||||
<Heading size="sm">{t('ui.launchpad.addStyleRef.title')}</Heading>
|
||||
<Text color="base.300">{t('ui.launchpad.addStyleRef.description')}</Text>
|
||||
<Text>{t('ui.launchpad.addStyleRef.description')}</Text>
|
||||
</Flex>
|
||||
<Flex position="absolute" right={3} bottom={3}>
|
||||
<PiUploadBold />
|
||||
<input {...uploadApi.getUploadInputProps()} />
|
||||
</Flex>
|
||||
<DndDropTarget dndTarget={newCanvasFromImageDndTarget} dndTargetData={dndTargetData} label="Drop" />
|
||||
<DndDropTarget dndTarget={addGlobalReferenceImageDndTarget} dndTargetData={dndTargetData} label="Drop" />
|
||||
</LaunchpadButton>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||
@@ -17,6 +18,7 @@ const dndTargetData = newCanvasFromImageDndTarget.getData(NEW_CANVAS_OPTIONS);
|
||||
export const LaunchpadEditImageButton = memo((props: { extraAction?: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { getState, dispatch } = useAppStore();
|
||||
const isStaging = useCanvasIsStaging();
|
||||
|
||||
const onUpload = useCallback(
|
||||
(imageDTO: ImageDTO) => {
|
||||
@@ -28,17 +30,22 @@ export const LaunchpadEditImageButton = memo((props: { extraAction?: () => void
|
||||
const uploadApi = useImageUploadButton({ allowMultiple: false, onUpload });
|
||||
|
||||
return (
|
||||
<LaunchpadButton {...uploadApi.getUploadButtonProps()} position="relative" gap={8}>
|
||||
<LaunchpadButton {...uploadApi.getUploadButtonProps()} position="relative" gap={8} isDisabled={isStaging}>
|
||||
<Icon as={PiPencilBold} boxSize={8} color="base.500" />
|
||||
<Flex flexDir="column" alignItems="flex-start" gap={2}>
|
||||
<Heading size="sm">{t('ui.launchpad.editImage.title')}</Heading>
|
||||
<Text color="base.300">{t('ui.launchpad.editImage.description')}</Text>
|
||||
<Text>{t('ui.launchpad.editImage.description')}</Text>
|
||||
</Flex>
|
||||
<Flex position="absolute" right={3} bottom={3}>
|
||||
<PiUploadBold />
|
||||
<input {...uploadApi.getUploadInputProps()} />
|
||||
</Flex>
|
||||
<DndDropTarget dndTarget={newCanvasFromImageDndTarget} dndTargetData={dndTargetData} label="Drop" />
|
||||
<DndDropTarget
|
||||
dndTarget={newCanvasFromImageDndTarget}
|
||||
dndTargetData={dndTargetData}
|
||||
label="Drop"
|
||||
isDisabled={isStaging}
|
||||
/>
|
||||
</LaunchpadButton>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { LaunchpadButton } from 'features/ui/layouts/LaunchpadButton';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCursorTextBold, PiTextAaBold } from 'react-icons/pi';
|
||||
@@ -14,16 +17,19 @@ const focusOnPrompt = () => {
|
||||
|
||||
export const LaunchpadGenerateFromTextButton = memo((props: { extraAction?: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const isStaging = useCanvasIsStaging();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
focusOnPrompt();
|
||||
props.extraAction?.();
|
||||
}, [props]);
|
||||
return (
|
||||
<LaunchpadButton onClick={onClick} position="relative" gap={8}>
|
||||
<LaunchpadButton onClick={onClick} position="relative" gap={8} isDisabled={tab === 'canvas' && isStaging}>
|
||||
<Icon as={PiTextAaBold} boxSize={8} color="base.500" />
|
||||
<Flex flexDir="column" alignItems="flex-start" gap={2}>
|
||||
<Heading size="sm">{t('ui.launchpad.generateFromText.title')}</Heading>
|
||||
<Text color="base.300">{t('ui.launchpad.generateFromText.description')}</Text>
|
||||
<Text>{t('ui.launchpad.generateFromText.description')}</Text>
|
||||
</Flex>
|
||||
<Flex position="absolute" right={3} bottom={3}>
|
||||
<PiCursorTextBold />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||
@@ -18,6 +19,7 @@ const dndTargetData = newCanvasFromImageDndTarget.getData(NEW_CANVAS_OPTIONS);
|
||||
export const LaunchpadUseALayoutImageButton = memo((props: { extraAction?: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { getState, dispatch } = useAppStore();
|
||||
const isStaging = useCanvasIsStaging();
|
||||
|
||||
const onUpload = useCallback(
|
||||
(imageDTO: ImageDTO) => {
|
||||
@@ -29,17 +31,22 @@ export const LaunchpadUseALayoutImageButton = memo((props: { extraAction?: () =>
|
||||
const uploadApi = useImageUploadButton({ allowMultiple: false, onUpload });
|
||||
|
||||
return (
|
||||
<LaunchpadButton {...uploadApi.getUploadButtonProps()} position="relative" gap={8}>
|
||||
<LaunchpadButton {...uploadApi.getUploadButtonProps()} position="relative" gap={8} isDisabled={isStaging}>
|
||||
<Icon as={PiRectangleDashedBold} boxSize={8} color="base.500" />
|
||||
<Flex flexDir="column" alignItems="flex-start" gap={2}>
|
||||
<Heading size="sm">{t('ui.launchpad.useALayoutImage.title')}</Heading>
|
||||
<Text color="base.300">{t('ui.launchpad.useALayoutImage.description')}</Text>
|
||||
<Text>{t('ui.launchpad.useALayoutImage.description')}</Text>
|
||||
</Flex>
|
||||
<Flex position="absolute" right={3} bottom={3}>
|
||||
<PiUploadBold />
|
||||
<input {...uploadApi.getUploadInputProps()} />
|
||||
</Flex>
|
||||
<DndDropTarget dndTarget={newCanvasFromImageDndTarget} dndTargetData={dndTargetData} label="Drop" />
|
||||
<DndDropTarget
|
||||
dndTarget={newCanvasFromImageDndTarget}
|
||||
dndTargetData={dndTargetData}
|
||||
label="Drop"
|
||||
isDisabled={isStaging}
|
||||
/>
|
||||
</LaunchpadButton>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -12358,6 +12358,7 @@ export type components = {
|
||||
* hashing_algorithm: Model hashing algorthim for model installs. 'blake3_multi' is best for SSDs. 'blake3_single' is best for spinning disk HDDs. 'random' disables hashing, instead assigning a UUID to models. Useful when using a memory db to reduce model installation time, or if you don't care about storing stable hashes for models. Alternatively, any other hashlib algorithm is accepted, though these are not nearly as performant as blake3.<br>Valid values: `blake3_multi`, `blake3_single`, `random`, `md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`, `blake2b`, `blake2s`, `sha3_224`, `sha3_256`, `sha3_384`, `sha3_512`, `shake_128`, `shake_256`
|
||||
* remote_api_tokens: List of regular expression and token pairs used when downloading models from URLs. The download URL is tested against the regex, and if it matches, the token is provided in as a Bearer token.
|
||||
* scan_models_on_startup: Scan the models directory on startup, registering orphaned models. This is typically only used in conjunction with `use_memory_db` for testing purposes.
|
||||
* unsafe_disable_picklescan: UNSAFE. Disable the picklescan security check during model installation. Recommended only for development and testing purposes. This will allow arbitrary code execution during model installation, so should never be used in production.
|
||||
*/
|
||||
InvokeAIAppConfig: {
|
||||
/**
|
||||
@@ -12707,6 +12708,12 @@ export type components = {
|
||||
* @default false
|
||||
*/
|
||||
scan_models_on_startup?: boolean;
|
||||
/**
|
||||
* Unsafe Disable Picklescan
|
||||
* @description UNSAFE. Disable the picklescan security check during model installation. Recommended only for development and testing purposes. This will allow arbitrary code execution during model installation, so should never be used in production.
|
||||
* @default false
|
||||
*/
|
||||
unsafe_disable_picklescan?: boolean;
|
||||
};
|
||||
/**
|
||||
* InvokeAIAppConfigWithSetFields
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "6.3.0rc1"
|
||||
__version__ = "6.3.0"
|
||||
|
||||
Reference in New Issue
Block a user