mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-17 17:37:55 -05:00
Compare commits
246 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1b009f7b8 | ||
|
|
3431e6385c | ||
|
|
5db1027d32 | ||
|
|
579f182fe9 | ||
|
|
55bf41f63f | ||
|
|
fc32fd2d2e | ||
|
|
a2b6536078 | ||
|
|
144c54a6c8 | ||
|
|
ca40daeb97 | ||
|
|
e600cdc826 | ||
|
|
b7c52f33dc | ||
|
|
e78157fcf0 | ||
|
|
7d7b98249f | ||
|
|
f5bf84f304 | ||
|
|
c30d5bece2 | ||
|
|
27845b2f1b | ||
|
|
bad6eea077 | ||
|
|
9c26ac5ce3 | ||
|
|
b7306bb5c9 | ||
|
|
0c115177b2 | ||
|
|
5aae41b5bb | ||
|
|
7ad09a2f79 | ||
|
|
5a6d3639b7 | ||
|
|
84617d3df2 | ||
|
|
e05f30749e | ||
|
|
88a2e27338 | ||
|
|
15a6fd76c8 | ||
|
|
6adb46a86c | ||
|
|
e8a74eb79d | ||
|
|
dcd716c384 | ||
|
|
56697635dd | ||
|
|
5b5657e292 | ||
|
|
ad3dfbe1ed | ||
|
|
59ddc4f7b0 | ||
|
|
4653b79f12 | ||
|
|
778d6f167f | ||
|
|
05c71f50f1 | ||
|
|
406e0be39c | ||
|
|
0d71234a12 | ||
|
|
e38019bb70 | ||
|
|
a879880b42 | ||
|
|
71c8accbfe | ||
|
|
154fb99daf | ||
|
|
0df476ce13 | ||
|
|
e7ad830fa9 | ||
|
|
e81e0a8286 | ||
|
|
d0f7e72cbb | ||
|
|
fdead4fb8c | ||
|
|
31c9945b32 | ||
|
|
22de8a4b12 | ||
|
|
89cb3c3230 | ||
|
|
7bb99ece4e | ||
|
|
28f040123f | ||
|
|
1be3a4db64 | ||
|
|
cb44c995d2 | ||
|
|
9b9b35c315 | ||
|
|
f6edab6032 | ||
|
|
f79665b023 | ||
|
|
6b1bc7a87d | ||
|
|
c6f2994c84 | ||
|
|
0cff67ff23 | ||
|
|
e957c11c9a | ||
|
|
4baa685c7a | ||
|
|
1bd5907a12 | ||
|
|
2fd56e6029 | ||
|
|
b0548edc8c | ||
|
|
41d781176f | ||
|
|
8709de0b33 | ||
|
|
af43fe2fd4 | ||
|
|
ebbb11c3b1 | ||
|
|
0fc8c08da3 | ||
|
|
bfadcffe3c | ||
|
|
49c2332c13 | ||
|
|
dacef158c4 | ||
|
|
0c34d8201e | ||
|
|
77132075ff | ||
|
|
f008d3b0b2 | ||
|
|
4e66ccefe8 | ||
|
|
5d0ed45326 | ||
|
|
379d633ac6 | ||
|
|
93bba1b692 | ||
|
|
667e175ab7 | ||
|
|
de146aa4aa | ||
|
|
ed9c2c8208 | ||
|
|
9d984878f3 | ||
|
|
585eb8c69d | ||
|
|
c105bae127 | ||
|
|
c39f26266f | ||
|
|
47dffd123a | ||
|
|
b946ec3172 | ||
|
|
024c02329d | ||
|
|
4b43b59472 | ||
|
|
d11f115e1a | ||
|
|
92253ce854 | ||
|
|
0ebbfa90c9 | ||
|
|
fdfee11e37 | ||
|
|
6091bf4f60 | ||
|
|
07271ca468 | ||
|
|
3971382a6d | ||
|
|
0d827d8306 | ||
|
|
ec793cb636 | ||
|
|
e4f24c4dc4 | ||
|
|
a6b0581939 | ||
|
|
2a6cfde488 | ||
|
|
8c2e6a3988 | ||
|
|
0b05b24e9a | ||
|
|
842d729ec8 | ||
|
|
8642e8881d | ||
|
|
239fb86a46 | ||
|
|
269d4fe670 | ||
|
|
20813b5615 | ||
|
|
36c16d2781 | ||
|
|
3ae99df091 | ||
|
|
431fd83a43 | ||
|
|
ab41f71a36 | ||
|
|
1f526a1c27 | ||
|
|
8a60def51f | ||
|
|
4845d31857 | ||
|
|
0de5097207 | ||
|
|
505c75a5ab | ||
|
|
4c32b2a123 | ||
|
|
b2ed3c99d4 | ||
|
|
8eb3f40e1b | ||
|
|
9fcba3b876 | ||
|
|
5cabc37a87 | ||
|
|
c5a76806c1 | ||
|
|
bc6dd12083 | ||
|
|
41e1697e79 | ||
|
|
378f33bc92 | ||
|
|
1bf25fadb3 | ||
|
|
6a20271dba | ||
|
|
e36490c2ec | ||
|
|
d4378d9f2a | ||
|
|
1cc6893d0d | ||
|
|
b16d1a943d | ||
|
|
6c375b228e | ||
|
|
23cde86bc4 | ||
|
|
c6f2d127ef | ||
|
|
fb0a924918 | ||
|
|
2d9c82da85 | ||
|
|
7e031e9c01 | ||
|
|
26fe937d97 | ||
|
|
55139bb169 | ||
|
|
6a7fe6668b | ||
|
|
f5fdba795a | ||
|
|
84dc4e4ea9 | ||
|
|
24f22d539f | ||
|
|
89efe9c2b1 | ||
|
|
fbf8aa17c8 | ||
|
|
e55d39a20b | ||
|
|
3a1cedbced | ||
|
|
3d9889e272 | ||
|
|
b2026d9c00 | ||
|
|
f631b5178f | ||
|
|
8df3067599 | ||
|
|
b377b80446 | ||
|
|
7828102b67 | ||
|
|
1b0d599dc2 | ||
|
|
aa4e3adadb | ||
|
|
637d19c22b | ||
|
|
45b4432833 | ||
|
|
b71829a827 | ||
|
|
d95a698ebd | ||
|
|
49d569ec59 | ||
|
|
6ef1c2a5e1 | ||
|
|
0ec6d33086 | ||
|
|
64dfa125d2 | ||
|
|
67042e6dec | ||
|
|
a918198d4f | ||
|
|
288ac0a293 | ||
|
|
963c2ec60c | ||
|
|
79e8482b27 | ||
|
|
f98bbc32dd | ||
|
|
9380d8901c | ||
|
|
67de3f2d9b | ||
|
|
530d20c1be | ||
|
|
4d8bcad15b | ||
|
|
5c93e53195 | ||
|
|
e9c4e12454 | ||
|
|
295b5a20a8 | ||
|
|
eff9c7b92f | ||
|
|
07565d4015 | ||
|
|
94ba840948 | ||
|
|
bd251f8cce | ||
|
|
97719b0aab | ||
|
|
e89266bfe3 | ||
|
|
453ef1a220 | ||
|
|
faf8f0f291 | ||
|
|
5d36499982 | ||
|
|
151d67a0cc | ||
|
|
72431ff197 | ||
|
|
0de1feed76 | ||
|
|
7ffb626dbe | ||
|
|
79753289b1 | ||
|
|
bac4c05fd9 | ||
|
|
8a3b5d2c6f | ||
|
|
309578c19a | ||
|
|
fd58e1d0f2 | ||
|
|
04ffb979ce | ||
|
|
35c00d5a83 | ||
|
|
c2b49d58f5 | ||
|
|
6ff6b40a35 | ||
|
|
1f1beda567 | ||
|
|
91d62eb242 | ||
|
|
013e02d08b | ||
|
|
115053972c | ||
|
|
bcab754ac2 | ||
|
|
f1a542aca2 | ||
|
|
0701cc63a1 | ||
|
|
9337710b45 | ||
|
|
592ef5a9ee | ||
|
|
5fe39a3ae9 | ||
|
|
1888c586ca | ||
|
|
88922a467e | ||
|
|
84115e598c | ||
|
|
370fc67777 | ||
|
|
fa810e1d02 | ||
|
|
ec5043aa83 | ||
|
|
9a2a0cef74 | ||
|
|
c205c1d19e | ||
|
|
ae1a815453 | ||
|
|
687bc281e5 | ||
|
|
567316d753 | ||
|
|
53ac7c9d2c | ||
|
|
90be2a0cdf | ||
|
|
c7fb8f69ae | ||
|
|
7fecb8e88b | ||
|
|
ee6a2a6603 | ||
|
|
2496ac19c4 | ||
|
|
e34ed199c9 | ||
|
|
569533ef80 | ||
|
|
dfac73f9f0 | ||
|
|
f4219d5db3 | ||
|
|
04d1958e93 | ||
|
|
47d7d93e78 | ||
|
|
0e17950949 | ||
|
|
b0cfdc94b5 | ||
|
|
bb153b55d3 | ||
|
|
93ef637d59 | ||
|
|
c5689ca1a7 | ||
|
|
008e421ad4 | ||
|
|
28a77ab06c | ||
|
|
be48d3c12d | ||
|
|
518b21a49a | ||
|
|
68825ca9eb | ||
|
|
73c5f0b479 |
30
.github/workflows/lfs-checks.yml
vendored
Normal file
30
.github/workflows/lfs-checks.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# Checks that large files and LFS-tracked files are properly checked in with pointer format.
|
||||
# Uses https://github.com/ppremk/lfs-warning to detect LFS issues.
|
||||
|
||||
name: 'lfs checks'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
pull_request:
|
||||
types:
|
||||
- 'ready_for_review'
|
||||
- 'opened'
|
||||
- 'synchronize'
|
||||
merge_group:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
lfs-check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
# Required to label and comment on the PRs
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: check lfs files
|
||||
uses: ppremk/lfs-warning@v3.3
|
||||
@@ -33,30 +33,45 @@ Hardware requirements vary significantly depending on model and image output siz
|
||||
|
||||
More detail on system requirements can be found [here](./requirements.md).
|
||||
|
||||
## Step 2: Download
|
||||
## Step 2: Download and Set Up the Launcher
|
||||
|
||||
Download the most recent launcher for your operating system:
|
||||
The Launcher manages your Invoke install. Follow these instructions to download and set up the Launcher.
|
||||
|
||||
- [Download for Windows](https://download.invoke.ai/Invoke%20Community%20Edition.exe)
|
||||
- [Download for macOS](https://download.invoke.ai/Invoke%20Community%20Edition.dmg)
|
||||
- [Download for Linux](https://download.invoke.ai/Invoke%20Community%20Edition.AppImage)
|
||||
!!! info "Instructions for each OS"
|
||||
|
||||
## Step 3: Install or Update
|
||||
=== "Windows"
|
||||
|
||||
Run the launcher you just downloaded, click **Install** and follow the instructions to get set up.
|
||||
- [Download for Windows](https://github.com/invoke-ai/launcher/releases/latest/download/Invoke.Community.Edition.Setup.latest.exe)
|
||||
- Run the `EXE` to install the Launcher and start it.
|
||||
- A desktop shortcut will be created; use this to run the Launcher in the future.
|
||||
- You can delete the `EXE` file you downloaded.
|
||||
|
||||
=== "macOS"
|
||||
|
||||
- [Download for macOS](https://github.com/invoke-ai/launcher/releases/latest/download/Invoke.Community.Edition-latest-arm64.dmg)
|
||||
- Open the `DMG` and drag the app into `Applications`.
|
||||
- Run the Launcher using its entry in `Applications`.
|
||||
- You can delete the `DMG` file you downloaded.
|
||||
|
||||
=== "Linux"
|
||||
|
||||
- [Download for Linux](https://github.com/invoke-ai/launcher/releases/latest/download/Invoke.Community.Edition-latest.AppImage)
|
||||
- You may need to edit the `AppImage` file properties and make it executable.
|
||||
- Optionally move the file to a location that does not require admin privileges and add a desktop shortcut for it.
|
||||
- Run the Launcher by double-clicking the `AppImage` or the shortcut you made.
|
||||
|
||||
## Step 3: Install Invoke
|
||||
|
||||
Run the Launcher you just set up if you haven't already. Click **Install** and follow the instructions to install (or update) Invoke.
|
||||
|
||||
If you have an existing Invoke installation, you can select it and let the launcher manage the install. You'll be able to update or launch the installation.
|
||||
|
||||
!!! warning "Problem running the launcher on macOS"
|
||||
!!! tip "Updating"
|
||||
|
||||
macOS may not allow you to run the launcher. We are working to resolve this by signing the launcher executable. Until that is done, you can manually flag the launcher as safe:
|
||||
The Launcher will check for updates for itself _and_ Invoke.
|
||||
|
||||
- Open the **Invoke Community Edition.dmg** file.
|
||||
- Drag the launcher to **Applications**.
|
||||
- Open a terminal.
|
||||
- Run `xattr -d 'com.apple.quarantine' /Applications/Invoke\ Community\ Edition.app`.
|
||||
|
||||
You should now be able to run the launcher.
|
||||
- When the Launcher detects an update is available for itself, you'll get a small popup window. Click through this and the Launcher will update itself.
|
||||
- When the Launcher detects an update for Invoke, you'll see a small green alert in the Launcher. Click that and follow the instructions to update Invoke.
|
||||
|
||||
## Step 4: Launch
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ class UIType(str, Enum, metaclass=MetaEnum):
|
||||
Imagen3Model = "Imagen3ModelField"
|
||||
Imagen4Model = "Imagen4ModelField"
|
||||
ChatGPT4oModel = "ChatGPT4oModelField"
|
||||
Gemini2_5Model = "Gemini2_5ModelField"
|
||||
FluxKontextModel = "FluxKontextModelField"
|
||||
# endregion
|
||||
|
||||
|
||||
304
invokeai/backend/image_util/imwatermark/vendor.py
Normal file
304
invokeai/backend/image_util/imwatermark/vendor.py
Normal file
@@ -0,0 +1,304 @@
|
||||
# This file is vendored from https://github.com/ShieldMnt/invisible-watermark
|
||||
#
|
||||
# `invisible-watermark` is MIT licensed as of August 23, 2025, when the code was copied into this repo.
|
||||
#
|
||||
# Why we vendored it in:
|
||||
# `invisible-watermark` has a dependency on `opencv-python`, which conflicts with Invoke's dependency on
|
||||
# `opencv-contrib-python`. It's easier to copy the code over than complicate the installation process by
|
||||
# requiring an extra post-install step of removing `opencv-python` and installing `opencv-contrib-python`.
|
||||
|
||||
import struct
|
||||
import uuid
|
||||
import base64
|
||||
import cv2
|
||||
import numpy as np
|
||||
import pywt
|
||||
|
||||
|
||||
class WatermarkEncoder(object):
|
||||
def __init__(self, content=b""):
|
||||
seq = np.array([n for n in content], dtype=np.uint8)
|
||||
self._watermarks = list(np.unpackbits(seq))
|
||||
self._wmLen = len(self._watermarks)
|
||||
self._wmType = "bytes"
|
||||
|
||||
def set_by_ipv4(self, addr):
|
||||
bits = []
|
||||
ips = addr.split(".")
|
||||
for ip in ips:
|
||||
bits += list(np.unpackbits(np.array([ip % 255], dtype=np.uint8)))
|
||||
self._watermarks = bits
|
||||
self._wmLen = len(self._watermarks)
|
||||
self._wmType = "ipv4"
|
||||
assert self._wmLen == 32
|
||||
|
||||
def set_by_uuid(self, uid):
|
||||
u = uuid.UUID(uid)
|
||||
self._wmType = "uuid"
|
||||
seq = np.array([n for n in u.bytes], dtype=np.uint8)
|
||||
self._watermarks = list(np.unpackbits(seq))
|
||||
self._wmLen = len(self._watermarks)
|
||||
|
||||
def set_by_bytes(self, content):
|
||||
self._wmType = "bytes"
|
||||
seq = np.array([n for n in content], dtype=np.uint8)
|
||||
self._watermarks = list(np.unpackbits(seq))
|
||||
self._wmLen = len(self._watermarks)
|
||||
|
||||
def set_by_b16(self, b16):
|
||||
content = base64.b16decode(b16)
|
||||
self.set_by_bytes(content)
|
||||
self._wmType = "b16"
|
||||
|
||||
def set_by_bits(self, bits=[]):
|
||||
self._watermarks = [int(bit) % 2 for bit in bits]
|
||||
self._wmLen = len(self._watermarks)
|
||||
self._wmType = "bits"
|
||||
|
||||
def set_watermark(self, wmType="bytes", content=""):
|
||||
if wmType == "ipv4":
|
||||
self.set_by_ipv4(content)
|
||||
elif wmType == "uuid":
|
||||
self.set_by_uuid(content)
|
||||
elif wmType == "bits":
|
||||
self.set_by_bits(content)
|
||||
elif wmType == "bytes":
|
||||
self.set_by_bytes(content)
|
||||
elif wmType == "b16":
|
||||
self.set_by_b16(content)
|
||||
else:
|
||||
raise NameError("%s is not supported" % wmType)
|
||||
|
||||
def get_length(self):
|
||||
return self._wmLen
|
||||
|
||||
# @classmethod
|
||||
# def loadModel(cls):
|
||||
# RivaWatermark.loadModel()
|
||||
|
||||
def encode(self, cv2Image, method="dwtDct", **configs):
|
||||
(r, c, channels) = cv2Image.shape
|
||||
if r * c < 256 * 256:
|
||||
raise RuntimeError("image too small, should be larger than 256x256")
|
||||
|
||||
if method == "dwtDct":
|
||||
embed = EmbedMaxDct(self._watermarks, wmLen=self._wmLen, **configs)
|
||||
return embed.encode(cv2Image)
|
||||
# elif method == 'dwtDctSvd':
|
||||
# embed = EmbedDwtDctSvd(self._watermarks, wmLen=self._wmLen, **configs)
|
||||
# return embed.encode(cv2Image)
|
||||
# elif method == 'rivaGan':
|
||||
# embed = RivaWatermark(self._watermarks, self._wmLen)
|
||||
# return embed.encode(cv2Image)
|
||||
else:
|
||||
raise NameError("%s is not supported" % method)
|
||||
|
||||
|
||||
class WatermarkDecoder(object):
|
||||
def __init__(self, wm_type="bytes", length=0):
|
||||
self._wmType = wm_type
|
||||
if wm_type == "ipv4":
|
||||
self._wmLen = 32
|
||||
elif wm_type == "uuid":
|
||||
self._wmLen = 128
|
||||
elif wm_type == "bytes":
|
||||
self._wmLen = length
|
||||
elif wm_type == "bits":
|
||||
self._wmLen = length
|
||||
elif wm_type == "b16":
|
||||
self._wmLen = length
|
||||
else:
|
||||
raise NameError("%s is unsupported" % wm_type)
|
||||
|
||||
def reconstruct_ipv4(self, bits):
|
||||
ips = [str(ip) for ip in list(np.packbits(bits))]
|
||||
return ".".join(ips)
|
||||
|
||||
def reconstruct_uuid(self, bits):
|
||||
nums = np.packbits(bits)
|
||||
bstr = b""
|
||||
for i in range(16):
|
||||
bstr += struct.pack(">B", nums[i])
|
||||
|
||||
return str(uuid.UUID(bytes=bstr))
|
||||
|
||||
def reconstruct_bits(self, bits):
|
||||
# return ''.join([str(b) for b in bits])
|
||||
return bits
|
||||
|
||||
def reconstruct_b16(self, bits):
|
||||
bstr = self.reconstruct_bytes(bits)
|
||||
return base64.b16encode(bstr)
|
||||
|
||||
def reconstruct_bytes(self, bits):
|
||||
nums = np.packbits(bits)
|
||||
bstr = b""
|
||||
for i in range(self._wmLen // 8):
|
||||
bstr += struct.pack(">B", nums[i])
|
||||
return bstr
|
||||
|
||||
def reconstruct(self, bits):
|
||||
if len(bits) != self._wmLen:
|
||||
raise RuntimeError("bits are not matched with watermark length")
|
||||
|
||||
if self._wmType == "ipv4":
|
||||
return self.reconstruct_ipv4(bits)
|
||||
elif self._wmType == "uuid":
|
||||
return self.reconstruct_uuid(bits)
|
||||
elif self._wmType == "bits":
|
||||
return self.reconstruct_bits(bits)
|
||||
elif self._wmType == "b16":
|
||||
return self.reconstruct_b16(bits)
|
||||
else:
|
||||
return self.reconstruct_bytes(bits)
|
||||
|
||||
def decode(self, cv2Image, method="dwtDct", **configs):
|
||||
(r, c, channels) = cv2Image.shape
|
||||
if r * c < 256 * 256:
|
||||
raise RuntimeError("image too small, should be larger than 256x256")
|
||||
|
||||
bits = []
|
||||
if method == "dwtDct":
|
||||
embed = EmbedMaxDct(watermarks=[], wmLen=self._wmLen, **configs)
|
||||
bits = embed.decode(cv2Image)
|
||||
# elif method == 'dwtDctSvd':
|
||||
# embed = EmbedDwtDctSvd(watermarks=[], wmLen=self._wmLen, **configs)
|
||||
# bits = embed.decode(cv2Image)
|
||||
# elif method == 'rivaGan':
|
||||
# embed = RivaWatermark(watermarks=[], wmLen=self._wmLen, **configs)
|
||||
# bits = embed.decode(cv2Image)
|
||||
else:
|
||||
raise NameError("%s is not supported" % method)
|
||||
return self.reconstruct(bits)
|
||||
|
||||
# @classmethod
|
||||
# def loadModel(cls):
|
||||
# RivaWatermark.loadModel()
|
||||
|
||||
|
||||
class EmbedMaxDct(object):
|
||||
def __init__(self, watermarks=[], wmLen=8, scales=[0, 36, 36], block=4):
|
||||
self._watermarks = watermarks
|
||||
self._wmLen = wmLen
|
||||
self._scales = scales
|
||||
self._block = block
|
||||
|
||||
def encode(self, bgr):
|
||||
(row, col, channels) = bgr.shape
|
||||
|
||||
yuv = cv2.cvtColor(bgr, cv2.COLOR_BGR2YUV)
|
||||
|
||||
for channel in range(2):
|
||||
if self._scales[channel] <= 0:
|
||||
continue
|
||||
|
||||
ca1, (h1, v1, d1) = pywt.dwt2(yuv[: row // 4 * 4, : col // 4 * 4, channel], "haar")
|
||||
self.encode_frame(ca1, self._scales[channel])
|
||||
|
||||
yuv[: row // 4 * 4, : col // 4 * 4, channel] = pywt.idwt2((ca1, (v1, h1, d1)), "haar")
|
||||
|
||||
bgr_encoded = cv2.cvtColor(yuv, cv2.COLOR_YUV2BGR)
|
||||
return bgr_encoded
|
||||
|
||||
def decode(self, bgr):
|
||||
(row, col, channels) = bgr.shape
|
||||
|
||||
yuv = cv2.cvtColor(bgr, cv2.COLOR_BGR2YUV)
|
||||
|
||||
scores = [[] for i in range(self._wmLen)]
|
||||
for channel in range(2):
|
||||
if self._scales[channel] <= 0:
|
||||
continue
|
||||
|
||||
ca1, (h1, v1, d1) = pywt.dwt2(yuv[: row // 4 * 4, : col // 4 * 4, channel], "haar")
|
||||
|
||||
scores = self.decode_frame(ca1, self._scales[channel], scores)
|
||||
|
||||
avgScores = list(map(lambda l: np.array(l).mean(), scores))
|
||||
|
||||
bits = np.array(avgScores) * 255 > 127
|
||||
return bits
|
||||
|
||||
def decode_frame(self, frame, scale, scores):
|
||||
(row, col) = frame.shape
|
||||
num = 0
|
||||
|
||||
for i in range(row // self._block):
|
||||
for j in range(col // self._block):
|
||||
block = frame[
|
||||
i * self._block : i * self._block + self._block, j * self._block : j * self._block + self._block
|
||||
]
|
||||
|
||||
score = self.infer_dct_matrix(block, scale)
|
||||
# score = self.infer_dct_svd(block, scale)
|
||||
wmBit = num % self._wmLen
|
||||
scores[wmBit].append(score)
|
||||
num = num + 1
|
||||
|
||||
return scores
|
||||
|
||||
def diffuse_dct_svd(self, block, wmBit, scale):
|
||||
u, s, v = np.linalg.svd(cv2.dct(block))
|
||||
|
||||
s[0] = (s[0] // scale + 0.25 + 0.5 * wmBit) * scale
|
||||
return cv2.idct(np.dot(u, np.dot(np.diag(s), v)))
|
||||
|
||||
def infer_dct_svd(self, block, scale):
|
||||
u, s, v = np.linalg.svd(cv2.dct(block))
|
||||
|
||||
score = 0
|
||||
score = int((s[0] % scale) > scale * 0.5)
|
||||
return score
|
||||
if score >= 0.5:
|
||||
return 1.0
|
||||
else:
|
||||
return 0.0
|
||||
|
||||
def diffuse_dct_matrix(self, block, wmBit, scale):
|
||||
pos = np.argmax(abs(block.flatten()[1:])) + 1
|
||||
i, j = pos // self._block, pos % self._block
|
||||
val = block[i][j]
|
||||
if val >= 0.0:
|
||||
block[i][j] = (val // scale + 0.25 + 0.5 * wmBit) * scale
|
||||
else:
|
||||
val = abs(val)
|
||||
block[i][j] = -1.0 * (val // scale + 0.25 + 0.5 * wmBit) * scale
|
||||
return block
|
||||
|
||||
def infer_dct_matrix(self, block, scale):
|
||||
pos = np.argmax(abs(block.flatten()[1:])) + 1
|
||||
i, j = pos // self._block, pos % self._block
|
||||
|
||||
val = block[i][j]
|
||||
if val < 0:
|
||||
val = abs(val)
|
||||
|
||||
if (val % scale) > 0.5 * scale:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def encode_frame(self, frame, scale):
|
||||
"""
|
||||
frame is a matrix (M, N)
|
||||
|
||||
we get K (watermark bits size) blocks (self._block x self._block)
|
||||
|
||||
For i-th block, we encode watermark[i] bit into it
|
||||
"""
|
||||
(row, col) = frame.shape
|
||||
num = 0
|
||||
for i in range(row // self._block):
|
||||
for j in range(col // self._block):
|
||||
block = frame[
|
||||
i * self._block : i * self._block + self._block, j * self._block : j * self._block + self._block
|
||||
]
|
||||
wmBit = self._watermarks[(num % self._wmLen)]
|
||||
|
||||
diffusedBlock = self.diffuse_dct_matrix(block, wmBit, scale)
|
||||
# diffusedBlock = self.diffuse_dct_svd(block, wmBit, scale)
|
||||
frame[
|
||||
i * self._block : i * self._block + self._block, j * self._block : j * self._block + self._block
|
||||
] = diffusedBlock
|
||||
|
||||
num = num + 1
|
||||
@@ -6,13 +6,10 @@ configuration variable, that allows the watermarking to be supressed.
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from imwatermark import WatermarkEncoder
|
||||
from PIL import Image
|
||||
|
||||
import invokeai.backend.util.logging as logger
|
||||
from invokeai.app.services.config.config_default import get_config
|
||||
|
||||
config = get_config()
|
||||
from invokeai.backend.image_util.imwatermark.vendor import WatermarkEncoder
|
||||
|
||||
|
||||
class InvisibleWatermark:
|
||||
|
||||
@@ -28,6 +28,7 @@ class BaseModelType(str, Enum):
|
||||
CogView4 = "cogview4"
|
||||
Imagen3 = "imagen3"
|
||||
Imagen4 = "imagen4"
|
||||
Gemini2_5 = "gemini-2.5"
|
||||
ChatGPT4o = "chatgpt-4o"
|
||||
FluxKontext = "flux-kontext"
|
||||
|
||||
|
||||
@@ -622,6 +622,10 @@
|
||||
"title": "Fit Bbox To Masks",
|
||||
"desc": "Automatically adjust the generation bounding box to fit visible inpaint masks"
|
||||
},
|
||||
"toggleBbox": {
|
||||
"title": "Toggle Bbox Visibility",
|
||||
"desc": "Hide or show the generation bounding box"
|
||||
},
|
||||
"applySegmentAnything": {
|
||||
"title": "Apply Segment Anything",
|
||||
"desc": "Apply the current Segment Anything mask.",
|
||||
@@ -1377,8 +1381,8 @@
|
||||
"addedToBoard": "Added to board {{name}}'s assets",
|
||||
"addedToUncategorized": "Added to board $t(boards.uncategorized)'s assets",
|
||||
"baseModelChanged": "Base Model Changed",
|
||||
"baseModelChangedCleared_one": "Cleared or disabled {{count}} incompatible submodel",
|
||||
"baseModelChangedCleared_other": "Cleared or disabled {{count}} incompatible submodels",
|
||||
"baseModelChangedCleared_one": "Updated, cleared or disabled {{count}} incompatible submodel",
|
||||
"baseModelChangedCleared_other": "Updated, cleared or disabled {{count}} incompatible submodels",
|
||||
"canceled": "Processing Canceled",
|
||||
"connected": "Connected to Server",
|
||||
"imageCopied": "Image Copied",
|
||||
@@ -1946,8 +1950,11 @@
|
||||
"zoomToNode": "Zoom to Node",
|
||||
"nodeFieldTooltip": "To add a node field, click the small plus sign button on the field in the Workflow Editor, or drag the field by its name into the form.",
|
||||
"addToForm": "Add to Form",
|
||||
"removeFromForm": "Remove from Form",
|
||||
"label": "Label",
|
||||
"showDescription": "Show Description",
|
||||
"showShuffle": "Show Shuffle",
|
||||
"shuffle": "Shuffle",
|
||||
"component": "Component",
|
||||
"numberInput": "Number Input",
|
||||
"singleLine": "Single Line",
|
||||
@@ -2682,8 +2689,8 @@
|
||||
"whatsNew": {
|
||||
"whatsNewInInvoke": "What's New in Invoke",
|
||||
"items": [
|
||||
"Misc QoL: Toggle Bbox visibility, highlight nodes with errors, prevent adding node fields to Builder form multiple times, CLIP Skip metadata recallable",
|
||||
"Reduced VRAM usage for multiple Kontext Ref images and VAE encoding"
|
||||
"Canvas: Color Picker does not sample alpha, bbox respects aspect ratio lock when resizing shuffle button for number fields in Workflow Builder, hide pixel dimension sliders when using a model that doesn't support them",
|
||||
"Workflows: Add a Shuffle button to number input fields"
|
||||
],
|
||||
"readReleaseNotes": "Read Release Notes",
|
||||
"watchRecentReleaseVideos": "Watch Recent Release Videos",
|
||||
|
||||
@@ -130,7 +130,8 @@
|
||||
"compactView": "Vista compatta",
|
||||
"fullView": "Vista completa",
|
||||
"removeNegativePrompt": "Rimuovi prompt negativo",
|
||||
"addNegativePrompt": "Aggiungi prompt negativo"
|
||||
"addNegativePrompt": "Aggiungi prompt negativo",
|
||||
"selectYourModel": "Seleziona il modello"
|
||||
},
|
||||
"gallery": {
|
||||
"galleryImageSize": "Dimensione dell'immagine",
|
||||
@@ -416,6 +417,10 @@
|
||||
"fitBboxToLayers": {
|
||||
"title": "Adatta il riquadro di delimitazione ai livelli",
|
||||
"desc": "Regola automaticamente il riquadro di delimitazione della generazione per adattarlo ai livelli visibili"
|
||||
},
|
||||
"toggleBbox": {
|
||||
"title": "Attiva/disattiva la visibilità del riquadro di delimitazione",
|
||||
"desc": "Nascondi o mostra il riquadro di delimitazione della generazione"
|
||||
}
|
||||
},
|
||||
"workflows": {
|
||||
@@ -717,7 +722,10 @@
|
||||
"bundleDescription": "Ogni pacchetto include modelli essenziali per ogni famiglia di modelli e modelli base selezionati per iniziare.",
|
||||
"browseAll": "Oppure scopri tutti i modelli disponibili:"
|
||||
},
|
||||
"launchpadTab": "Rampa di lancio"
|
||||
"launchpadTab": "Rampa di lancio",
|
||||
"installBundle": "Installa pacchetto",
|
||||
"installBundleMsg1": "Vuoi davvero installare il pacchetto {{bundleName}}?",
|
||||
"installBundleMsg2": "Questo pacchetto installerà i seguenti {{count}} modelli:"
|
||||
},
|
||||
"parameters": {
|
||||
"images": "Immagini",
|
||||
@@ -804,7 +812,6 @@
|
||||
"modelIncompatibleScaledBboxWidth": "La larghezza scalata del riquadro è {{width}} ma {{model}} richiede multipli di {{multiple}}",
|
||||
"modelIncompatibleScaledBboxHeight": "L'altezza scalata del riquadro è {{height}} ma {{model}} richiede multipli di {{multiple}}",
|
||||
"modelDisabledForTrial": "La generazione con {{modelName}} non è disponibile per gli account di prova. Accedi alle impostazioni del tuo account per effettuare l'upgrade.",
|
||||
"fluxKontextMultipleReferenceImages": "È possibile utilizzare solo 1 immagine di riferimento alla volta con FLUX Kontext tramite BFL API",
|
||||
"promptExpansionResultPending": "Accetta o ignora il risultato dell'espansione del prompt",
|
||||
"promptExpansionPending": "Espansione del prompt in corso"
|
||||
},
|
||||
@@ -834,7 +841,8 @@
|
||||
"coherenceMinDenoise": "Min rid. rumore",
|
||||
"recallMetadata": "Richiama i metadati",
|
||||
"disabledNoRasterContent": "Disabilitato (nessun contenuto Raster)",
|
||||
"modelDisabledForTrial": "La generazione con {{modelName}} non è disponibile per gli account di prova. Visita le <LinkComponent>impostazioni account</LinkComponent> per effettuare l'upgrade."
|
||||
"modelDisabledForTrial": "La generazione con {{modelName}} non è disponibile per gli account di prova. Visita le <LinkComponent>impostazioni account</LinkComponent> per effettuare l'upgrade.",
|
||||
"useClipSkip": "Usa CLIP Skip"
|
||||
},
|
||||
"settings": {
|
||||
"models": "Modelli",
|
||||
@@ -887,8 +895,8 @@
|
||||
"parameterSet": "Parametro richiamato",
|
||||
"parameterNotSet": "Parametro non richiamato",
|
||||
"problemCopyingImage": "Impossibile copiare l'immagine",
|
||||
"baseModelChangedCleared_one": "Cancellato o disabilitato {{count}} sottomodello incompatibile",
|
||||
"baseModelChangedCleared_many": "Cancellati o disabilitati {{count}} sottomodelli incompatibili",
|
||||
"baseModelChangedCleared_one": "Aggiornato, cancellato o disabilitato {{count}} sottomodello incompatibile",
|
||||
"baseModelChangedCleared_many": "Aggiornati, cancellati o disabilitati {{count}} sottomodelli incompatibili",
|
||||
"baseModelChangedCleared_other": "Cancellati o disabilitati {{count}} sottomodelli incompatibili",
|
||||
"loadedWithWarnings": "Flusso di lavoro caricato con avvisi",
|
||||
"imageUploaded": "Immagine caricata",
|
||||
@@ -1233,7 +1241,8 @@
|
||||
"updateBoardError": "Errore durante l'aggiornamento della bacheca",
|
||||
"uncategorizedImages": "Immagini non categorizzate",
|
||||
"deleteAllUncategorizedImages": "Elimina tutte le immagini non categorizzate",
|
||||
"deletedImagesCannotBeRestored": "Le immagini eliminate non possono essere ripristinate."
|
||||
"deletedImagesCannotBeRestored": "Le immagini eliminate non possono essere ripristinate.",
|
||||
"locateInGalery": "Trova nella Galleria"
|
||||
},
|
||||
"queue": {
|
||||
"queueFront": "Aggiungi all'inizio della coda",
|
||||
@@ -1980,7 +1989,10 @@
|
||||
"publishInProgress": "Pubblicazione in corso",
|
||||
"selectingOutputNode": "Selezione del nodo di uscita",
|
||||
"selectingOutputNodeDesc": "Fare clic su un nodo per selezionarlo come nodo di uscita del flusso di lavoro.",
|
||||
"errorWorkflowHasUnpublishableNodes": "Il flusso di lavoro ha nodi di estrazione lotto, generatore o metadati"
|
||||
"errorWorkflowHasUnpublishableNodes": "Il flusso di lavoro ha nodi di estrazione lotto, generatore o metadati",
|
||||
"showShuffle": "Mostra Mescola",
|
||||
"shuffle": "Mescola",
|
||||
"removeFromForm": "Rimuovi dal modulo"
|
||||
},
|
||||
"loadMore": "Carica altro",
|
||||
"searchPlaceholder": "Cerca per nome, descrizione o etichetta",
|
||||
@@ -2461,7 +2473,8 @@
|
||||
"ipAdapterIncompatibleBaseModel": "modello base dell'immagine di riferimento incompatibile",
|
||||
"ipAdapterNoImageSelected": "nessuna immagine di riferimento selezionata",
|
||||
"rgAutoNegativeNotSupported": "Auto-Negativo non supportato per il modello base selezionato",
|
||||
"fluxFillIncompatibleWithControlLoRA": "Il controllo LoRA non è compatibile con FLUX Fill"
|
||||
"fluxFillIncompatibleWithControlLoRA": "Il controllo LoRA non è compatibile con FLUX Fill",
|
||||
"bboxHidden": "Il riquadro di delimitazione è nascosto (Shift+o per attivarlo)"
|
||||
},
|
||||
"pasteTo": "Incolla su",
|
||||
"pasteToBboxDesc": "Nuovo livello (nel riquadro di delimitazione)",
|
||||
@@ -2691,8 +2704,8 @@
|
||||
"watchRecentReleaseVideos": "Guarda i video su questa versione",
|
||||
"watchUiUpdatesOverview": "Guarda le novità dell'interfaccia",
|
||||
"items": [
|
||||
"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)."
|
||||
"Tela: Color Picker non campiona l'alfa, il riquadro di delimitazione rispetta il blocco delle proporzioni quando si ridimensiona il pulsante Mescola per i campi numerici nel generatore di flusso di lavoro, nasconde i cursori delle dimensioni dei pixel quando si utilizza un modello che non li supporta",
|
||||
"Flussi di lavoro: aggiunto un pulsante Mescola ai campi di input numerici"
|
||||
]
|
||||
},
|
||||
"system": {
|
||||
|
||||
@@ -755,7 +755,6 @@
|
||||
"noFLUXVAEModelSelected": "FLUX生成にVAEモデルが選択されていません",
|
||||
"noT5EncoderModelSelected": "FLUX生成にT5エンコーダモデルが選択されていません",
|
||||
"modelDisabledForTrial": "{{modelName}} を使用した生成はトライアルアカウントではご利用いただけません.アカウント設定にアクセスしてアップグレードしてください。",
|
||||
"fluxKontextMultipleReferenceImages": "Flux Kontext では一度に 1 つの参照画像しか使用できません",
|
||||
"promptExpansionPending": "プロンプト拡張が進行中",
|
||||
"promptExpansionResultPending": "プロンプト拡張結果を受け入れるか破棄してください"
|
||||
},
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
"assetsWithCount_other": "{{count}} tài nguyên",
|
||||
"uncategorizedImages": "Ảnh Chưa Sắp Xếp",
|
||||
"deleteAllUncategorizedImages": "Xoá Tất Cả Ảnh Chưa Sắp Xếp",
|
||||
"deletedImagesCannotBeRestored": "Ảnh đã xoá không thể phục hồi lại."
|
||||
"deletedImagesCannotBeRestored": "Ảnh đã xoá không thể phục hồi lại.",
|
||||
"locateInGalery": "Vị Trí Ở Thư Viện Ảnh"
|
||||
},
|
||||
"gallery": {
|
||||
"swapImages": "Đổi Hình Ảnh",
|
||||
@@ -499,6 +500,10 @@
|
||||
"fitBboxToLayers": {
|
||||
"title": "Xếp Vừa Hộp Giới Hạn Vào Layer",
|
||||
"desc": "Tự động điểu chỉnh hộp giới hạn tạo sinh vừa vặn vào layer nhìn thấy được"
|
||||
},
|
||||
"toggleBbox": {
|
||||
"title": "Bật/Tắt Hiển Thị Hộp Giới Hạn",
|
||||
"desc": "Ẩn hoặc hiện hộp giới hạn tạo sinh"
|
||||
}
|
||||
},
|
||||
"workflows": {
|
||||
@@ -872,7 +877,10 @@
|
||||
"stableDiffusion15": "Stable Diffusion 1.5",
|
||||
"sdxl": "SDXL",
|
||||
"fluxDev": "FLUX.1 dev"
|
||||
}
|
||||
},
|
||||
"installBundle": "Tải Xuống Gói",
|
||||
"installBundleMsg1": "Bạn có chắc chắn muốn tải xuống gói {{bundleName}}?",
|
||||
"installBundleMsg2": "Gói này sẽ tải xuống {{count}} model sau đây:"
|
||||
},
|
||||
"metadata": {
|
||||
"guidance": "Hướng Dẫn",
|
||||
@@ -1649,7 +1657,6 @@
|
||||
"modelIncompatibleScaledBboxHeight": "Chiều dài hộp giới hạn theo tỉ lệ là {{height}} nhưng {{model}} yêu cầu bội số của {{multiple}}",
|
||||
"modelIncompatibleScaledBboxWidth": "Chiều rộng hộp giới hạn theo tỉ lệ là {{width}} nhưng {{model}} yêu cầu bội số của {{multiple}}",
|
||||
"modelDisabledForTrial": "Tạo sinh với {{modelName}} là không thể với tài khoản trial. Vào phần thiết lập tài khoản để nâng cấp.",
|
||||
"fluxKontextMultipleReferenceImages": "Chỉ có thể dùng 1 Ảnh Mẫu cùng lúc với LUX Kontext thông qua BFL API",
|
||||
"promptExpansionPending": "Trong quá trình mở rộng lệnh",
|
||||
"promptExpansionResultPending": "Hãy chấp thuận hoặc huỷ bỏ kết quả mở rộng lệnh của bạn"
|
||||
},
|
||||
@@ -2632,7 +2639,10 @@
|
||||
"publishingValidationRunInProgress": "Quá trình kiểm tra tính hợp lệ đang diễn ra.",
|
||||
"selectingOutputNodeDesc": "Bấm vào node để biến nó thành node đầu ra của workflow.",
|
||||
"selectingOutputNode": "Chọn node đầu ra",
|
||||
"errorWorkflowHasUnpublishableNodes": "Workflow có lô node, node sản sinh, hoặc node tách metadata"
|
||||
"errorWorkflowHasUnpublishableNodes": "Workflow có lô node, node sản sinh, hoặc node tách metadata",
|
||||
"removeFromForm": "Xóa Khỏi Vùng Nhập",
|
||||
"showShuffle": "Hiện Xáo Trộn",
|
||||
"shuffle": "Xáo Trộn"
|
||||
},
|
||||
"yourWorkflows": "Workflow Của Bạn",
|
||||
"browseWorkflows": "Khám Phá Workflow",
|
||||
@@ -2689,8 +2699,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": [
|
||||
"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)."
|
||||
"Misc QoL: Bật/Tắt hiển thị hộp giới hạn, đánh dấu node bị lỗi, chặn lỗi thêm node vào vùng nhập nhiều lần, khả năng đọc lại metadata của CLIP Skip",
|
||||
"Giảm lượng tiêu thụ VRAM cho các ảnh mẫu Kontext và mã hóa VAE"
|
||||
]
|
||||
},
|
||||
"upsell": {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/store';
|
||||
import { bboxSyncedToOptimalDimension, rgRefImageModelChanged } from 'features/controlLayers/store/canvasSlice';
|
||||
import { buildSelectIsStaging, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { loraDeleted } from 'features/controlLayers/store/lorasSlice';
|
||||
import { loraIsEnabledChanged } from 'features/controlLayers/store/lorasSlice';
|
||||
import { modelChanged, syncedToOptimalDimension, vaeSelected } from 'features/controlLayers/store/paramsSlice';
|
||||
import { refImageModelChanged, selectReferenceImageEntities } from 'features/controlLayers/store/refImagesSlice';
|
||||
import {
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from 'features/controlLayers/store/selectors';
|
||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { modelSelected } from 'features/parameters/store/actions';
|
||||
import { SUPPORTS_REF_IMAGES_BASE_MODELS } from 'features/parameters/types/constants';
|
||||
import { zParameterModel } from 'features/parameters/types/parameterSchemas';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
isFluxKontextApiModelConfig,
|
||||
isFluxKontextModelConfig,
|
||||
isFluxReduxModelConfig,
|
||||
isGemini2_5ModelConfig,
|
||||
} from 'services/api/types';
|
||||
|
||||
const log = logger('models');
|
||||
@@ -44,13 +46,13 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
|
||||
|
||||
if (didBaseModelChange) {
|
||||
// we may need to reset some incompatible submodels
|
||||
let modelsCleared = 0;
|
||||
let modelsUpdatedDisabledOrCleared = 0;
|
||||
|
||||
// handle incompatible loras
|
||||
state.loras.loras.forEach((lora) => {
|
||||
if (lora.model.base !== newBase) {
|
||||
dispatch(loraDeleted({ id: lora.id }));
|
||||
modelsCleared += 1;
|
||||
dispatch(loraIsEnabledChanged({ id: lora.id, isEnabled: false }));
|
||||
modelsUpdatedDisabledOrCleared += 1;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -58,52 +60,57 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
|
||||
const { vae } = state.params;
|
||||
if (vae && vae.base !== newBase) {
|
||||
dispatch(vaeSelected(null));
|
||||
modelsCleared += 1;
|
||||
modelsUpdatedDisabledOrCleared += 1;
|
||||
}
|
||||
|
||||
// Handle incompatible reference image models - switch to first compatible model, with some smart logic
|
||||
// to choose the best available model based on the new main model.
|
||||
const allRefImageModels = selectGlobalRefImageModels(state).filter(({ base }) => base === newBase);
|
||||
if (SUPPORTS_REF_IMAGES_BASE_MODELS.includes(newModel.base)) {
|
||||
// Handle incompatible reference image models - switch to first compatible model, with some smart logic
|
||||
// to choose the best available model based on the new main model.
|
||||
const allRefImageModels = selectGlobalRefImageModels(state).filter(({ base }) => base === newBase);
|
||||
|
||||
let newGlobalRefImageModel = null;
|
||||
let newGlobalRefImageModel = null;
|
||||
|
||||
// Certain models require the ref image model to be the same as the main model - others just need a matching
|
||||
// base. Helper to grab the first exact match or the first available model if no exact match is found.
|
||||
const exactMatchOrFirst = <T extends AnyModelConfig>(candidates: T[]): T | null =>
|
||||
candidates.find(({ key }) => key === newModel.key) ?? candidates[0] ?? null;
|
||||
// Certain models require the ref image model to be the same as the main model - others just need a matching
|
||||
// base. Helper to grab the first exact match or the first available model if no exact match is found.
|
||||
const exactMatchOrFirst = <T extends AnyModelConfig>(candidates: T[]): T | null =>
|
||||
candidates.find(({ key }) => key === newModel.key) ?? candidates[0] ?? null;
|
||||
|
||||
// The only way we can differentiate between FLUX and FLUX Kontext is to check for "kontext" in the name
|
||||
if (newModel.base === 'flux' && newModel.name.toLowerCase().includes('kontext')) {
|
||||
const fluxKontextDevModels = allRefImageModels.filter(isFluxKontextModelConfig);
|
||||
newGlobalRefImageModel = exactMatchOrFirst(fluxKontextDevModels);
|
||||
} else if (newModel.base === 'chatgpt-4o') {
|
||||
const chatGPT4oModels = allRefImageModels.filter(isChatGPT4oModelConfig);
|
||||
newGlobalRefImageModel = exactMatchOrFirst(chatGPT4oModels);
|
||||
} else if (newModel.base === 'flux-kontext') {
|
||||
const fluxKontextApiModels = allRefImageModels.filter(isFluxKontextApiModelConfig);
|
||||
newGlobalRefImageModel = exactMatchOrFirst(fluxKontextApiModels);
|
||||
} else if (newModel.base === 'flux') {
|
||||
const fluxReduxModels = allRefImageModels.filter(isFluxReduxModelConfig);
|
||||
newGlobalRefImageModel = fluxReduxModels[0] ?? null;
|
||||
} else {
|
||||
newGlobalRefImageModel = allRefImageModels[0] ?? null;
|
||||
}
|
||||
// The only way we can differentiate between FLUX and FLUX Kontext is to check for "kontext" in the name
|
||||
if (newModel.base === 'flux' && newModel.name.toLowerCase().includes('kontext')) {
|
||||
const fluxKontextDevModels = allRefImageModels.filter(isFluxKontextModelConfig);
|
||||
newGlobalRefImageModel = exactMatchOrFirst(fluxKontextDevModels);
|
||||
} else if (newModel.base === 'chatgpt-4o') {
|
||||
const chatGPT4oModels = allRefImageModels.filter(isChatGPT4oModelConfig);
|
||||
newGlobalRefImageModel = exactMatchOrFirst(chatGPT4oModels);
|
||||
} else if (newModel.base === 'gemini-2.5') {
|
||||
const gemini2_5Models = allRefImageModels.filter(isGemini2_5ModelConfig);
|
||||
newGlobalRefImageModel = exactMatchOrFirst(gemini2_5Models);
|
||||
} else if (newModel.base === 'flux-kontext') {
|
||||
const fluxKontextApiModels = allRefImageModels.filter(isFluxKontextApiModelConfig);
|
||||
newGlobalRefImageModel = exactMatchOrFirst(fluxKontextApiModels);
|
||||
} else if (newModel.base === 'flux') {
|
||||
const fluxReduxModels = allRefImageModels.filter(isFluxReduxModelConfig);
|
||||
newGlobalRefImageModel = fluxReduxModels[0] ?? null;
|
||||
} else {
|
||||
newGlobalRefImageModel = allRefImageModels[0] ?? null;
|
||||
}
|
||||
|
||||
// All ref image entities are updated to use the same new model
|
||||
const refImageEntities = selectReferenceImageEntities(state);
|
||||
for (const entity of refImageEntities) {
|
||||
const shouldUpdateModel =
|
||||
(entity.config.model && entity.config.model.base !== newBase) ||
|
||||
(!entity.config.model && newGlobalRefImageModel);
|
||||
// All ref image entities are updated to use the same new model
|
||||
const refImageEntities = selectReferenceImageEntities(state);
|
||||
for (const entity of refImageEntities) {
|
||||
const shouldUpdateModel =
|
||||
(entity.config.model && entity.config.model.base !== newBase) ||
|
||||
(!entity.config.model && newGlobalRefImageModel);
|
||||
|
||||
if (shouldUpdateModel) {
|
||||
dispatch(
|
||||
refImageModelChanged({
|
||||
id: entity.id,
|
||||
modelConfig: newGlobalRefImageModel,
|
||||
})
|
||||
);
|
||||
modelsCleared += 1;
|
||||
if (shouldUpdateModel) {
|
||||
dispatch(
|
||||
refImageModelChanged({
|
||||
id: entity.id,
|
||||
modelConfig: newGlobalRefImageModel,
|
||||
})
|
||||
);
|
||||
modelsUpdatedDisabledOrCleared += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,17 +135,17 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
|
||||
modelConfig: newRegionalRefImageModel,
|
||||
})
|
||||
);
|
||||
modelsCleared += 1;
|
||||
modelsUpdatedDisabledOrCleared += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (modelsCleared > 0) {
|
||||
if (modelsUpdatedDisabledOrCleared > 0) {
|
||||
toast({
|
||||
id: 'BASE_MODEL_CHANGED',
|
||||
title: t('toast.baseModelChanged'),
|
||||
description: t('toast.baseModelChangedCleared', {
|
||||
count: modelsCleared,
|
||||
count: modelsUpdatedDisabledOrCleared,
|
||||
}),
|
||||
status: 'warning',
|
||||
});
|
||||
|
||||
5
invokeai/frontend/web/src/common/util/randomFloat.ts
Normal file
5
invokeai/frontend/web/src/common/util/randomFloat.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
const randomFloat = (min: number, max: number): number => {
|
||||
return Math.random() * (max - min + Number.EPSILON) + min;
|
||||
};
|
||||
|
||||
export default randomFloat;
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
isModalOpenChanged,
|
||||
selectChangeBoardModalSlice,
|
||||
} from 'features/changeBoardModal/store/slice';
|
||||
import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
@@ -26,7 +27,8 @@ const selectIsModalOpen = createSelector(
|
||||
const ChangeBoardModal = () => {
|
||||
useAssertSingleton('ChangeBoardModal');
|
||||
const dispatch = useAppDispatch();
|
||||
const [selectedBoard, setSelectedBoard] = useState<string | null>();
|
||||
const currentBoardId = useAppSelector(selectSelectedBoardId);
|
||||
const [selectedBoardId, setSelectedBoardId] = useState<string | null>();
|
||||
const { data: boards, isFetching } = useListAllBoardsQuery({ include_archived: true });
|
||||
const isModalOpen = useAppSelector(selectIsModalOpen);
|
||||
const imagesToChange = useAppSelector(selectImagesToChange);
|
||||
@@ -35,15 +37,19 @@ const ChangeBoardModal = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options = useMemo<ComboboxOption[]>(() => {
|
||||
return [{ label: t('boards.uncategorized'), value: 'none' }].concat(
|
||||
(boards ?? []).map((board) => ({
|
||||
label: board.board_name,
|
||||
value: board.board_id,
|
||||
}))
|
||||
);
|
||||
}, [boards, t]);
|
||||
return [{ label: t('boards.uncategorized'), value: 'none' }]
|
||||
.concat(
|
||||
(boards ?? [])
|
||||
.map((board) => ({
|
||||
label: board.board_name,
|
||||
value: board.board_id,
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
)
|
||||
.filter((board) => board.value !== currentBoardId);
|
||||
}, [boards, currentBoardId, t]);
|
||||
|
||||
const value = useMemo(() => options.find((o) => o.value === selectedBoard), [options, selectedBoard]);
|
||||
const value = useMemo(() => options.find((o) => o.value === selectedBoardId), [options, selectedBoardId]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
dispatch(changeBoardReset());
|
||||
@@ -51,27 +57,26 @@ const ChangeBoardModal = () => {
|
||||
}, [dispatch]);
|
||||
|
||||
const handleChangeBoard = useCallback(() => {
|
||||
if (!imagesToChange.length || !selectedBoard) {
|
||||
if (!imagesToChange.length || !selectedBoardId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedBoard === 'none') {
|
||||
if (selectedBoardId === 'none') {
|
||||
removeImagesFromBoard({ image_names: imagesToChange });
|
||||
} else {
|
||||
addImagesToBoard({
|
||||
image_names: imagesToChange,
|
||||
board_id: selectedBoard,
|
||||
board_id: selectedBoardId,
|
||||
});
|
||||
}
|
||||
setSelectedBoard(null);
|
||||
dispatch(changeBoardReset());
|
||||
}, [addImagesToBoard, dispatch, imagesToChange, removeImagesFromBoard, selectedBoard]);
|
||||
}, [addImagesToBoard, dispatch, imagesToChange, removeImagesFromBoard, selectedBoardId]);
|
||||
|
||||
const onChange = useCallback<ComboboxOnChange>((v) => {
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
setSelectedBoard(v.value);
|
||||
setSelectedBoardId(v.value);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -89,7 +94,6 @@ const ChangeBoardModal = () => {
|
||||
{t('boards.movingImagesToBoard', {
|
||||
count: imagesToChange.length,
|
||||
})}
|
||||
:
|
||||
</Text>
|
||||
<FormControl isDisabled={isFetching}>
|
||||
<Combobox
|
||||
|
||||
@@ -10,13 +10,19 @@ import type {
|
||||
ChatGPT4oModelConfig,
|
||||
FLUXKontextModelConfig,
|
||||
FLUXReduxModelConfig,
|
||||
Gemini2_5ModelConfig,
|
||||
IPAdapterModelConfig,
|
||||
} from 'services/api/types';
|
||||
|
||||
type Props = {
|
||||
modelKey: string | null;
|
||||
onChangeModel: (
|
||||
modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig | ChatGPT4oModelConfig | FLUXKontextModelConfig
|
||||
modelConfig:
|
||||
| IPAdapterModelConfig
|
||||
| FLUXReduxModelConfig
|
||||
| ChatGPT4oModelConfig
|
||||
| FLUXKontextModelConfig
|
||||
| Gemini2_5ModelConfig
|
||||
) => void;
|
||||
};
|
||||
|
||||
@@ -28,7 +34,13 @@ export const RefImageModel = memo(({ modelKey, onChangeModel }: Props) => {
|
||||
|
||||
const _onChangeModel = useCallback(
|
||||
(
|
||||
modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig | ChatGPT4oModelConfig | FLUXKontextModelConfig | null
|
||||
modelConfig:
|
||||
| IPAdapterModelConfig
|
||||
| FLUXReduxModelConfig
|
||||
| ChatGPT4oModelConfig
|
||||
| FLUXKontextModelConfig
|
||||
| Gemini2_5ModelConfig
|
||||
| null
|
||||
) => {
|
||||
if (!modelConfig) {
|
||||
return;
|
||||
@@ -39,7 +51,14 @@ export const RefImageModel = memo(({ modelKey, onChangeModel }: Props) => {
|
||||
);
|
||||
|
||||
const getIsDisabled = useCallback(
|
||||
(model: IPAdapterModelConfig | FLUXReduxModelConfig | ChatGPT4oModelConfig | FLUXKontextModelConfig): boolean => {
|
||||
(
|
||||
model:
|
||||
| IPAdapterModelConfig
|
||||
| FLUXReduxModelConfig
|
||||
| ChatGPT4oModelConfig
|
||||
| FLUXKontextModelConfig
|
||||
| Gemini2_5ModelConfig
|
||||
): boolean => {
|
||||
return !areBasesCompatibleForRefImage(mainModelConfig, model);
|
||||
},
|
||||
[mainModelConfig]
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { selectBboxRect, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
|
||||
import { imageNameToImageObject } from 'features/controlLayers/store/util';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { createContext, memo, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { getImageDTOSafe } from 'services/api/endpoints/images';
|
||||
@@ -71,8 +71,8 @@ export const StagingAreaContextProvider = memo(({ children, sessionId }: PropsWi
|
||||
},
|
||||
onAccept: (item, imageDTO) => {
|
||||
const bboxRect = selectBboxRect(store.getState());
|
||||
const { x, y, width, height } = bboxRect;
|
||||
const imageObject = imageNameToImageObject(imageDTO.image_name, { width, height });
|
||||
const { x, y } = bboxRect;
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const selectedEntityIdentifier = selectSelectedEntityIdentifier(store.getState());
|
||||
const overrides: Partial<CanvasRasterLayerState> = {
|
||||
position: { x, y },
|
||||
|
||||
@@ -28,6 +28,7 @@ import type {
|
||||
ControlLoRAConfig,
|
||||
ControlNetConfig,
|
||||
FluxKontextReferenceImageConfig,
|
||||
Gemini2_5ReferenceImageConfig,
|
||||
IPAdapterConfig,
|
||||
T2IAdapterConfig,
|
||||
} from 'features/controlLayers/store/types';
|
||||
@@ -35,6 +36,7 @@ import {
|
||||
initialChatGPT4oReferenceImage,
|
||||
initialControlNet,
|
||||
initialFluxKontextReferenceImage,
|
||||
initialGemini2_5ReferenceImage,
|
||||
initialIPAdapter,
|
||||
initialT2IAdapter,
|
||||
} from 'features/controlLayers/store/util';
|
||||
@@ -76,7 +78,11 @@ export const selectDefaultControlAdapter = createSelector(
|
||||
|
||||
export const getDefaultRefImageConfig = (
|
||||
getState: AppGetState
|
||||
): IPAdapterConfig | ChatGPT4oReferenceImageConfig | FluxKontextReferenceImageConfig => {
|
||||
):
|
||||
| IPAdapterConfig
|
||||
| ChatGPT4oReferenceImageConfig
|
||||
| FluxKontextReferenceImageConfig
|
||||
| Gemini2_5ReferenceImageConfig => {
|
||||
const state = getState();
|
||||
|
||||
const mainModelConfig = selectMainModelConfig(state);
|
||||
@@ -97,6 +103,12 @@ export const getDefaultRefImageConfig = (
|
||||
return config;
|
||||
}
|
||||
|
||||
if (base === 'gemini-2.5') {
|
||||
const config = deepClone(initialGemini2_5ReferenceImage);
|
||||
config.model = zModelIdentifierField.parse(mainModelConfig);
|
||||
return config;
|
||||
}
|
||||
|
||||
// Otherwise, find the first compatible IP Adapter model.
|
||||
const modelConfig = ipAdapterModelConfigs.find((m) => m.base === base);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
selectIsChatGPT4o,
|
||||
selectIsCogView4,
|
||||
selectIsFluxKontext,
|
||||
selectIsGemini2_5,
|
||||
selectIsImagen3,
|
||||
selectIsImagen4,
|
||||
selectIsSD3,
|
||||
@@ -19,21 +20,22 @@ export const useIsEntityTypeEnabled = (entityType: CanvasEntityType) => {
|
||||
const isImagen4 = useAppSelector(selectIsImagen4);
|
||||
const isFluxKontext = useAppSelector(selectIsFluxKontext);
|
||||
const isChatGPT4o = useAppSelector(selectIsChatGPT4o);
|
||||
const isGemini2_5 = useAppSelector(selectIsGemini2_5);
|
||||
|
||||
const isEntityTypeEnabled = useMemo<boolean>(() => {
|
||||
switch (entityType) {
|
||||
case 'regional_guidance':
|
||||
return !isSD3 && !isCogView4 && !isImagen3 && !isImagen4 && !isFluxKontext && !isChatGPT4o;
|
||||
return !isSD3 && !isCogView4 && !isImagen3 && !isImagen4 && !isFluxKontext && !isChatGPT4o && !isGemini2_5;
|
||||
case 'control_layer':
|
||||
return !isSD3 && !isCogView4 && !isImagen3 && !isImagen4 && !isFluxKontext && !isChatGPT4o;
|
||||
return !isSD3 && !isCogView4 && !isImagen3 && !isImagen4 && !isFluxKontext && !isChatGPT4o && !isGemini2_5;
|
||||
case 'inpaint_mask':
|
||||
return !isImagen3 && !isImagen4 && !isFluxKontext && !isChatGPT4o;
|
||||
return !isImagen3 && !isImagen4 && !isFluxKontext && !isChatGPT4o && !isGemini2_5;
|
||||
case 'raster_layer':
|
||||
return !isImagen3 && !isImagen4 && !isFluxKontext && !isChatGPT4o;
|
||||
return !isImagen3 && !isImagen4 && !isFluxKontext && !isChatGPT4o && !isGemini2_5;
|
||||
default:
|
||||
assert<Equals<typeof entityType, never>>(false);
|
||||
}
|
||||
}, [entityType, isSD3, isCogView4, isImagen3, isImagen4, isFluxKontext, isChatGPT4o]);
|
||||
}, [entityType, isSD3, isCogView4, isImagen3, isImagen4, isFluxKontext, isChatGPT4o, isGemini2_5]);
|
||||
|
||||
return isEntityTypeEnabled;
|
||||
};
|
||||
|
||||
@@ -214,6 +214,9 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
const isVisible = this.parent.konva.layer.visible();
|
||||
const isCached = this.konva.objectGroup.isCached();
|
||||
|
||||
// We should also never cache if the entity has no dimensions. Konva will log an error to console like this:
|
||||
// Konva error: Can not cache the node. Width or height of the node equals 0. Caching is skipped.
|
||||
|
||||
if (isVisible && (force || !isCached)) {
|
||||
this.log.trace('Caching object group');
|
||||
this.konva.objectGroup.clearCache();
|
||||
|
||||
@@ -115,7 +115,7 @@ export abstract class CanvasModuleBase {
|
||||
* ```
|
||||
*/
|
||||
destroy: () => void = () => {
|
||||
this.log('Destroying module');
|
||||
this.log.debug('Destroying module');
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ import { objectEquals } from '@observ33r/object-equals';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { withResultAsync } from 'common/util/result';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import type { CanvasEntityBufferObjectRenderer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityBufferObjectRenderer';
|
||||
import type { CanvasEntityFilterer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityFilterer';
|
||||
import type { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityObjectRenderer';
|
||||
@@ -10,12 +11,21 @@ import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'
|
||||
import type { CanvasSegmentAnythingModule } from 'features/controlLayers/konva/CanvasSegmentAnythingModule';
|
||||
import type { CanvasStagingAreaModule } from 'features/controlLayers/konva/CanvasStagingAreaModule';
|
||||
import { getKonvaNodeDebugAttrs, loadImage } from 'features/controlLayers/konva/util';
|
||||
import type { CanvasImageState } from 'features/controlLayers/store/types';
|
||||
import type { CanvasImageState, Dimensions } from 'features/controlLayers/store/types';
|
||||
import { t } from 'i18next';
|
||||
import Konva from 'konva';
|
||||
import type { Logger } from 'roarr';
|
||||
import type { JsonObject } from 'roarr/dist/types';
|
||||
import { getImageDTOSafe } from 'services/api/endpoints/images';
|
||||
|
||||
type CanvasObjectImageConfig = {
|
||||
usePhysicalDimensions: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_CONFIG: CanvasObjectImageConfig = {
|
||||
usePhysicalDimensions: false,
|
||||
};
|
||||
|
||||
export class CanvasObjectImage extends CanvasModuleBase {
|
||||
readonly type = 'object_image';
|
||||
readonly id: string;
|
||||
@@ -30,6 +40,9 @@ export class CanvasObjectImage extends CanvasModuleBase {
|
||||
readonly log: Logger;
|
||||
|
||||
state: CanvasImageState;
|
||||
|
||||
config: CanvasObjectImageConfig;
|
||||
|
||||
konva: {
|
||||
group: Konva.Group;
|
||||
placeholder: { group: Konva.Group; rect: Konva.Rect; text: Konva.Text };
|
||||
@@ -47,7 +60,8 @@ export class CanvasObjectImage extends CanvasModuleBase {
|
||||
| CanvasEntityBufferObjectRenderer
|
||||
| CanvasStagingAreaModule
|
||||
| CanvasSegmentAnythingModule
|
||||
| CanvasEntityFilterer
|
||||
| CanvasEntityFilterer,
|
||||
config = DEFAULT_CONFIG
|
||||
) {
|
||||
super();
|
||||
this.id = state.id;
|
||||
@@ -55,6 +69,7 @@ export class CanvasObjectImage extends CanvasModuleBase {
|
||||
this.manager = parent.manager;
|
||||
this.path = this.manager.buildPath(this);
|
||||
this.log = this.manager.buildLogger(this);
|
||||
this.config = config;
|
||||
|
||||
this.log.debug({ state }, 'Creating module');
|
||||
|
||||
@@ -116,7 +131,10 @@ export class CanvasObjectImage extends CanvasModuleBase {
|
||||
const imageElementResult = await withResultAsync(() => loadImage(imageDTO.image_url, true));
|
||||
if (imageElementResult.isErr()) {
|
||||
// Image loading failed (e.g. the URL to the "physical" image is invalid)
|
||||
this.onFailedToLoadImage(t('controlLayers.unableToLoadImage', 'Unable to load image'));
|
||||
this.onFailedToLoadImage(
|
||||
t('controlLayers.unableToLoadImage', 'Unable to load image'),
|
||||
parseify(imageElementResult.error)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -139,7 +157,10 @@ export class CanvasObjectImage extends CanvasModuleBase {
|
||||
const imageElementResult = await withResultAsync(() => loadImage(dataURL, false));
|
||||
if (imageElementResult.isErr()) {
|
||||
// Image loading failed (e.g. the URL to the "physical" image is invalid)
|
||||
this.onFailedToLoadImage(t('controlLayers.unableToLoadImage', 'Unable to load image'));
|
||||
this.onFailedToLoadImage(
|
||||
t('controlLayers.unableToLoadImage', 'Unable to load image'),
|
||||
parseify(imageElementResult.error)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -148,8 +169,8 @@ export class CanvasObjectImage extends CanvasModuleBase {
|
||||
this.updateImageElement();
|
||||
};
|
||||
|
||||
onFailedToLoadImage = (message: string) => {
|
||||
this.log({ image: this.state.image }, message);
|
||||
onFailedToLoadImage = (message: string, error?: JsonObject) => {
|
||||
this.log.error({ image: this.state.image, error }, message);
|
||||
this.konva.image?.visible(false);
|
||||
this.isLoading = false;
|
||||
this.isError = true;
|
||||
@@ -157,9 +178,22 @@ export class CanvasObjectImage extends CanvasModuleBase {
|
||||
this.konva.placeholder.group.visible(true);
|
||||
};
|
||||
|
||||
getDimensions = (): Dimensions => {
|
||||
if (this.config.usePhysicalDimensions && this.imageElement) {
|
||||
return {
|
||||
width: this.imageElement.width,
|
||||
height: this.imageElement.height,
|
||||
};
|
||||
}
|
||||
return {
|
||||
width: this.state.image.width,
|
||||
height: this.state.image.height,
|
||||
};
|
||||
};
|
||||
|
||||
updateImageElement = () => {
|
||||
if (this.imageElement) {
|
||||
const { width, height } = this.state.image;
|
||||
const { width, height } = this.getDimensions();
|
||||
|
||||
if (this.konva.image) {
|
||||
this.log.trace('Updating Konva image attrs');
|
||||
@@ -196,7 +230,6 @@ export class CanvasObjectImage extends CanvasModuleBase {
|
||||
this.log.trace({ state }, 'Updating image');
|
||||
|
||||
const { image } = state;
|
||||
const { width, height } = image;
|
||||
|
||||
if (force || (!objectEquals(this.state, state) && !this.isLoading)) {
|
||||
const release = await this.mutex.acquire();
|
||||
@@ -212,7 +245,7 @@ export class CanvasObjectImage extends CanvasModuleBase {
|
||||
}
|
||||
}
|
||||
|
||||
this.konva.image?.setAttrs({ width, height });
|
||||
this.konva.image?.setAttrs(this.getDimensions());
|
||||
this.state = state;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -230,7 +230,16 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
if (imageSrc) {
|
||||
const image = this._getImageFromSrc(imageSrc, width, height);
|
||||
if (!this.image) {
|
||||
this.image = new CanvasObjectImage({ id: 'staging-area-image', type: 'image', image }, this);
|
||||
this.image = new CanvasObjectImage({ id: 'staging-area-image', type: 'image', image }, this, {
|
||||
// Some models do not make guarantees about their output dimensions. This flag allows the staged images to
|
||||
// render at their real dimensions, instead of the bbox size.
|
||||
//
|
||||
// When the image source is an image name, it is a final output image. In that case, we should use its
|
||||
// physical dimensions. Otherwise, if it is a dataURL, that means it is a progress image. These come in at
|
||||
// a smaller resolution and need to be stretched to fill the bbox, so we do not use the physical
|
||||
// dimensions in that case.
|
||||
usePhysicalDimensions: imageSrc.type === 'imageName',
|
||||
});
|
||||
await this.image.update(this.image.state, true);
|
||||
this.konva.group.add(this.image.konva.group);
|
||||
} else if (this.image.isLoading || this.image.isError) {
|
||||
|
||||
@@ -231,7 +231,7 @@ export class CanvasStateApiModule extends CanvasModuleBase {
|
||||
/**
|
||||
* Sets the drawing color, pushing state to redux.
|
||||
*/
|
||||
setColor = (color: RgbaColor) => {
|
||||
setColor = (color: Partial<RgbaColor>) => {
|
||||
return this.store.dispatch(settingsColorChanged(color));
|
||||
};
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ const ALL_ANCHORS: string[] = [
|
||||
'bottom-center',
|
||||
'bottom-right',
|
||||
];
|
||||
const CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
||||
const NO_ANCHORS: string[] = [];
|
||||
|
||||
/**
|
||||
@@ -344,9 +343,23 @@ export class CanvasBboxToolModule extends CanvasModuleBase {
|
||||
let width = roundToMultipleMin(this.konva.proxyRect.width() * this.konva.proxyRect.scaleX(), gridSize);
|
||||
let height = roundToMultipleMin(this.konva.proxyRect.height() * this.konva.proxyRect.scaleY(), gridSize);
|
||||
|
||||
// If shift is held and we are resizing from a corner, retain aspect ratio - needs special handling. We skip this
|
||||
// if alt/opt is held - this requires math too big for my brain.
|
||||
if (shift && CORNER_ANCHORS.includes(anchor) && !alt) {
|
||||
// When resizing the bbox using the transformer, we may need to do some extra math to maintain the current aspect
|
||||
// ratio. Need to check a few things to determine if we should be maintaining the aspect ratio or not.
|
||||
let shouldMaintainAspectRatio = false;
|
||||
|
||||
if (alt) {
|
||||
// If alt is held, we are doing center-anchored transforming. In this case, maintaining aspect ratio is rather
|
||||
// complicated.
|
||||
shouldMaintainAspectRatio = false;
|
||||
} else if (this.manager.stateApi.getBbox().aspectRatio.isLocked) {
|
||||
// When the aspect ratio is locked, holding shift means we SHOULD NOT maintain the aspect ratio
|
||||
shouldMaintainAspectRatio = !shift;
|
||||
} else {
|
||||
// When the aspect ratio is not locked, holding shift means we SHOULD maintain aspect ratio
|
||||
shouldMaintainAspectRatio = shift;
|
||||
}
|
||||
|
||||
if (shouldMaintainAspectRatio) {
|
||||
// Fit the bbox to the last aspect ratio
|
||||
let fittedWidth = Math.sqrt(width * height * this.$aspectRatioBuffer.get());
|
||||
let fittedHeight = fittedWidth / this.$aspectRatioBuffer.get();
|
||||
@@ -387,7 +400,7 @@ export class CanvasBboxToolModule extends CanvasModuleBase {
|
||||
|
||||
// Update the aspect ratio buffer whenever the shift key is not held - this allows for a nice UX where you can start
|
||||
// a transform, get the right aspect ratio, then hold shift to lock it in.
|
||||
if (!shift) {
|
||||
if (!shouldMaintainAspectRatio) {
|
||||
this.$aspectRatioBuffer.set(bboxRect.width / bboxRect.height);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -289,6 +289,14 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase {
|
||||
this.manager.stage.setCursor('none');
|
||||
};
|
||||
|
||||
getCanPick = () => {
|
||||
if (this.manager.stage.getIsDragging()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the color picker tool preview on the canvas.
|
||||
*/
|
||||
@@ -298,6 +306,11 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.getCanPick()) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorPos = this.parent.$cursorPos.get();
|
||||
|
||||
if (!cursorPos) {
|
||||
@@ -406,11 +419,21 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase {
|
||||
};
|
||||
|
||||
onStagePointerUp = (_e: KonvaEventObject<PointerEvent>) => {
|
||||
const color = this.$colorUnderCursor.get();
|
||||
this.manager.stateApi.setColor({ ...color, a: color.a / 255 });
|
||||
if (!this.getCanPick()) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { a: _, ...color } = this.$colorUnderCursor.get();
|
||||
this.manager.stateApi.setColor(color);
|
||||
};
|
||||
|
||||
onStagePointerMove = (_e: KonvaEventObject<PointerEvent>) => {
|
||||
if (!this.getCanPick()) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.syncColorUnderCursor();
|
||||
};
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
const selectedEntityAdapter = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
|
||||
if (this.manager.stage.getIsDragging()) {
|
||||
this.tools.view.syncCursorStyle();
|
||||
stage.setCursor('grabbing');
|
||||
} else if (tool === 'view') {
|
||||
this.tools.view.syncCursorStyle();
|
||||
} else if (segmentingAdapter) {
|
||||
|
||||
@@ -134,8 +134,8 @@ const slice = createSlice({
|
||||
settingsEraserWidthChanged: (state, action: PayloadAction<CanvasSettingsState['eraserWidth']>) => {
|
||||
state.eraserWidth = Math.round(action.payload);
|
||||
},
|
||||
settingsColorChanged: (state, action: PayloadAction<CanvasSettingsState['color']>) => {
|
||||
state.color = action.payload;
|
||||
settingsColorChanged: (state, action: PayloadAction<Partial<CanvasSettingsState['color']>>) => {
|
||||
state.color = { ...state.color, ...action.payload };
|
||||
},
|
||||
settingsInvertScrollForToolWidthChanged: (
|
||||
state,
|
||||
|
||||
@@ -72,12 +72,14 @@ import {
|
||||
CHATGPT_ASPECT_RATIOS,
|
||||
DEFAULT_ASPECT_RATIO_CONFIG,
|
||||
FLUX_KONTEXT_ASPECT_RATIOS,
|
||||
GEMINI_2_5_ASPECT_RATIOS,
|
||||
getEntityIdentifier,
|
||||
getInitialCanvasState,
|
||||
IMAGEN_ASPECT_RATIOS,
|
||||
isChatGPT4oAspectRatioID,
|
||||
isFluxKontextAspectRatioID,
|
||||
isFLUXReduxConfig,
|
||||
isGemini2_5AspectRatioID,
|
||||
isImagenAspectRatioID,
|
||||
isIPAdapterConfig,
|
||||
zCanvasState,
|
||||
@@ -1144,6 +1146,12 @@ const slice = createSlice({
|
||||
state.bbox.rect.height = height;
|
||||
state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height;
|
||||
state.bbox.aspectRatio.isLocked = true;
|
||||
} else if (state.bbox.modelBase === 'gemini-2.5' && isGemini2_5AspectRatioID(id)) {
|
||||
const { width, height } = GEMINI_2_5_ASPECT_RATIOS[id];
|
||||
state.bbox.rect.width = width;
|
||||
state.bbox.rect.height = height;
|
||||
state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height;
|
||||
state.bbox.aspectRatio.isLocked = true;
|
||||
} else if (state.bbox.modelBase === 'flux-kontext' && isFluxKontextAspectRatioID(id)) {
|
||||
const { width, height } = FLUX_KONTEXT_ASPECT_RATIOS[id];
|
||||
state.bbox.rect.width = width;
|
||||
|
||||
@@ -11,15 +11,26 @@ import {
|
||||
CHATGPT_ASPECT_RATIOS,
|
||||
DEFAULT_ASPECT_RATIO_CONFIG,
|
||||
FLUX_KONTEXT_ASPECT_RATIOS,
|
||||
GEMINI_2_5_ASPECT_RATIOS,
|
||||
getInitialParamsState,
|
||||
IMAGEN_ASPECT_RATIOS,
|
||||
isChatGPT4oAspectRatioID,
|
||||
isFluxKontextAspectRatioID,
|
||||
isGemini2_5AspectRatioID,
|
||||
isImagenAspectRatioID,
|
||||
zParamsState,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { calculateNewSize } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
|
||||
import { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
|
||||
import {
|
||||
API_BASE_MODELS,
|
||||
CLIP_SKIP_MAP,
|
||||
SUPPORTS_ASPECT_RATIO_BASE_MODELS,
|
||||
SUPPORTS_NEGATIVE_PROMPT_BASE_MODELS,
|
||||
SUPPORTS_OPTIMIZED_DENOISING_BASE_MODELS,
|
||||
SUPPORTS_PIXEL_DIMENSIONS_BASE_MODELS,
|
||||
SUPPORTS_REF_IMAGES_BASE_MODELS,
|
||||
SUPPORTS_SEED_BASE_MODELS,
|
||||
} from 'features/parameters/types/constants';
|
||||
import type {
|
||||
ParameterCanvasCoherenceMode,
|
||||
ParameterCFGRescaleMultiplier,
|
||||
@@ -107,6 +118,14 @@ const slice = createSlice({
|
||||
return;
|
||||
}
|
||||
|
||||
if (API_BASE_MODELS.includes(model.base)) {
|
||||
state.dimensions.aspectRatio.isLocked = true;
|
||||
state.dimensions.aspectRatio.value = 1;
|
||||
state.dimensions.aspectRatio.id = '1:1';
|
||||
state.dimensions.rect.width = 1024;
|
||||
state.dimensions.rect.height = 1024;
|
||||
}
|
||||
|
||||
applyClipSkip(state, model, state.clipSkip);
|
||||
},
|
||||
vaeSelected: (state, action: PayloadAction<ParameterVAEModel | null>) => {
|
||||
@@ -290,6 +309,12 @@ const slice = createSlice({
|
||||
state.dimensions.rect.height = height;
|
||||
state.dimensions.aspectRatio.value = state.dimensions.rect.width / state.dimensions.rect.height;
|
||||
state.dimensions.aspectRatio.isLocked = true;
|
||||
} else if (state.model?.base === 'gemini-2.5' && isGemini2_5AspectRatioID(id)) {
|
||||
const { width, height } = GEMINI_2_5_ASPECT_RATIOS[id];
|
||||
state.dimensions.rect.width = width;
|
||||
state.dimensions.rect.height = height;
|
||||
state.dimensions.aspectRatio.value = state.dimensions.rect.width / state.dimensions.rect.height;
|
||||
state.dimensions.aspectRatio.isLocked = true;
|
||||
} else if (state.model?.base === 'flux-kontext' && isFluxKontextAspectRatioID(id)) {
|
||||
const { width, height } = FLUX_KONTEXT_ASPECT_RATIOS[id];
|
||||
state.dimensions.rect.width = width;
|
||||
@@ -477,7 +502,6 @@ export const selectIsSD3 = createParamsSelector((params) => params.model?.base =
|
||||
export const selectIsCogView4 = createParamsSelector((params) => params.model?.base === 'cogview4');
|
||||
export const selectIsImagen3 = createParamsSelector((params) => params.model?.base === 'imagen3');
|
||||
export const selectIsImagen4 = createParamsSelector((params) => params.model?.base === 'imagen4');
|
||||
export const selectIsFluxKontextApi = createParamsSelector((params) => params.model?.base === 'flux-kontext');
|
||||
export const selectIsFluxKontext = createParamsSelector((params) => {
|
||||
if (params.model?.base === 'flux-kontext') {
|
||||
return true;
|
||||
@@ -488,6 +512,7 @@ export const selectIsFluxKontext = createParamsSelector((params) => {
|
||||
return false;
|
||||
});
|
||||
export const selectIsChatGPT4o = createParamsSelector((params) => params.model?.base === 'chatgpt-4o');
|
||||
export const selectIsGemini2_5 = createParamsSelector((params) => params.model?.base === 'gemini-2.5');
|
||||
|
||||
export const selectModel = createParamsSelector((params) => params.model);
|
||||
export const selectModelKey = createParamsSelector((params) => params.model?.key);
|
||||
@@ -523,8 +548,32 @@ export const selectNegativePrompt = createParamsSelector((params) => params.nega
|
||||
export const selectNegativePromptWithFallback = createParamsSelector((params) => params.negativePrompt ?? '');
|
||||
export const selectHasNegativePrompt = createParamsSelector((params) => params.negativePrompt !== null);
|
||||
export const selectModelSupportsNegativePrompt = createSelector(
|
||||
[selectIsFLUX, selectIsChatGPT4o, selectIsFluxKontext],
|
||||
(isFLUX, isChatGPT4o, isFluxKontext) => !isFLUX && !isChatGPT4o && !isFluxKontext
|
||||
selectModel,
|
||||
(model) => !!model && SUPPORTS_NEGATIVE_PROMPT_BASE_MODELS.includes(model.base)
|
||||
);
|
||||
export const selectModelSupportsSeed = createSelector(
|
||||
selectModel,
|
||||
(model) => !!model && SUPPORTS_SEED_BASE_MODELS.includes(model.base)
|
||||
);
|
||||
export const selectModelSupportsRefImages = createSelector(
|
||||
selectModel,
|
||||
(model) => !!model && SUPPORTS_REF_IMAGES_BASE_MODELS.includes(model.base)
|
||||
);
|
||||
export const selectModelSupportsAspectRatio = createSelector(
|
||||
selectModel,
|
||||
(model) => !!model && SUPPORTS_ASPECT_RATIO_BASE_MODELS.includes(model.base)
|
||||
);
|
||||
export const selectModelSupportsPixelDimensions = createSelector(
|
||||
selectModel,
|
||||
(model) => !!model && SUPPORTS_PIXEL_DIMENSIONS_BASE_MODELS.includes(model.base)
|
||||
);
|
||||
export const selectIsApiBaseModel = createSelector(
|
||||
selectModel,
|
||||
(model) => !!model && API_BASE_MODELS.includes(model.base)
|
||||
);
|
||||
export const selectModelSupportsOptimizedDenoising = createSelector(
|
||||
selectModel,
|
||||
(model) => !!model && SUPPORTS_OPTIMIZED_DENOISING_BASE_MODELS.includes(model.base)
|
||||
);
|
||||
export const selectScheduler = createParamsSelector((params) => params.scheduler);
|
||||
export const selectSeamlessXAxis = createParamsSelector((params) => params.seamlessXAxis);
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
initialChatGPT4oReferenceImage,
|
||||
initialFluxKontextReferenceImage,
|
||||
initialFLUXRedux,
|
||||
initialGemini2_5ReferenceImage,
|
||||
initialIPAdapter,
|
||||
} from './util';
|
||||
|
||||
@@ -136,6 +137,16 @@ const slice = createSlice({
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity.config.model.base === 'gemini-2.5') {
|
||||
// Switching to Gemini 2.5 Flash Preview (nano banana) ref image
|
||||
entity.config = {
|
||||
...initialGemini2_5ReferenceImage,
|
||||
image: entity.config.image,
|
||||
model: entity.config.model,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
entity.config.model.base === 'flux-kontext' ||
|
||||
(entity.config.model.base === 'flux' && entity.config.model.name?.toLowerCase().includes('kontext'))
|
||||
|
||||
@@ -264,6 +264,13 @@ const zChatGPT4oReferenceImageConfig = z.object({
|
||||
});
|
||||
export type ChatGPT4oReferenceImageConfig = z.infer<typeof zChatGPT4oReferenceImageConfig>;
|
||||
|
||||
const zGemini2_5ReferenceImageConfig = z.object({
|
||||
type: z.literal('gemini_2_5_reference_image'),
|
||||
image: zImageWithDims.nullable(),
|
||||
model: zModelIdentifierField.nullable(),
|
||||
});
|
||||
export type Gemini2_5ReferenceImageConfig = z.infer<typeof zGemini2_5ReferenceImageConfig>;
|
||||
|
||||
const zFluxKontextReferenceImageConfig = z.object({
|
||||
type: z.literal('flux_kontext_reference_image'),
|
||||
image: zImageWithDims.nullable(),
|
||||
@@ -286,6 +293,7 @@ export const zRefImageState = z.object({
|
||||
zFLUXReduxConfig,
|
||||
zChatGPT4oReferenceImageConfig,
|
||||
zFluxKontextReferenceImageConfig,
|
||||
zGemini2_5ReferenceImageConfig,
|
||||
]),
|
||||
});
|
||||
export type RefImageState = z.infer<typeof zRefImageState>;
|
||||
@@ -298,10 +306,15 @@ export const isFLUXReduxConfig = (config: RefImageState['config']): config is FL
|
||||
export const isChatGPT4oReferenceImageConfig = (
|
||||
config: RefImageState['config']
|
||||
): config is ChatGPT4oReferenceImageConfig => config.type === 'chatgpt_4o_reference_image';
|
||||
|
||||
export const isFluxKontextReferenceImageConfig = (
|
||||
config: RefImageState['config']
|
||||
): config is FluxKontextReferenceImageConfig => config.type === 'flux_kontext_reference_image';
|
||||
|
||||
export const isGemini2_5ReferenceImageConfig = (
|
||||
config: RefImageState['config']
|
||||
): config is Gemini2_5ReferenceImageConfig => config.type === 'gemini_2_5_reference_image';
|
||||
|
||||
const zFillStyle = z.enum(['solid', 'grid', 'crosshatch', 'diagonal', 'horizontal', 'vertical']);
|
||||
export type FillStyle = z.infer<typeof zFillStyle>;
|
||||
export const isFillStyle = (v: unknown): v is FillStyle => zFillStyle.safeParse(v).success;
|
||||
@@ -447,6 +460,14 @@ export const CHATGPT_ASPECT_RATIOS: Record<ChatGPT4oAspectRatio, Dimensions> = {
|
||||
'2:3': { width: 1024, height: 1536 },
|
||||
} as const;
|
||||
|
||||
export const zGemini2_5AspectRatioID = z.enum(['1:1']);
|
||||
type Gemini2_5AspectRatio = z.infer<typeof zGemini2_5AspectRatioID>;
|
||||
export const isGemini2_5AspectRatioID = (v: unknown): v is Gemini2_5AspectRatio =>
|
||||
zGemini2_5AspectRatioID.safeParse(v).success;
|
||||
export const GEMINI_2_5_ASPECT_RATIOS: Record<Gemini2_5AspectRatio, Dimensions> = {
|
||||
'1:1': { width: 1024, height: 1024 },
|
||||
} as const;
|
||||
|
||||
export const zFluxKontextAspectRatioID = z.enum(['21:9', '16:9', '4:3', '1:1', '3:4', '9:16', '9:21']);
|
||||
type FluxKontextAspectRatio = z.infer<typeof zFluxKontextAspectRatioID>;
|
||||
export const isFluxKontextAspectRatioID = (v: unknown): v is z.infer<typeof zFluxKontextAspectRatioID> =>
|
||||
@@ -491,6 +512,8 @@ const zBboxState = z.object({
|
||||
});
|
||||
|
||||
const zDimensionsState = z.object({
|
||||
// TODO(psyche): There is no concept of x/y coords for the dimensions state here... It's just width and height.
|
||||
// Remove the extraneous data.
|
||||
rect: z.object({
|
||||
x: z.number().int(),
|
||||
y: z.number().int(),
|
||||
@@ -655,7 +678,12 @@ export const getInitialRefImagesState = (): RefImagesState => ({
|
||||
|
||||
export const zCanvasReferenceImageState_OLD = zCanvasEntityBase.extend({
|
||||
type: z.literal('reference_image'),
|
||||
ipAdapter: z.discriminatedUnion('type', [zIPAdapterConfig, zFLUXReduxConfig, zChatGPT4oReferenceImageConfig]),
|
||||
ipAdapter: z.discriminatedUnion('type', [
|
||||
zIPAdapterConfig,
|
||||
zFLUXReduxConfig,
|
||||
zChatGPT4oReferenceImageConfig,
|
||||
zGemini2_5ReferenceImageConfig,
|
||||
]),
|
||||
});
|
||||
|
||||
export const zCanvasMetadata = z.object({
|
||||
|
||||
@@ -10,9 +10,9 @@ import type {
|
||||
ChatGPT4oReferenceImageConfig,
|
||||
ControlLoRAConfig,
|
||||
ControlNetConfig,
|
||||
Dimensions,
|
||||
FluxKontextReferenceImageConfig,
|
||||
FLUXReduxConfig,
|
||||
Gemini2_5ReferenceImageConfig,
|
||||
ImageWithDims,
|
||||
IPAdapterConfig,
|
||||
RefImageState,
|
||||
@@ -38,22 +38,6 @@ export const imageDTOToImageObject = (imageDTO: ImageDTO, overrides?: Partial<Ca
|
||||
};
|
||||
};
|
||||
|
||||
export const imageNameToImageObject = (
|
||||
imageName: string,
|
||||
dimensions: Dimensions,
|
||||
overrides?: Partial<CanvasImageState>
|
||||
): CanvasImageState => {
|
||||
return {
|
||||
id: getPrefixedId('image'),
|
||||
type: 'image',
|
||||
image: {
|
||||
image_name: imageName,
|
||||
...dimensions,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO): ImageWithDims => ({
|
||||
image_name,
|
||||
width,
|
||||
@@ -105,6 +89,11 @@ export const initialChatGPT4oReferenceImage: ChatGPT4oReferenceImageConfig = {
|
||||
image: null,
|
||||
model: null,
|
||||
};
|
||||
export const initialGemini2_5ReferenceImage: Gemini2_5ReferenceImageConfig = {
|
||||
type: 'gemini_2_5_reference_image',
|
||||
image: null,
|
||||
model: null,
|
||||
};
|
||||
export const initialFluxKontextReferenceImage: FluxKontextReferenceImageConfig = {
|
||||
type: 'flux_kontext_reference_image',
|
||||
image: null,
|
||||
|
||||
@@ -118,6 +118,9 @@ const buildOnClick =
|
||||
const start = Math.min(lastClickedIndex, currentClickedIndex);
|
||||
const end = Math.max(lastClickedIndex, currentClickedIndex);
|
||||
const imagesToSelect = imageNames.slice(start, end + 1);
|
||||
if (currentClickedIndex < lastClickedIndex) {
|
||||
imagesToSelect.reverse();
|
||||
}
|
||||
dispatch(selectionChanged(uniq(selection.concat(imagesToSelect))));
|
||||
}
|
||||
} else if (ctrlKey || metaKey) {
|
||||
|
||||
@@ -83,7 +83,15 @@ export const ImageViewerContextProvider = memo((props: PropsWithChildren) => {
|
||||
// switch to the final image automatically. In this case, we clear the progress image immediately.
|
||||
//
|
||||
// We also clear the progress image if the queue item is canceled or failed, as there is no final image to show.
|
||||
if (data.status === 'canceled' || data.status === 'failed' || !autoSwitch) {
|
||||
if (
|
||||
data.status === 'canceled' ||
|
||||
data.status === 'failed' ||
|
||||
!autoSwitch ||
|
||||
// When the origin is 'canvas' and destination is 'canvas' (without a ':<session id>' suffix), that means the
|
||||
// image is going to be added to the staging area. In this case, we need to clear the progress image else it
|
||||
// will be stuck on the viewer.
|
||||
(data.origin === 'canvas' && data.destination !== 'canvas')
|
||||
) {
|
||||
$progressEvent.set(null);
|
||||
$progressImage.set(null);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export const BASE_COLOR_MAP: Record<BaseModelType, string> = {
|
||||
imagen4: 'pink',
|
||||
'chatgpt-4o': 'pink',
|
||||
'flux-kontext': 'pink',
|
||||
'gemini-2.5': 'pink',
|
||||
};
|
||||
|
||||
const ModelBaseBadge = ({ base }: Props) => {
|
||||
|
||||
@@ -1,35 +1,43 @@
|
||||
import { CompositeNumberInput } from '@invoke-ai/ui-library';
|
||||
import { Button, CompositeNumberInput } from '@invoke-ai/ui-library';
|
||||
import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField';
|
||||
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
|
||||
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
|
||||
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import type { NodeFieldFloatSettings } from 'features/nodes/types/workflow';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiShuffleBold } from 'react-icons/pi';
|
||||
|
||||
export const FloatFieldInput = memo(
|
||||
(
|
||||
props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate, { settings?: NodeFieldFloatSettings }>
|
||||
) => {
|
||||
const { nodeId, field, fieldTemplate, settings } = props;
|
||||
const { defaultValue, onChange, min, max, step, fineStep } = useFloatField(
|
||||
nodeId,
|
||||
field.name,
|
||||
fieldTemplate,
|
||||
settings
|
||||
);
|
||||
const { defaultValue, onChange, min, max, step, fineStep, constrainValue, showShuffle, randomizeValue } =
|
||||
useFloatField(nodeId, field.name, fieldTemplate, settings);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<CompositeNumberInput
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={field.value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
fineStep={fineStep}
|
||||
className={NO_DRAG_CLASS}
|
||||
flex="1 1 0"
|
||||
/>
|
||||
<>
|
||||
<CompositeNumberInput
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={field.value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
fineStep={fineStep}
|
||||
className={NO_DRAG_CLASS}
|
||||
flex="1 1 0"
|
||||
constrainValue={constrainValue}
|
||||
/>
|
||||
{showShuffle && (
|
||||
<Button size="sm" isDisabled={false} onClick={randomizeValue} leftIcon={<PiShuffleBold />} flexShrink={0}>
|
||||
{t('workflows.builder.shuffle')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { CompositeNumberInput, CompositeSlider } from '@invoke-ai/ui-library';
|
||||
import { Button, CompositeNumberInput, CompositeSlider } from '@invoke-ai/ui-library';
|
||||
import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField';
|
||||
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
|
||||
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
|
||||
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import type { NodeFieldFloatSettings } from 'features/nodes/types/workflow';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiShuffleBold } from 'react-icons/pi';
|
||||
|
||||
export const FloatFieldInputAndSlider = memo(
|
||||
(
|
||||
props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate, { settings?: NodeFieldFloatSettings }>
|
||||
) => {
|
||||
const { nodeId, field, fieldTemplate, settings } = props;
|
||||
const { defaultValue, onChange, min, max, step, fineStep } = useFloatField(
|
||||
nodeId,
|
||||
field.name,
|
||||
fieldTemplate,
|
||||
settings
|
||||
);
|
||||
const { defaultValue, onChange, min, max, step, fineStep, constrainValue, showShuffle, randomizeValue } =
|
||||
useFloatField(nodeId, field.name, fieldTemplate, settings);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -43,7 +43,13 @@ export const FloatFieldInputAndSlider = memo(
|
||||
fineStep={fineStep}
|
||||
className={NO_DRAG_CLASS}
|
||||
flex="1 1 0"
|
||||
constrainValue={constrainValue}
|
||||
/>
|
||||
{showShuffle && (
|
||||
<Button size="sm" isDisabled={false} onClick={randomizeValue} leftIcon={<PiShuffleBold />} flexShrink={0}>
|
||||
{t('workflows.builder.shuffle')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,37 +1,48 @@
|
||||
import { CompositeSlider } from '@invoke-ai/ui-library';
|
||||
import { Button, CompositeSlider } from '@invoke-ai/ui-library';
|
||||
import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField';
|
||||
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
|
||||
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
|
||||
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import type { NodeFieldFloatSettings } from 'features/nodes/types/workflow';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiShuffleBold } from 'react-icons/pi';
|
||||
|
||||
export const FloatFieldSlider = memo(
|
||||
(
|
||||
props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate, { settings?: NodeFieldFloatSettings }>
|
||||
) => {
|
||||
const { nodeId, field, fieldTemplate, settings } = props;
|
||||
const { defaultValue, onChange, min, max, step, fineStep } = useFloatField(
|
||||
const { defaultValue, onChange, min, max, step, fineStep, showShuffle, randomizeValue } = useFloatField(
|
||||
nodeId,
|
||||
field.name,
|
||||
fieldTemplate,
|
||||
settings
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<CompositeSlider
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={field.value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
fineStep={fineStep}
|
||||
className={NO_DRAG_CLASS}
|
||||
marks
|
||||
withThumbTooltip
|
||||
flex="1 1 0"
|
||||
/>
|
||||
<>
|
||||
<CompositeSlider
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={field.value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
fineStep={fineStep}
|
||||
className={NO_DRAG_CLASS}
|
||||
marks
|
||||
withThumbTooltip
|
||||
flex="1 1 0"
|
||||
/>
|
||||
{showShuffle && (
|
||||
<Button size="sm" isDisabled={false} onClick={randomizeValue} leftIcon={<PiShuffleBold />} flexShrink={0}>
|
||||
{t('workflows.builder.shuffle')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NUMPY_RAND_MAX } from 'app/constants';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import randomFloat from 'common/util/randomFloat';
|
||||
import { roundDownToMultiple, roundUpToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import { isNil } from 'es-toolkit/compat';
|
||||
import { fieldFloatValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
@@ -11,9 +12,9 @@ export const useFloatField = (
|
||||
nodeId: string,
|
||||
fieldName: string,
|
||||
fieldTemplate: FloatFieldInputTemplate,
|
||||
overrides: { min?: number; max?: number; step?: number } = {}
|
||||
overrides: { showShuffle?: boolean; min?: number; max?: number; step?: number } = {}
|
||||
) => {
|
||||
const { min: overrideMin, max: overrideMax, step: overrideStep } = overrides;
|
||||
const { showShuffle, min: overrideMin, max: overrideMax, step: overrideStep } = overrides;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const step = useMemo(() => {
|
||||
@@ -36,6 +37,13 @@ export const useFloatField = (
|
||||
return fieldTemplate.multipleOf;
|
||||
}, [fieldTemplate.multipleOf, overrideStep]);
|
||||
|
||||
const baseStep = useMemo(() => {
|
||||
if (isNil(fieldTemplate.multipleOf)) {
|
||||
return undefined;
|
||||
}
|
||||
return fieldTemplate.multipleOf;
|
||||
}, [fieldTemplate.multipleOf]);
|
||||
|
||||
const min = useMemo(() => {
|
||||
let min = -NUMPY_RAND_MAX;
|
||||
|
||||
@@ -66,8 +74,8 @@ export const useFloatField = (
|
||||
|
||||
const constrainValue = useCallback(
|
||||
(v: number) =>
|
||||
constrainNumber(v, { min, max, step: step }, { min: overrideMin, max: overrideMax, step: overrideStep }),
|
||||
[max, min, overrideMax, overrideMin, overrideStep, step]
|
||||
constrainNumber(v, { min, max, step: baseStep }, { min: overrideMin, max: overrideMax, step: overrideStep }),
|
||||
[max, min, overrideMax, overrideMin, overrideStep, baseStep]
|
||||
);
|
||||
|
||||
const onChange = useCallback(
|
||||
@@ -77,6 +85,11 @@ export const useFloatField = (
|
||||
[dispatch, fieldName, nodeId]
|
||||
);
|
||||
|
||||
const randomizeValue = useCallback(() => {
|
||||
const value = Number((Math.round(randomFloat(min, max) / step) * step).toFixed(10));
|
||||
dispatch(fieldFloatValueChanged({ nodeId, fieldName, value }));
|
||||
}, [dispatch, fieldName, nodeId, min, max, step]);
|
||||
|
||||
return {
|
||||
defaultValue: fieldTemplate.default,
|
||||
onChange,
|
||||
@@ -85,5 +98,7 @@ export const useFloatField = (
|
||||
step,
|
||||
fineStep,
|
||||
constrainValue,
|
||||
showShuffle,
|
||||
randomizeValue,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAddRemoveFormElement } from 'features/nodes/components/sidePanel/builder/use-add-remove-form-element';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiMinusBold, PiPlusBold } from 'react-icons/pi';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export const InputFieldAddRemoveFormRoot = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isAddedToRoot, addNodeFieldToRoot, removeNodeFieldFromRoot } = useAddRemoveFormElement(nodeId, fieldName);
|
||||
|
||||
const description = useMemo(() => {
|
||||
return isAddedToRoot ? t('workflows.builder.removeFromForm') : t('workflows.builder.addToForm');
|
||||
}, [isAddedToRoot, t]);
|
||||
|
||||
const icon = useMemo(() => {
|
||||
return isAddedToRoot ? <PiMinusBold /> : <PiPlusBold />;
|
||||
}, [isAddedToRoot]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
return isAddedToRoot ? removeNodeFieldFromRoot() : addNodeFieldToRoot();
|
||||
}, [isAddedToRoot, addNodeFieldToRoot, removeNodeFieldFromRoot]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
tooltip={description}
|
||||
aria-label={description}
|
||||
icon={icon}
|
||||
pointerEvents="auto"
|
||||
size="xs"
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
InputFieldAddRemoveFormRoot.displayName = 'InputFieldAddRemoveFormRoot';
|
||||
@@ -1,30 +0,0 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAddNodeFieldToRoot } from 'features/nodes/components/sidePanel/builder/use-add-node-field-to-root';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiPlusBold } from 'react-icons/pi';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export const InputFieldAddToFormRoot = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isAddedToRoot, addNodeFieldToRoot } = useAddNodeFieldToRoot(nodeId, fieldName);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
tooltip={t('workflows.builder.addToForm')}
|
||||
aria-label={t('workflows.builder.addToForm')}
|
||||
icon={<PiPlusBold />}
|
||||
pointerEvents="auto"
|
||||
size="xs"
|
||||
onClick={addNodeFieldToRoot}
|
||||
isDisabled={isAddedToRoot}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
InputFieldAddToFormRoot.displayName = 'InputFieldAddToFormRoot';
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { InputFieldAddToFormRoot } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldAddToFormRoot';
|
||||
import { InputFieldDescriptionPopover } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldDescriptionPopover';
|
||||
import { InputFieldHandle } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldHandle';
|
||||
import { InputFieldResetToDefaultValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToDefaultValueIconButton';
|
||||
@@ -12,6 +11,7 @@ import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
|
||||
import type { FieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { memo, useRef } from 'react';
|
||||
|
||||
import { InputFieldAddRemoveFormRoot } from './InputFieldAddRemoveFormRoot';
|
||||
import { InputFieldRenderer } from './InputFieldRenderer';
|
||||
import { InputFieldTitle } from './InputFieldTitle';
|
||||
import { InputFieldWrapper } from './InputFieldWrapper';
|
||||
@@ -113,7 +113,7 @@ const DirectField = memo(({ nodeId, fieldName, isInvalid, isConnected, fieldTemp
|
||||
<Flex className="direct-field-action-buttons">
|
||||
<InputFieldDescriptionPopover nodeId={nodeId} fieldName={fieldName} />
|
||||
<InputFieldResetToDefaultValueIconButton nodeId={nodeId} fieldName={fieldName} />
|
||||
<InputFieldAddToFormRoot nodeId={nodeId} fieldName={fieldName} />
|
||||
<InputFieldAddRemoveFormRoot nodeId={nodeId} fieldName={fieldName} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { CompositeNumberInput } from '@invoke-ai/ui-library';
|
||||
import { Button, CompositeNumberInput } from '@invoke-ai/ui-library';
|
||||
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
|
||||
import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField';
|
||||
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
|
||||
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import type { NodeFieldIntegerSettings } from 'features/nodes/types/workflow';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiShuffleBold } from 'react-icons/pi';
|
||||
|
||||
export const IntegerFieldInput = memo(
|
||||
(
|
||||
@@ -15,26 +17,31 @@ export const IntegerFieldInput = memo(
|
||||
>
|
||||
) => {
|
||||
const { nodeId, field, fieldTemplate, settings } = props;
|
||||
const { defaultValue, onChange, min, max, step, fineStep, constrainValue } = useIntegerField(
|
||||
nodeId,
|
||||
field.name,
|
||||
fieldTemplate,
|
||||
settings
|
||||
);
|
||||
const { defaultValue, onChange, min, max, step, fineStep, constrainValue, showShuffle, randomizeValue } =
|
||||
useIntegerField(nodeId, field.name, fieldTemplate, settings);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<CompositeNumberInput
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={field.value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
fineStep={fineStep}
|
||||
className={NO_DRAG_CLASS}
|
||||
flex="1 1 0"
|
||||
constrainValue={constrainValue}
|
||||
/>
|
||||
<>
|
||||
<CompositeNumberInput
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={field.value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
fineStep={fineStep}
|
||||
className={NO_DRAG_CLASS}
|
||||
flex="1 1 0"
|
||||
constrainValue={constrainValue}
|
||||
/>
|
||||
{showShuffle && (
|
||||
<Button size="sm" isDisabled={false} onClick={randomizeValue} leftIcon={<PiShuffleBold />} flexShrink={0}>
|
||||
{t('workflows.builder.shuffle')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { CompositeNumberInput, CompositeSlider } from '@invoke-ai/ui-library';
|
||||
import { Button, CompositeNumberInput, CompositeSlider } from '@invoke-ai/ui-library';
|
||||
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
|
||||
import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField';
|
||||
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
|
||||
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import type { NodeFieldIntegerSettings } from 'features/nodes/types/workflow';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiShuffleBold } from 'react-icons/pi';
|
||||
|
||||
export const IntegerFieldInputAndSlider = memo(
|
||||
(
|
||||
@@ -15,12 +17,10 @@ export const IntegerFieldInputAndSlider = memo(
|
||||
>
|
||||
) => {
|
||||
const { nodeId, field, fieldTemplate, settings } = props;
|
||||
const { defaultValue, onChange, min, max, step, fineStep } = useIntegerField(
|
||||
nodeId,
|
||||
field.name,
|
||||
fieldTemplate,
|
||||
settings
|
||||
);
|
||||
const { defaultValue, onChange, min, max, step, fineStep, constrainValue, showShuffle, randomizeValue } =
|
||||
useIntegerField(nodeId, field.name, fieldTemplate, settings);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -47,7 +47,13 @@ export const IntegerFieldInputAndSlider = memo(
|
||||
fineStep={fineStep}
|
||||
className={NO_DRAG_CLASS}
|
||||
flex="1 1 0"
|
||||
constrainValue={constrainValue}
|
||||
/>
|
||||
{showShuffle && (
|
||||
<Button size="sm" isDisabled={false} onClick={randomizeValue} leftIcon={<PiShuffleBold />} flexShrink={0}>
|
||||
{t('workflows.builder.shuffle')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { CompositeSlider } from '@invoke-ai/ui-library';
|
||||
import { Button, CompositeSlider } from '@invoke-ai/ui-library';
|
||||
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
|
||||
import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField';
|
||||
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
|
||||
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import type { NodeFieldIntegerSettings } from 'features/nodes/types/workflow';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiShuffleBold } from 'react-icons/pi';
|
||||
|
||||
export const IntegerFieldSlider = memo(
|
||||
(
|
||||
@@ -15,27 +17,36 @@ export const IntegerFieldSlider = memo(
|
||||
>
|
||||
) => {
|
||||
const { nodeId, field, fieldTemplate, settings } = props;
|
||||
const { defaultValue, onChange, min, max, step, fineStep } = useIntegerField(
|
||||
const { defaultValue, onChange, min, max, step, fineStep, showShuffle, randomizeValue } = useIntegerField(
|
||||
nodeId,
|
||||
field.name,
|
||||
fieldTemplate,
|
||||
settings
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<CompositeSlider
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={field.value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
fineStep={fineStep}
|
||||
className={NO_DRAG_CLASS}
|
||||
marks
|
||||
withThumbTooltip
|
||||
flex="1 1 0"
|
||||
/>
|
||||
<>
|
||||
<CompositeSlider
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={field.value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
fineStep={fineStep}
|
||||
className={NO_DRAG_CLASS}
|
||||
marks
|
||||
withThumbTooltip
|
||||
flex="1 1 0"
|
||||
/>
|
||||
{showShuffle && (
|
||||
<Button size="sm" isDisabled={false} onClick={randomizeValue} leftIcon={<PiShuffleBold />} flexShrink={0}>
|
||||
{t('workflows.builder.shuffle')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NUMPY_RAND_MAX } from 'app/constants';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import randomInt from 'common/util/randomInt';
|
||||
import { roundDownToMultiple, roundUpToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import { isNil } from 'es-toolkit/compat';
|
||||
import { fieldIntegerValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
@@ -11,9 +12,9 @@ export const useIntegerField = (
|
||||
nodeId: string,
|
||||
fieldName: string,
|
||||
fieldTemplate: IntegerFieldInputTemplate,
|
||||
overrides: { min?: number; max?: number; step?: number } = {}
|
||||
overrides: { showShuffle?: boolean; min?: number; max?: number; step?: number } = {}
|
||||
) => {
|
||||
const { min: overrideMin, max: overrideMax, step: overrideStep } = overrides;
|
||||
const { showShuffle, min: overrideMin, max: overrideMax, step: overrideStep } = overrides;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const step = useMemo(() => {
|
||||
@@ -65,8 +66,7 @@ export const useIntegerField = (
|
||||
}, [fieldTemplate.exclusiveMaximum, fieldTemplate.maximum, overrideMax, step]);
|
||||
|
||||
const constrainValue = useCallback(
|
||||
(v: number) =>
|
||||
constrainNumber(v, { min, max, step: step }, { min: overrideMin, max: overrideMax, step: overrideStep }),
|
||||
(v: number) => constrainNumber(v, { min, max, step }, { min: overrideMin, max: overrideMax, step: overrideStep }),
|
||||
[max, min, overrideMax, overrideMin, overrideStep, step]
|
||||
);
|
||||
|
||||
@@ -77,6 +77,11 @@ export const useIntegerField = (
|
||||
[dispatch, fieldName, nodeId]
|
||||
);
|
||||
|
||||
const randomizeValue = useCallback(() => {
|
||||
const value = Math.round(randomInt(min, max) / step) * step;
|
||||
dispatch(fieldIntegerValueChanged({ nodeId, fieldName, value }));
|
||||
}, [dispatch, fieldName, nodeId, min, max, step]);
|
||||
|
||||
return {
|
||||
defaultValue: fieldTemplate.default,
|
||||
onChange,
|
||||
@@ -85,5 +90,7 @@ export const useIntegerField = (
|
||||
step,
|
||||
fineStep,
|
||||
constrainValue,
|
||||
showShuffle,
|
||||
randomizeValue,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ type Props = {
|
||||
export const NodeFieldElementFloatSettings = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<SettingShuffle id={id} settings={settings} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
|
||||
<SettingComponent
|
||||
id={id}
|
||||
settings={settings}
|
||||
@@ -36,6 +37,29 @@ export const NodeFieldElementFloatSettings = memo(({ id, settings, nodeId, field
|
||||
});
|
||||
NodeFieldElementFloatSettings.displayName = 'NodeFieldElementFloatSettings';
|
||||
|
||||
const SettingShuffle = memo(({ id, settings }: Props) => {
|
||||
const { showShuffle } = settings;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const toggleShowShuffle = useCallback(() => {
|
||||
const newSettings: NodeFieldFloatSettings = {
|
||||
...settings,
|
||||
showShuffle: !showShuffle,
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
|
||||
}, [dispatch, id, settings, showShuffle]);
|
||||
|
||||
return (
|
||||
<FormControl>
|
||||
<FormLabel flex={1}>{t('workflows.builder.showShuffle')}</FormLabel>
|
||||
<Switch size="sm" isChecked={showShuffle} onChange={toggleShowShuffle} />
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
SettingShuffle.displayName = 'SettingShuffle';
|
||||
|
||||
const SettingComponent = memo(({ id, settings }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -23,6 +23,7 @@ type Props = {
|
||||
export const NodeFieldElementIntegerSettings = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<SettingShuffle id={id} settings={settings} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
|
||||
<SettingComponent
|
||||
id={id}
|
||||
settings={settings}
|
||||
@@ -37,6 +38,29 @@ export const NodeFieldElementIntegerSettings = memo(({ id, settings, nodeId, fie
|
||||
});
|
||||
NodeFieldElementIntegerSettings.displayName = 'NodeFieldElementIntegerSettings';
|
||||
|
||||
const SettingShuffle = memo(({ id, settings }: Props) => {
|
||||
const { showShuffle } = settings;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const toggleShowShuffle = useCallback(() => {
|
||||
const newSettings: NodeFieldIntegerSettings = {
|
||||
...settings,
|
||||
showShuffle: !showShuffle,
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
|
||||
}, [dispatch, id, settings, showShuffle]);
|
||||
|
||||
return (
|
||||
<FormControl>
|
||||
<FormLabel flex={1}>{t('workflows.builder.showShuffle')}</FormLabel>
|
||||
<Switch size="sm" isChecked={showShuffle} onChange={toggleShowShuffle} />
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
SettingShuffle.displayName = 'SettingShuffle';
|
||||
|
||||
const SettingComponent = memo(({ id, settings }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
import { selectFormRootElementId, selectNodesSlice, selectWorkflowForm } from 'features/nodes/store/selectors';
|
||||
import type { FieldInputTemplate, StatefulFieldValue } from 'features/nodes/types/field';
|
||||
import type { ElementId, FormElement } from 'features/nodes/types/workflow';
|
||||
import { buildNodeFieldElement, isContainerElement } from 'features/nodes/types/workflow';
|
||||
import { buildNodeFieldElement, isContainerElement, isNodeFieldElement } from 'features/nodes/types/workflow';
|
||||
import type { RefObject } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
@@ -121,6 +121,29 @@ const useElementExists = () => {
|
||||
return _elementExists;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a node field element exists in the form.
|
||||
*
|
||||
* @param form The form to check
|
||||
* @param nodeId The id of the node
|
||||
* @param fieldName The name of field
|
||||
*
|
||||
* @returns True if the element exists, false otherwise
|
||||
*/
|
||||
const useNodeFieldElementExists = () => {
|
||||
const store = useAppStore();
|
||||
const nodeFieldElementExists = useCallback(
|
||||
(nodeId: string, fieldName: string): boolean => {
|
||||
const form = selectWorkflowForm(store.getState());
|
||||
return Object.values(form.elements)
|
||||
.filter(isNodeFieldElement)
|
||||
.some((el) => el.data.fieldIdentifier.nodeId === nodeId && el.data.fieldIdentifier.fieldName === fieldName);
|
||||
},
|
||||
[store]
|
||||
);
|
||||
return nodeFieldElementExists;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper around `getAllowedDropRegions` that provides the form state from the store.
|
||||
* @see {@link getAllowedDropRegions}
|
||||
@@ -368,6 +391,7 @@ export const useFormElementDnd = (
|
||||
const [activeDropRegion, setActiveDropRegion] = useState<CenterOrEdge | null>(null);
|
||||
const getElement = useGetElement();
|
||||
const getAllowedDropRegions = useGetAllowedDropRegions();
|
||||
const nodeFieldElementExists = useNodeFieldElementExists();
|
||||
|
||||
useEffect(() => {
|
||||
if (isRootElement) {
|
||||
@@ -401,7 +425,7 @@ export const useFormElementDnd = (
|
||||
// TODO(psyche): This causes a kinda jittery behaviour - need a better heuristic to determine stickiness
|
||||
getIsSticky: () => false,
|
||||
canDrop: ({ source }) => {
|
||||
if (isNodeFieldDndData(source.data)) {
|
||||
if (isNodeFieldDndData(source.data) && !nodeFieldElementExists(source.data.nodeId, source.data.fieldName)) {
|
||||
return true;
|
||||
}
|
||||
if (isFormElementDndData(source.data)) {
|
||||
@@ -449,7 +473,15 @@ export const useFormElementDnd = (
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [dragHandleRef, draggableRef, elementId, getAllowedDropRegions, getElement, isRootElement]);
|
||||
}, [
|
||||
dragHandleRef,
|
||||
draggableRef,
|
||||
elementId,
|
||||
getAllowedDropRegions,
|
||||
getElement,
|
||||
nodeFieldElementExists,
|
||||
isRootElement,
|
||||
]);
|
||||
|
||||
return [activeDropRegion, isDragging] as const;
|
||||
};
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
|
||||
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplateOrThrow';
|
||||
import { formElementAdded } from 'features/nodes/store/nodesSlice';
|
||||
import { buildSelectWorkflowFormNodeExists, selectFormRootElementId } from 'features/nodes/store/selectors';
|
||||
import { formElementAdded, formElementRemoved } from 'features/nodes/store/nodesSlice';
|
||||
import { buildSelectWorkflowFormNodeElement, selectFormRootElementId } from 'features/nodes/store/selectors';
|
||||
import { buildNodeFieldElement } from 'features/nodes/types/workflow';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export const useAddNodeFieldToRoot = (nodeId: string, fieldName: string) => {
|
||||
export const useAddRemoveFormElement = (nodeId: string, fieldName: string) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const rootElementId = useAppSelector(selectFormRootElementId);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(fieldName);
|
||||
const field = useInputFieldInstance(fieldName);
|
||||
const selectWorkflowFormNodeExists = useMemo(
|
||||
() => buildSelectWorkflowFormNodeExists(nodeId, fieldName),
|
||||
const selectWorkflowFormNodeElement = useMemo(
|
||||
() => buildSelectWorkflowFormNodeElement(nodeId, fieldName),
|
||||
[nodeId, fieldName]
|
||||
);
|
||||
const isAddedToRoot = useAppSelector(selectWorkflowFormNodeExists);
|
||||
const workflowFormNodeElement = useAppSelector(selectWorkflowFormNodeElement);
|
||||
const isAddedToRoot = useMemo(() => {
|
||||
return !!workflowFormNodeElement;
|
||||
}, [workflowFormNodeElement]);
|
||||
|
||||
const addNodeFieldToRoot = useCallback(() => {
|
||||
const element = buildNodeFieldElement(nodeId, fieldName, fieldTemplate.type);
|
||||
@@ -28,5 +31,16 @@ export const useAddNodeFieldToRoot = (nodeId: string, fieldName: string) => {
|
||||
);
|
||||
}, [nodeId, fieldName, fieldTemplate.type, dispatch, rootElementId, field.value]);
|
||||
|
||||
return { isAddedToRoot, addNodeFieldToRoot };
|
||||
const removeNodeFieldFromRoot = useCallback(() => {
|
||||
if (!workflowFormNodeElement) {
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
formElementRemoved({
|
||||
id: workflowFormNodeElement.id,
|
||||
})
|
||||
);
|
||||
}, [workflowFormNodeElement, dispatch]);
|
||||
|
||||
return { isAddedToRoot, addNodeFieldToRoot, removeNodeFieldFromRoot };
|
||||
};
|
||||
@@ -215,6 +215,9 @@ const slice = createSlice({
|
||||
initialState: getInitialState(),
|
||||
reducers: {
|
||||
nodesChanged: (state, action: PayloadAction<NodeChange<AnyNode>[]>) => {
|
||||
// TODO(psyche): The below TS issue was recently fixed upstream. Need to upgrade @xyflow/react and then we
|
||||
// should be able to remove this cast.
|
||||
//
|
||||
// In v12.7.0, @xyflow/react added a `domAttributes` property to the node data. One DOM attribute is
|
||||
// defaultValue, which may have a value of type `readonly string[]`. This conflicts with the immer-
|
||||
// provided Draft type, used internally by RTK. We don't use `domAttributes`, so we can safely cast
|
||||
|
||||
@@ -103,7 +103,10 @@ export const selectWorkflowFormNodeFieldFieldIdentifiersDeduped = createSelector
|
||||
);
|
||||
|
||||
export const buildSelectElement = (id: string) => createNodesSelector((workflow) => workflow.form?.elements[id]);
|
||||
export const buildSelectWorkflowFormNodeExists = (nodeId: string, fieldName: string) =>
|
||||
createSelector(selectWorkflowFormNodeFieldFieldIdentifiersDeduped, (identifiers) =>
|
||||
identifiers.some((identifier) => identifier.nodeId === nodeId && identifier.fieldName === fieldName)
|
||||
export const buildSelectWorkflowFormNodeElement = (nodeId: string, fieldName: string) =>
|
||||
createSelector(selectNodeFieldElements, (elements) =>
|
||||
elements.find(
|
||||
(element) =>
|
||||
element.data.fieldIdentifier.nodeId === nodeId && element.data.fieldIdentifier.fieldName === fieldName
|
||||
)
|
||||
);
|
||||
|
||||
@@ -76,6 +76,7 @@ const zBaseModel = z.enum([
|
||||
'imagen4',
|
||||
'chatgpt-4o',
|
||||
'flux-kontext',
|
||||
'gemini-2.5',
|
||||
]);
|
||||
export type BaseModelType = z.infer<typeof zBaseModel>;
|
||||
export const zMainModelBase = z.enum([
|
||||
@@ -89,6 +90,7 @@ export const zMainModelBase = z.enum([
|
||||
'imagen4',
|
||||
'chatgpt-4o',
|
||||
'flux-kontext',
|
||||
'gemini-2.5',
|
||||
]);
|
||||
type MainModelBase = z.infer<typeof zMainModelBase>;
|
||||
export const isMainModelBase = (base: unknown): base is MainModelBase => zMainModelBase.safeParse(base).success;
|
||||
|
||||
@@ -74,6 +74,7 @@ export const NODE_FIELD_CLASS_NAME = `form-builder-${NODE_FIELD_TYPE}`;
|
||||
const FLOAT_FIELD_SETTINGS_TYPE = 'float-field-config';
|
||||
const zNodeFieldFloatSettings = z.object({
|
||||
type: z.literal(FLOAT_FIELD_SETTINGS_TYPE).default(FLOAT_FIELD_SETTINGS_TYPE),
|
||||
showShuffle: z.boolean().default(false),
|
||||
component: zNumberComponent.default('number-input'),
|
||||
min: z.number().optional(),
|
||||
max: z.number().optional(),
|
||||
@@ -84,6 +85,7 @@ export type NodeFieldFloatSettings = z.infer<typeof zNodeFieldFloatSettings>;
|
||||
const INTEGER_FIELD_CONFIG_TYPE = 'integer-field-config';
|
||||
const zNodeFieldIntegerSettings = z.object({
|
||||
type: z.literal(INTEGER_FIELD_CONFIG_TYPE).default(INTEGER_FIELD_CONFIG_TYPE),
|
||||
showShuffle: z.boolean().default(false),
|
||||
component: zNumberComponent.default('number-input'),
|
||||
min: z.number().optional(),
|
||||
max: z.number().optional(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
|
||||
type NumberConstraints = { min: number; max: number; step: number };
|
||||
type NumberConstraints = { min: number; max: number; step?: number };
|
||||
|
||||
/**
|
||||
* Constrain a number to a range and round to the nearest multiple of a given value.
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
|
||||
import { isGemini2_5ReferenceImageConfig } from 'features/controlLayers/store/types';
|
||||
import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators';
|
||||
import type { ImageField } from 'features/nodes/types/common';
|
||||
import { Graph } from 'features/nodes/util/graph/generation/Graph';
|
||||
import { selectCanvasOutputFields } from 'features/nodes/util/graph/graphBuilderUtils';
|
||||
import type { GraphBuilderArg, GraphBuilderReturn } from 'features/nodes/util/graph/types';
|
||||
import { UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
|
||||
import { t } from 'i18next';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
const log = logger('system');
|
||||
|
||||
export const buildGemini2_5Graph = (arg: GraphBuilderArg): GraphBuilderReturn => {
|
||||
const { generationMode, state, manager } = arg;
|
||||
|
||||
if (generationMode !== 'txt2img') {
|
||||
throw new UnsupportedGenerationModeError(
|
||||
t('toast.imagenIncompatibleGenerationMode', { model: 'Gemini 2.5 Flash Preview' })
|
||||
);
|
||||
}
|
||||
|
||||
log.debug({ generationMode, manager: manager?.id }, 'Building Gemini 2.5 graph');
|
||||
|
||||
const model = selectMainModelConfig(state);
|
||||
|
||||
const refImages = selectRefImagesSlice(state);
|
||||
|
||||
assert(model, 'No model selected');
|
||||
assert(model.base === 'gemini-2.5', 'Selected model is not a Gemini 2.5 API model');
|
||||
|
||||
const validRefImages = refImages.entities
|
||||
.filter((entity) => entity.isEnabled)
|
||||
.filter((entity) => isGemini2_5ReferenceImageConfig(entity.config))
|
||||
.filter((entity) => getGlobalReferenceImageWarnings(entity, model).length === 0)
|
||||
.toReversed(); // sends them in order they are displayed in the list
|
||||
|
||||
let reference_images: ImageField[] | undefined = undefined;
|
||||
|
||||
if (validRefImages.length > 0) {
|
||||
reference_images = [];
|
||||
for (const entity of validRefImages) {
|
||||
assert(entity.config.image, 'Image is required for reference image');
|
||||
reference_images.push({
|
||||
image_name: entity.config.image.image_name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const g = new Graph(getPrefixedId('gemini_2_5_txt2img_graph'));
|
||||
const positivePrompt = g.addNode({
|
||||
id: getPrefixedId('positive_prompt'),
|
||||
type: 'string',
|
||||
});
|
||||
const geminiImage = g.addNode({
|
||||
// @ts-expect-error: These nodes are not available in the OSS application
|
||||
type: 'google_gemini_generate_image',
|
||||
reference_images,
|
||||
...selectCanvasOutputFields(state),
|
||||
});
|
||||
|
||||
g.addEdge(
|
||||
positivePrompt,
|
||||
'value',
|
||||
geminiImage,
|
||||
// @ts-expect-error: These nodes are not available in the OSS application
|
||||
'positive_prompt'
|
||||
);
|
||||
g.addEdgeToMetadata(positivePrompt, 'value', 'positive_prompt');
|
||||
g.upsertMetadata({
|
||||
model: Graph.getModelMetadataField(model),
|
||||
});
|
||||
return {
|
||||
g,
|
||||
positivePrompt,
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,10 @@
|
||||
import type { FormLabelProps } from '@invoke-ai/ui-library';
|
||||
import { Flex, FormControlGroup } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
selectModelSupportsAspectRatio,
|
||||
selectModelSupportsPixelDimensions,
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
import { BboxAspectRatioSelect } from 'features/parameters/components/Bbox/BboxAspectRatioSelect';
|
||||
import { BboxHeight } from 'features/parameters/components/Bbox/BboxHeight';
|
||||
import { BboxLockAspectRatioButton } from 'features/parameters/components/Bbox/BboxLockAspectRatioButton';
|
||||
@@ -7,9 +12,17 @@ import { BboxPreview } from 'features/parameters/components/Bbox/BboxPreview';
|
||||
import { BboxSetOptimalSizeButton } from 'features/parameters/components/Bbox/BboxSetOptimalSizeButton';
|
||||
import { BboxSwapDimensionsButton } from 'features/parameters/components/Bbox/BboxSwapDimensionsButton';
|
||||
import { BboxWidth } from 'features/parameters/components/Bbox/BboxWidth';
|
||||
import { PixelDimensionsUnsupportedAlert } from 'features/parameters/components/PixelDimensionsUnsupportedAlert';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const BboxSettings = memo(() => {
|
||||
const supportsAspectRatio = useAppSelector(selectModelSupportsAspectRatio);
|
||||
const supportsPixelDimensions = useAppSelector(selectModelSupportsPixelDimensions);
|
||||
|
||||
if (!supportsAspectRatio) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex gap={4} alignItems="center">
|
||||
<Flex gap={4} flexDirection="column" width="full">
|
||||
@@ -17,11 +30,20 @@ export const BboxSettings = memo(() => {
|
||||
<Flex gap={4}>
|
||||
<BboxAspectRatioSelect />
|
||||
<BboxSwapDimensionsButton />
|
||||
<BboxLockAspectRatioButton />
|
||||
<BboxSetOptimalSizeButton />
|
||||
{supportsPixelDimensions && (
|
||||
<>
|
||||
<BboxLockAspectRatioButton />
|
||||
<BboxSetOptimalSizeButton />
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
<BboxWidth />
|
||||
<BboxHeight />
|
||||
{supportsPixelDimensions && (
|
||||
<>
|
||||
<BboxWidth />
|
||||
<BboxHeight />
|
||||
</>
|
||||
)}
|
||||
{!supportsPixelDimensions && <PixelDimensionsUnsupportedAlert />}
|
||||
</FormControlGroup>
|
||||
</Flex>
|
||||
<Flex w="108px" h="108px" flexShrink={0} flexGrow={0} alignItems="center" justifyContent="center">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
|
||||
import { selectIsApiBaseModel } from 'features/controlLayers/store/paramsSlice';
|
||||
|
||||
export const useIsBboxSizeLocked = () => {
|
||||
const isStaging = useCanvasIsStaging();
|
||||
const isApiModel = useIsApiModel();
|
||||
|
||||
const isApiModel = useAppSelector(selectIsApiBaseModel);
|
||||
return isApiModel || isStaging;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { FormLabelProps } from '@invoke-ai/ui-library';
|
||||
import { Flex, FormControlGroup } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
selectModelSupportsAspectRatio,
|
||||
selectModelSupportsPixelDimensions,
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
import { PixelDimensionsUnsupportedAlert } from 'features/parameters/components/PixelDimensionsUnsupportedAlert';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { DimensionsAspectRatioSelect } from './DimensionsAspectRatioSelect';
|
||||
@@ -11,6 +17,13 @@ import { DimensionsSwapButton } from './DimensionsSwapButton';
|
||||
import { DimensionsWidth } from './DimensionsWidth';
|
||||
|
||||
export const Dimensions = memo(() => {
|
||||
const supportsAspectRatio = useAppSelector(selectModelSupportsAspectRatio);
|
||||
const supportsPixelDimensions = useAppSelector(selectModelSupportsPixelDimensions);
|
||||
|
||||
if (!supportsAspectRatio) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex gap={4} alignItems="center">
|
||||
<Flex gap={4} flexDirection="column" width="full">
|
||||
@@ -18,11 +31,20 @@ export const Dimensions = memo(() => {
|
||||
<Flex gap={4}>
|
||||
<DimensionsAspectRatioSelect />
|
||||
<DimensionsSwapButton />
|
||||
<DimensionsLockAspectRatioButton />
|
||||
<DimensionsSetOptimalSizeButton />
|
||||
{supportsPixelDimensions && (
|
||||
<>
|
||||
<DimensionsLockAspectRatioButton />
|
||||
<DimensionsSetOptimalSizeButton />
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
<DimensionsWidth />
|
||||
<DimensionsHeight />
|
||||
{supportsPixelDimensions && (
|
||||
<>
|
||||
<DimensionsWidth />
|
||||
<DimensionsHeight />
|
||||
</>
|
||||
)}
|
||||
{!supportsPixelDimensions && <PixelDimensionsUnsupportedAlert />}
|
||||
</FormControlGroup>
|
||||
</Flex>
|
||||
<Flex w="108px" h="108px" flexShrink={0} flexGrow={0} alignItems="center" justifyContent="center">
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
selectAspectRatioID,
|
||||
selectIsChatGPT4o,
|
||||
selectIsFluxKontext,
|
||||
selectIsGemini2_5,
|
||||
selectIsImagen3,
|
||||
selectIsImagen4,
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
zAspectRatioID,
|
||||
zChatGPT4oAspectRatioID,
|
||||
zFluxKontextAspectRatioID,
|
||||
zGemini2_5AspectRatioID,
|
||||
zImagen3AspectRatioID,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
@@ -29,6 +31,7 @@ export const DimensionsAspectRatioSelect = memo(() => {
|
||||
const isChatGPT4o = useAppSelector(selectIsChatGPT4o);
|
||||
const isImagen4 = useAppSelector(selectIsImagen4);
|
||||
const isFluxKontext = useAppSelector(selectIsFluxKontext);
|
||||
const isGemini2_5 = useAppSelector(selectIsGemini2_5);
|
||||
const options = useMemo(() => {
|
||||
// Imagen3 and ChatGPT4o have different aspect ratio options, and do not support freeform sizes
|
||||
if (isImagen3 || isImagen4) {
|
||||
@@ -40,9 +43,12 @@ export const DimensionsAspectRatioSelect = memo(() => {
|
||||
if (isFluxKontext) {
|
||||
return zFluxKontextAspectRatioID.options;
|
||||
}
|
||||
if (isGemini2_5) {
|
||||
return zGemini2_5AspectRatioID.options;
|
||||
}
|
||||
// All other models
|
||||
return zAspectRatioID.options;
|
||||
}, [isImagen3, isChatGPT4o, isImagen4, isFluxKontext]);
|
||||
}, [isImagen3, isChatGPT4o, isImagen4, isFluxKontext, isGemini2_5]);
|
||||
|
||||
const onChange = useCallback<ChangeEventHandler<HTMLSelectElement>>(
|
||||
(e) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { heightChanged, selectHeight } from 'features/controlLayers/store/paramsSlice';
|
||||
import { heightChanged, selectHeight, selectIsApiBaseModel } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectGridSize, selectOptimalDimension } from 'features/controlLayers/store/selectors';
|
||||
import { selectHeightConfig } from 'features/system/store/configSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
@@ -14,6 +14,7 @@ export const DimensionsHeight = memo(() => {
|
||||
const height = useAppSelector(selectHeight);
|
||||
const config = useAppSelector(selectHeightConfig);
|
||||
const gridSize = useAppSelector(selectGridSize);
|
||||
const isApiModel = useAppSelector(selectIsApiBaseModel);
|
||||
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
@@ -28,7 +29,7 @@ export const DimensionsHeight = memo(() => {
|
||||
);
|
||||
|
||||
return (
|
||||
<FormControl>
|
||||
<FormControl isDisabled={isApiModel}>
|
||||
<InformationalPopover feature="paramHeight">
|
||||
<FormLabel>{t('parameters.height')}</FormLabel>
|
||||
</InformationalPopover>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { aspectRatioLockToggled, selectAspectRatioIsLocked } from 'features/controlLayers/store/paramsSlice';
|
||||
import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
|
||||
import {
|
||||
aspectRatioLockToggled,
|
||||
selectAspectRatioIsLocked,
|
||||
selectIsApiBaseModel,
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiLockSimpleFill, PiLockSimpleOpenBold } from 'react-icons/pi';
|
||||
@@ -10,7 +13,7 @@ export const DimensionsLockAspectRatioButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const isLocked = useAppSelector(selectAspectRatioIsLocked);
|
||||
const isApiModel = useIsApiModel();
|
||||
const isApiModel = useAppSelector(selectIsApiBaseModel);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(aspectRatioLockToggled());
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectHeight, selectWidth, sizeOptimized } from 'features/controlLayers/store/paramsSlice';
|
||||
import {
|
||||
selectHeight,
|
||||
selectIsApiBaseModel,
|
||||
selectWidth,
|
||||
sizeOptimized,
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectOptimalDimension } from 'features/controlLayers/store/selectors';
|
||||
import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
|
||||
import { getIsSizeTooLarge, getIsSizeTooSmall } from 'features/parameters/util/optimalDimension';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -11,7 +15,7 @@ import { PiSparkleFill } from 'react-icons/pi';
|
||||
export const DimensionsSetOptimalSizeButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const isApiModel = useIsApiModel();
|
||||
const isApiModel = useAppSelector(selectIsApiBaseModel);
|
||||
const width = useAppSelector(selectWidth);
|
||||
const height = useAppSelector(selectHeight);
|
||||
const optimalDimension = useAppSelector(selectOptimalDimension);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { selectWidth, widthChanged } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectIsApiBaseModel, selectWidth, widthChanged } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectGridSize, selectOptimalDimension } from 'features/controlLayers/store/selectors';
|
||||
import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
|
||||
import { selectWidthConfig } from 'features/system/store/configSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -14,7 +13,7 @@ export const DimensionsWidth = memo(() => {
|
||||
const width = useAppSelector(selectWidth);
|
||||
const optimalDimension = useAppSelector(selectOptimalDimension);
|
||||
const config = useAppSelector(selectWidthConfig);
|
||||
const isApiModel = useIsApiModel();
|
||||
const isApiModel = useAppSelector(selectIsApiBaseModel);
|
||||
const gridSize = useAppSelector(selectGridSize);
|
||||
|
||||
const onChange = useCallback(
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Alert, Text } from '@invoke-ai/ui-library';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const PixelDimensionsUnsupportedAlert = memo(() => {
|
||||
return (
|
||||
<Alert status="info" borderRadius="base" flexDir="column" gap={2} overflow="unset">
|
||||
<Text fontSize="sm" color="base.100">
|
||||
Select an aspect ratio to control the size of the resulting image from this model.
|
||||
</Text>
|
||||
</Alert>
|
||||
);
|
||||
});
|
||||
|
||||
PixelDimensionsUnsupportedAlert.displayName = 'PixelDimensionsUnsupportedAlert';
|
||||
@@ -1,19 +1,24 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { RefImageList } from 'features/controlLayers/components/RefImage/RefImageList';
|
||||
import { selectHasNegativePrompt, selectModelSupportsNegativePrompt } from 'features/controlLayers/store/paramsSlice';
|
||||
import {
|
||||
selectHasNegativePrompt,
|
||||
selectModelSupportsNegativePrompt,
|
||||
selectModelSupportsRefImages,
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt';
|
||||
import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const Prompts = memo(() => {
|
||||
const modelSupportsNegativePrompt = useAppSelector(selectModelSupportsNegativePrompt);
|
||||
const modelSupportsRefImages = useAppSelector(selectModelSupportsRefImages);
|
||||
const hasNegativePrompt = useAppSelector(selectHasNegativePrompt);
|
||||
return (
|
||||
<Flex flexDir="column" gap={2}>
|
||||
<ParamPositivePrompt />
|
||||
{modelSupportsNegativePrompt && hasNegativePrompt && <ParamNegativePrompt />}
|
||||
<RefImageList />
|
||||
{modelSupportsRefImages && <RefImageList />}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
selectIsChatGPT4o,
|
||||
selectIsFluxKontextApi,
|
||||
selectIsImagen3,
|
||||
selectIsImagen4,
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
|
||||
export const useIsApiModel = () => {
|
||||
const isImagen3 = useAppSelector(selectIsImagen3);
|
||||
const isImagen4 = useAppSelector(selectIsImagen4);
|
||||
const isFluxKontextApi = useAppSelector(selectIsFluxKontextApi);
|
||||
const isChatGPT4o = useAppSelector(selectIsChatGPT4o);
|
||||
|
||||
return isImagen3 || isImagen4 || isChatGPT4o || isFluxKontextApi;
|
||||
};
|
||||
@@ -17,6 +17,7 @@ export const MODEL_TYPE_MAP: Record<BaseModelType, string> = {
|
||||
imagen4: 'Imagen4',
|
||||
'chatgpt-4o': 'ChatGPT 4o',
|
||||
'flux-kontext': 'Flux Kontext',
|
||||
'gemini-2.5': 'Gemini 2.5',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -35,6 +36,7 @@ export const MODEL_TYPE_SHORT_MAP: Record<BaseModelType, string> = {
|
||||
imagen4: 'Imagen4',
|
||||
'chatgpt-4o': 'ChatGPT 4o',
|
||||
'flux-kontext': 'Flux Kontext',
|
||||
'gemini-2.5': 'Gemini 2.5',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -89,6 +91,10 @@ export const CLIP_SKIP_MAP: Record<BaseModelType, { maxClip: number; markers: nu
|
||||
maxClip: 0,
|
||||
markers: [],
|
||||
},
|
||||
'gemini-2.5': {
|
||||
maxClip: 0,
|
||||
markers: [],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -130,4 +136,48 @@ export const SCHEDULER_OPTIONS: ComboboxOption[] = [
|
||||
/**
|
||||
* List of base models that make API requests
|
||||
*/
|
||||
export const API_BASE_MODELS = ['imagen3', 'imagen4', 'chatgpt-4o', 'flux-kontext'];
|
||||
export const API_BASE_MODELS: BaseModelType[] = ['imagen3', 'imagen4', 'chatgpt-4o', 'flux-kontext', 'gemini-2.5'];
|
||||
|
||||
export const SUPPORTS_SEED_BASE_MODELS: BaseModelType[] = ['sd-1', 'sd-2', 'sd-3', 'sdxl', 'flux', 'cogview4'];
|
||||
|
||||
export const SUPPORTS_OPTIMIZED_DENOISING_BASE_MODELS: BaseModelType[] = ['flux', 'sd-3'];
|
||||
|
||||
export const SUPPORTS_REF_IMAGES_BASE_MODELS: BaseModelType[] = [
|
||||
'sd-1',
|
||||
'sdxl',
|
||||
'flux',
|
||||
'flux-kontext',
|
||||
'chatgpt-4o',
|
||||
'gemini-2.5',
|
||||
];
|
||||
|
||||
export const SUPPORTS_NEGATIVE_PROMPT_BASE_MODELS: BaseModelType[] = [
|
||||
'sd-1',
|
||||
'sdxl',
|
||||
'cogview4',
|
||||
'sd-3',
|
||||
'imagen3',
|
||||
'imagen4',
|
||||
];
|
||||
|
||||
export const SUPPORTS_PIXEL_DIMENSIONS_BASE_MODELS: BaseModelType[] = [
|
||||
'sd-1',
|
||||
'sd-2',
|
||||
'sd-3',
|
||||
'sdxl',
|
||||
'flux',
|
||||
'cogview4',
|
||||
];
|
||||
|
||||
export const SUPPORTS_ASPECT_RATIO_BASE_MODELS: BaseModelType[] = [
|
||||
'sd-1',
|
||||
'sd-2',
|
||||
'sd-3',
|
||||
'sdxl',
|
||||
'flux',
|
||||
'cogview4',
|
||||
'imagen3',
|
||||
'imagen4',
|
||||
'flux-kontext',
|
||||
'chatgpt-4o',
|
||||
];
|
||||
|
||||
@@ -21,6 +21,7 @@ export const getOptimalDimension = (base?: BaseModelType | null): number => {
|
||||
case 'imagen4':
|
||||
case 'chatgpt-4o':
|
||||
case 'flux-kontext':
|
||||
case 'gemini-2.5':
|
||||
default:
|
||||
return 1024;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { buildChatGPT4oGraph } from 'features/nodes/util/graph/generation/buildC
|
||||
import { buildCogView4Graph } from 'features/nodes/util/graph/generation/buildCogView4Graph';
|
||||
import { buildFLUXGraph } from 'features/nodes/util/graph/generation/buildFLUXGraph';
|
||||
import { buildFluxKontextGraph } from 'features/nodes/util/graph/generation/buildFluxKontextGraph';
|
||||
import { buildGemini2_5Graph } from 'features/nodes/util/graph/generation/buildGemini2_5Graph';
|
||||
import { buildImagen3Graph } from 'features/nodes/util/graph/generation/buildImagen3Graph';
|
||||
import { buildImagen4Graph } from 'features/nodes/util/graph/generation/buildImagen4Graph';
|
||||
import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph';
|
||||
@@ -66,6 +67,8 @@ const enqueueCanvas = async (store: AppStore, canvasManager: CanvasManager, prep
|
||||
return await buildChatGPT4oGraph(graphBuilderArg);
|
||||
case 'flux-kontext':
|
||||
return buildFluxKontextGraph(graphBuilderArg);
|
||||
case 'gemini-2.5':
|
||||
return buildGemini2_5Graph(graphBuilderArg);
|
||||
default:
|
||||
assert(false, `No graph builders for base ${base}`);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { buildChatGPT4oGraph } from 'features/nodes/util/graph/generation/buildC
|
||||
import { buildCogView4Graph } from 'features/nodes/util/graph/generation/buildCogView4Graph';
|
||||
import { buildFLUXGraph } from 'features/nodes/util/graph/generation/buildFLUXGraph';
|
||||
import { buildFluxKontextGraph } from 'features/nodes/util/graph/generation/buildFluxKontextGraph';
|
||||
import { buildGemini2_5Graph } from 'features/nodes/util/graph/generation/buildGemini2_5Graph';
|
||||
import { buildImagen3Graph } from 'features/nodes/util/graph/generation/buildImagen3Graph';
|
||||
import { buildImagen4Graph } from 'features/nodes/util/graph/generation/buildImagen4Graph';
|
||||
import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph';
|
||||
@@ -63,6 +64,8 @@ const enqueueGenerate = async (store: AppStore, prepend: boolean) => {
|
||||
return await buildChatGPT4oGraph(graphBuilderArg);
|
||||
case 'flux-kontext':
|
||||
return buildFluxKontextGraph(graphBuilderArg);
|
||||
case 'gemini-2.5':
|
||||
return buildGemini2_5Graph(graphBuilderArg);
|
||||
default:
|
||||
assert(false, `No graph builders for base ${base}`);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import { resolveBatchValue } from 'features/nodes/util/node/resolveBatchValue';
|
||||
import { useIsModelDisabled } from 'features/parameters/hooks/useIsModelDisabled';
|
||||
import type { UpscaleState } from 'features/parameters/store/upscaleSlice';
|
||||
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
|
||||
import { SUPPORTS_REF_IMAGES_BASE_MODELS } from 'features/parameters/types/constants';
|
||||
import type { ParameterModel } from 'features/parameters/types/parameterSchemas';
|
||||
import { getGridSize } from 'features/parameters/util/optimalDimension';
|
||||
import { promptExpansionApi, type PromptExpansionRequestState } from 'features/prompt/PromptExpansion/state';
|
||||
@@ -277,19 +278,21 @@ const getReasonsWhyCannotEnqueueGenerateTab = (arg: {
|
||||
reasons.push({ content: i18n.t('parameters.invoke.promptExpansionResultPending') });
|
||||
}
|
||||
|
||||
const enabledRefImages = refImages.entities.filter(({ isEnabled }) => isEnabled);
|
||||
if (model && SUPPORTS_REF_IMAGES_BASE_MODELS.includes(model.base)) {
|
||||
const enabledRefImages = refImages.entities.filter(({ isEnabled }) => isEnabled);
|
||||
|
||||
enabledRefImages.forEach((entity, i) => {
|
||||
const layerNumber = i + 1;
|
||||
const refImageLiteral = i18n.t(LAYER_TYPE_TO_TKEY['reference_image']);
|
||||
const prefix = `${refImageLiteral} #${layerNumber}`;
|
||||
const problems = getGlobalReferenceImageWarnings(entity, model);
|
||||
enabledRefImages.forEach((entity, i) => {
|
||||
const layerNumber = i + 1;
|
||||
const refImageLiteral = i18n.t(LAYER_TYPE_TO_TKEY['reference_image']);
|
||||
const prefix = `${refImageLiteral} #${layerNumber}`;
|
||||
const problems = getGlobalReferenceImageWarnings(entity, model);
|
||||
|
||||
if (problems.length) {
|
||||
const content = upperFirst(problems.map((p) => i18n.t(p)).join(', '));
|
||||
reasons.push({ prefix, content });
|
||||
}
|
||||
});
|
||||
if (problems.length) {
|
||||
const content = upperFirst(problems.map((p) => i18n.t(p)).join(', '));
|
||||
reasons.push({ prefix, content });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return reasons;
|
||||
};
|
||||
@@ -626,19 +629,21 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
|
||||
}
|
||||
});
|
||||
|
||||
const enabledRefImages = refImages.entities.filter(({ isEnabled }) => isEnabled);
|
||||
if (model && SUPPORTS_REF_IMAGES_BASE_MODELS.includes(model.base)) {
|
||||
const enabledRefImages = refImages.entities.filter(({ isEnabled }) => isEnabled);
|
||||
|
||||
enabledRefImages.forEach((entity, i) => {
|
||||
const layerNumber = i + 1;
|
||||
const refImageLiteral = i18n.t(LAYER_TYPE_TO_TKEY['reference_image']);
|
||||
const prefix = `${refImageLiteral} #${layerNumber}`;
|
||||
const problems = getGlobalReferenceImageWarnings(entity, model);
|
||||
enabledRefImages.forEach((entity, i) => {
|
||||
const layerNumber = i + 1;
|
||||
const refImageLiteral = i18n.t(LAYER_TYPE_TO_TKEY['reference_image']);
|
||||
const prefix = `${refImageLiteral} #${layerNumber}`;
|
||||
const problems = getGlobalReferenceImageWarnings(entity, model);
|
||||
|
||||
if (problems.length) {
|
||||
const content = upperFirst(problems.map((p) => i18n.t(p)).join(', '));
|
||||
reasons.push({ prefix, content });
|
||||
}
|
||||
});
|
||||
if (problems.length) {
|
||||
const content = upperFirst(problems.map((p) => i18n.t(p)).join(', '));
|
||||
reasons.push({ prefix, content });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
canvas.regionalGuidance.entities
|
||||
.filter((entity) => entity.isEnabled)
|
||||
|
||||
@@ -4,7 +4,12 @@ import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectLoRAsSlice } from 'features/controlLayers/store/lorasSlice';
|
||||
import { selectIsCogView4, selectIsFLUX, selectIsSD3 } from 'features/controlLayers/store/paramsSlice';
|
||||
import {
|
||||
selectIsApiBaseModel,
|
||||
selectIsCogView4,
|
||||
selectIsFLUX,
|
||||
selectIsSD3,
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
import { LoRAList } from 'features/lora/components/LoRAList';
|
||||
import LoRASelect from 'features/lora/components/LoRASelect';
|
||||
import ParamCFGScale from 'features/parameters/components/Core/ParamCFGScale';
|
||||
@@ -12,7 +17,6 @@ import ParamGuidance from 'features/parameters/components/Core/ParamGuidance';
|
||||
import ParamScheduler from 'features/parameters/components/Core/ParamScheduler';
|
||||
import ParamSteps from 'features/parameters/components/Core/ParamSteps';
|
||||
import { DisabledModelWarning } from 'features/parameters/components/MainModel/DisabledModelWarning';
|
||||
import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
|
||||
import { API_BASE_MODELS } from 'features/parameters/types/constants';
|
||||
import { MainModelPicker } from 'features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker';
|
||||
import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle';
|
||||
@@ -33,7 +37,7 @@ export const GenerationSettingsAccordion = memo(() => {
|
||||
const isSD3 = useAppSelector(selectIsSD3);
|
||||
const isCogView4 = useAppSelector(selectIsCogView4);
|
||||
|
||||
const isApiModel = useIsApiModel();
|
||||
const isApiModel = useAppSelector(selectIsApiBaseModel);
|
||||
|
||||
const selectBadges = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectLoRAsSlice } from 'features/controlLayers/store/lorasSlice';
|
||||
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectIsApiBaseModel, selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
|
||||
import { LoRAList } from 'features/lora/components/LoRAList';
|
||||
import LoRASelect from 'features/lora/components/LoRASelect';
|
||||
import ParamGuidance from 'features/parameters/components/Core/ParamGuidance';
|
||||
@@ -12,7 +12,6 @@ import ParamSteps from 'features/parameters/components/Core/ParamSteps';
|
||||
import { DisabledModelWarning } from 'features/parameters/components/MainModel/DisabledModelWarning';
|
||||
import ParamUpscaleCFGScale from 'features/parameters/components/Upscale/ParamUpscaleCFGScale';
|
||||
import ParamUpscaleScheduler from 'features/parameters/components/Upscale/ParamUpscaleScheduler';
|
||||
import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
|
||||
import { API_BASE_MODELS } from 'features/parameters/types/constants';
|
||||
import { MainModelPicker } from 'features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker';
|
||||
import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle';
|
||||
@@ -31,7 +30,7 @@ export const UpscaleTabGenerationSettingsAccordion = memo(() => {
|
||||
const modelConfig = useSelectedModelConfig();
|
||||
const isFLUX = useAppSelector(selectIsFLUX);
|
||||
|
||||
const isApiModel = useIsApiModel();
|
||||
const isApiModel = useAppSelector(selectIsApiBaseModel);
|
||||
|
||||
const selectBadges = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -3,44 +3,65 @@ import { Expander, Flex, FormControlGroup, StandaloneAccordion } from '@invoke-a
|
||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectIsFLUX, selectIsSD3, selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasSlice, selectScaleMethod } from 'features/controlLayers/store/selectors';
|
||||
import {
|
||||
selectModelSupportsAspectRatio,
|
||||
selectModelSupportsOptimizedDenoising,
|
||||
selectModelSupportsPixelDimensions,
|
||||
selectModelSupportsSeed,
|
||||
selectShouldRandomizeSeed,
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectBbox, selectScaleMethod } from 'features/controlLayers/store/selectors';
|
||||
import { ParamOptimizedDenoisingToggle } from 'features/parameters/components/Advanced/ParamOptimizedDenoisingToggle';
|
||||
import BboxScaledHeight from 'features/parameters/components/Bbox/BboxScaledHeight';
|
||||
import BboxScaledWidth from 'features/parameters/components/Bbox/BboxScaledWidth';
|
||||
import BboxScaleMethod from 'features/parameters/components/Bbox/BboxScaleMethod';
|
||||
import { BboxSettings } from 'features/parameters/components/Bbox/BboxSettings';
|
||||
import { ParamSeed } from 'features/parameters/components/Seed/ParamSeed';
|
||||
import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
|
||||
import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle';
|
||||
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectBadges = createMemoizedSelector([selectCanvasSlice, selectParamsSlice], (canvas, params) => {
|
||||
const { shouldRandomizeSeed } = params;
|
||||
const badges: string[] = [];
|
||||
const selectBadges = createMemoizedSelector(
|
||||
[
|
||||
selectBbox,
|
||||
selectShouldRandomizeSeed,
|
||||
selectModelSupportsSeed,
|
||||
selectModelSupportsAspectRatio,
|
||||
selectModelSupportsPixelDimensions,
|
||||
],
|
||||
(bbox, shouldRandomizeSeed, modelSupportsSeed, modelSupportsAspectRatio, modelSupportsPixelDimensions) => {
|
||||
const badges: string[] = [];
|
||||
|
||||
const { aspectRatio } = canvas.bbox;
|
||||
const { width, height } = canvas.bbox.rect;
|
||||
const { aspectRatio, rect } = bbox;
|
||||
const { width, height } = rect;
|
||||
|
||||
badges.push(`${width}×${height}`);
|
||||
badges.push(aspectRatio.id);
|
||||
if (modelSupportsPixelDimensions) {
|
||||
badges.push(`${width}×${height}`);
|
||||
}
|
||||
|
||||
if (aspectRatio.isLocked) {
|
||||
badges.push('locked');
|
||||
if (modelSupportsAspectRatio) {
|
||||
badges.push(aspectRatio.id);
|
||||
|
||||
// If a model does not support pixel dimensions, the ratio is essentially always locked.
|
||||
if (modelSupportsPixelDimensions && aspectRatio.isLocked) {
|
||||
badges.push('locked');
|
||||
}
|
||||
}
|
||||
|
||||
if (modelSupportsSeed) {
|
||||
if (!shouldRandomizeSeed) {
|
||||
badges.push('Manual Seed');
|
||||
}
|
||||
}
|
||||
|
||||
if (badges.length === 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
return badges;
|
||||
}
|
||||
|
||||
if (!shouldRandomizeSeed) {
|
||||
badges.push('Manual Seed');
|
||||
}
|
||||
|
||||
if (badges.length === 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
return badges;
|
||||
});
|
||||
);
|
||||
|
||||
const scalingLabelProps: FormLabelProps = {
|
||||
minW: '4.5rem',
|
||||
@@ -58,9 +79,16 @@ export const CanvasTabImageSettingsAccordion = memo(() => {
|
||||
id: 'image-settings-advanced',
|
||||
defaultIsOpen: false,
|
||||
});
|
||||
const isFLUX = useAppSelector(selectIsFLUX);
|
||||
const isSD3 = useAppSelector(selectIsSD3);
|
||||
const isApiModel = useIsApiModel();
|
||||
const modelSupportsOptimizedDenoising = useAppSelector(selectModelSupportsOptimizedDenoising);
|
||||
const modelSupportsSeed = useAppSelector(selectModelSupportsSeed);
|
||||
const modelSupportsAspectRatio = useAppSelector(selectModelSupportsAspectRatio);
|
||||
const modelSupportsPixelDimensions = useAppSelector(selectModelSupportsPixelDimensions);
|
||||
|
||||
if (!modelSupportsAspectRatio && !modelSupportsSeed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const withAdvancedSettingsExpander = modelSupportsPixelDimensions;
|
||||
|
||||
return (
|
||||
<StandaloneAccordion
|
||||
@@ -72,18 +100,18 @@ export const CanvasTabImageSettingsAccordion = memo(() => {
|
||||
<Flex
|
||||
px={4}
|
||||
pt={4}
|
||||
pb={isApiModel ? 4 : 0}
|
||||
pb={withAdvancedSettingsExpander ? 0 : 4}
|
||||
w="full"
|
||||
h="full"
|
||||
flexDir="column"
|
||||
data-testid="image-settings-accordion"
|
||||
>
|
||||
<BboxSettings />
|
||||
{!isApiModel && <ParamSeed py={3} />}
|
||||
{!isApiModel && (
|
||||
{modelSupportsSeed && <ParamSeed pt={3} pb={withAdvancedSettingsExpander ? 0 : 3} />}
|
||||
{withAdvancedSettingsExpander && (
|
||||
<Expander label={t('accordions.advanced.options')} isOpen={isOpenExpander} onToggle={onToggleExpander}>
|
||||
<Flex gap={4} pb={4} flexDir="column">
|
||||
{(isFLUX || isSD3) && <ParamOptimizedDenoisingToggle />}
|
||||
{modelSupportsOptimizedDenoising && <ParamOptimizedDenoisingToggle />}
|
||||
<BboxScaleMethod />
|
||||
{scaleMethod !== 'none' && (
|
||||
<FormControlGroup formLabelProps={scalingLabelProps}>
|
||||
|
||||
@@ -6,30 +6,58 @@ import {
|
||||
selectAspectRatioID,
|
||||
selectAspectRatioIsLocked,
|
||||
selectHeight,
|
||||
selectModelSupportsAspectRatio,
|
||||
selectModelSupportsPixelDimensions,
|
||||
selectModelSupportsSeed,
|
||||
selectShouldRandomizeSeed,
|
||||
selectWidth,
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
import { Dimensions } from 'features/parameters/components/Dimensions/Dimensions';
|
||||
import { ParamSeed } from 'features/parameters/components/Seed/ParamSeed';
|
||||
import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
|
||||
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectBadges = createMemoizedSelector(
|
||||
[selectWidth, selectHeight, selectAspectRatioID, selectAspectRatioIsLocked, selectShouldRandomizeSeed],
|
||||
(width, height, aspectRatioID, aspectRatioIsLocked, shouldRandomizeSeed) => {
|
||||
[
|
||||
selectWidth,
|
||||
selectHeight,
|
||||
selectAspectRatioID,
|
||||
selectAspectRatioIsLocked,
|
||||
selectShouldRandomizeSeed,
|
||||
selectModelSupportsSeed,
|
||||
selectModelSupportsAspectRatio,
|
||||
selectModelSupportsPixelDimensions,
|
||||
],
|
||||
(
|
||||
width,
|
||||
height,
|
||||
aspectRatioID,
|
||||
aspectRatioIsLocked,
|
||||
shouldRandomizeSeed,
|
||||
modelSupportsSeed,
|
||||
modelSupportsAspectRatio,
|
||||
modelSupportsPixelDimensions
|
||||
) => {
|
||||
const badges: string[] = [];
|
||||
|
||||
badges.push(`${width}×${height}`);
|
||||
badges.push(aspectRatioID);
|
||||
|
||||
if (aspectRatioIsLocked) {
|
||||
badges.push('locked');
|
||||
if (modelSupportsPixelDimensions) {
|
||||
badges.push(`${width}×${height}`);
|
||||
}
|
||||
|
||||
if (!shouldRandomizeSeed) {
|
||||
badges.push('Manual Seed');
|
||||
if (modelSupportsAspectRatio) {
|
||||
badges.push(aspectRatioID);
|
||||
|
||||
// If a model does not support pixel dimensions, the ratio is essentially always locked.
|
||||
if (modelSupportsPixelDimensions && aspectRatioIsLocked) {
|
||||
badges.push('locked');
|
||||
}
|
||||
}
|
||||
|
||||
if (modelSupportsSeed) {
|
||||
if (!shouldRandomizeSeed) {
|
||||
badges.push('Manual Seed');
|
||||
}
|
||||
}
|
||||
|
||||
if (badges.length === 0) {
|
||||
@@ -47,7 +75,12 @@ export const GenerateTabImageSettingsAccordion = memo(() => {
|
||||
id: 'image-settings-generate-tab',
|
||||
defaultIsOpen: true,
|
||||
});
|
||||
const isApiModel = useIsApiModel();
|
||||
const supportsSeed = useAppSelector(selectModelSupportsSeed);
|
||||
const supportsAspectRatio = useAppSelector(selectModelSupportsAspectRatio);
|
||||
|
||||
if (!supportsAspectRatio && !supportsSeed) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<StandaloneAccordion
|
||||
@@ -56,17 +89,9 @@ export const GenerateTabImageSettingsAccordion = memo(() => {
|
||||
isOpen={isOpenAccordion}
|
||||
onToggle={onToggleAccordion}
|
||||
>
|
||||
<Flex
|
||||
px={4}
|
||||
pt={4}
|
||||
pb={isApiModel ? 4 : 0}
|
||||
w="full"
|
||||
h="full"
|
||||
flexDir="column"
|
||||
data-testid="image-settings-accordion"
|
||||
>
|
||||
<Dimensions />
|
||||
{!isApiModel && <ParamSeed py={3} />}
|
||||
<Flex px={4} pt={4} pb={4} w="full" h="full" flexDir="column" data-testid="image-settings-accordion">
|
||||
{supportsAspectRatio && <Dimensions />}
|
||||
{supportsSeed && <ParamSeed py={3} />}
|
||||
</Flex>
|
||||
</StandaloneAccordion>
|
||||
);
|
||||
|
||||
@@ -2,9 +2,8 @@ import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||
import { selectIsCogView4, selectIsSDXL } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectIsApiBaseModel, selectIsCogView4, selectIsSDXL } from 'features/controlLayers/store/paramsSlice';
|
||||
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
||||
import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
|
||||
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
|
||||
import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion';
|
||||
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
|
||||
@@ -27,7 +26,7 @@ export const ParametersPanelCanvas = memo(() => {
|
||||
const isCogview4 = useAppSelector(selectIsCogView4);
|
||||
const isStylePresetsMenuOpen = useStore($isStylePresetsMenuOpen);
|
||||
|
||||
const isApiModel = useIsApiModel();
|
||||
const isApiModel = useAppSelector(selectIsApiBaseModel);
|
||||
|
||||
return (
|
||||
<Flex w="full" h="full" flexDir="column" gap={2}>
|
||||
|
||||
@@ -2,9 +2,8 @@ import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||
import { selectIsCogView4, selectIsSDXL } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectIsApiBaseModel, selectIsCogView4, selectIsSDXL } from 'features/controlLayers/store/paramsSlice';
|
||||
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
||||
import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
|
||||
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
|
||||
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
|
||||
import { GenerateTabImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/GenerateTabImageSettingsAccordion';
|
||||
@@ -26,7 +25,7 @@ export const ParametersPanelGenerate = memo(() => {
|
||||
const isCogview4 = useAppSelector(selectIsCogView4);
|
||||
const isStylePresetsMenuOpen = useStore($isStylePresetsMenuOpen);
|
||||
|
||||
const isApiModel = useIsApiModel();
|
||||
const isApiModel = useAppSelector(selectIsApiBaseModel);
|
||||
|
||||
return (
|
||||
<Flex w="full" h="full" flexDir="column" gap={2}>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
isFluxMainModelModelConfig,
|
||||
isFluxReduxModelConfig,
|
||||
isFluxVAEModelConfig,
|
||||
isGemini2_5ModelConfig,
|
||||
isImagen3ModelConfig,
|
||||
isImagen4ModelConfig,
|
||||
isIPAdapterModelConfig,
|
||||
@@ -92,7 +93,8 @@ export const useGlobalReferenceImageModels = buildModelsHook(
|
||||
isFluxReduxModelConfig(config) ||
|
||||
isChatGPT4oModelConfig(config) ||
|
||||
isFluxKontextApiModelConfig(config) ||
|
||||
isFluxKontextModelConfig(config)
|
||||
isFluxKontextModelConfig(config) ||
|
||||
isGemini2_5ModelConfig(config)
|
||||
);
|
||||
export const useRegionalReferenceImageModels = buildModelsHook(
|
||||
(config) => isIPAdapterModelConfig(config) || isFluxReduxModelConfig(config)
|
||||
@@ -135,7 +137,8 @@ export const selectGlobalRefImageModels = buildModelsSelector(
|
||||
isFluxReduxModelConfig(config) ||
|
||||
isChatGPT4oModelConfig(config) ||
|
||||
isFluxKontextApiModelConfig(config) ||
|
||||
isFluxKontextModelConfig(config)
|
||||
isFluxKontextModelConfig(config) ||
|
||||
isGemini2_5ModelConfig(config)
|
||||
);
|
||||
export const selectRegionalRefImageModels = buildModelsSelector(
|
||||
(config) => isIPAdapterModelConfig(config) || isFluxReduxModelConfig(config)
|
||||
|
||||
@@ -2239,7 +2239,7 @@ export type components = {
|
||||
* @description Base model type.
|
||||
* @enum {string}
|
||||
*/
|
||||
BaseModelType: "any" | "sd-1" | "sd-2" | "sd-3" | "sdxl" | "sdxl-refiner" | "flux" | "cogview4" | "imagen3" | "imagen4" | "chatgpt-4o" | "flux-kontext";
|
||||
BaseModelType: "any" | "sd-1" | "sd-2" | "sd-3" | "sdxl" | "sdxl-refiner" | "flux" | "cogview4" | "imagen3" | "imagen4" | "gemini-2.5" | "chatgpt-4o" | "flux-kontext";
|
||||
/** Batch */
|
||||
Batch: {
|
||||
/**
|
||||
@@ -21459,7 +21459,7 @@ export type components = {
|
||||
* used, and the type will be ignored. They are included here for backwards compatibility.
|
||||
* @enum {string}
|
||||
*/
|
||||
UIType: "MainModelField" | "CogView4MainModelField" | "FluxMainModelField" | "SD3MainModelField" | "SDXLMainModelField" | "SDXLRefinerModelField" | "ONNXModelField" | "VAEModelField" | "FluxVAEModelField" | "LoRAModelField" | "ControlNetModelField" | "IPAdapterModelField" | "T2IAdapterModelField" | "T5EncoderModelField" | "CLIPEmbedModelField" | "CLIPLEmbedModelField" | "CLIPGEmbedModelField" | "SpandrelImageToImageModelField" | "ControlLoRAModelField" | "SigLipModelField" | "FluxReduxModelField" | "LLaVAModelField" | "Imagen3ModelField" | "Imagen4ModelField" | "ChatGPT4oModelField" | "FluxKontextModelField" | "SchedulerField" | "AnyField" | "CollectionField" | "CollectionItemField" | "DEPRECATED_Boolean" | "DEPRECATED_Color" | "DEPRECATED_Conditioning" | "DEPRECATED_Control" | "DEPRECATED_Float" | "DEPRECATED_Image" | "DEPRECATED_Integer" | "DEPRECATED_Latents" | "DEPRECATED_String" | "DEPRECATED_BooleanCollection" | "DEPRECATED_ColorCollection" | "DEPRECATED_ConditioningCollection" | "DEPRECATED_ControlCollection" | "DEPRECATED_FloatCollection" | "DEPRECATED_ImageCollection" | "DEPRECATED_IntegerCollection" | "DEPRECATED_LatentsCollection" | "DEPRECATED_StringCollection" | "DEPRECATED_BooleanPolymorphic" | "DEPRECATED_ColorPolymorphic" | "DEPRECATED_ConditioningPolymorphic" | "DEPRECATED_ControlPolymorphic" | "DEPRECATED_FloatPolymorphic" | "DEPRECATED_ImagePolymorphic" | "DEPRECATED_IntegerPolymorphic" | "DEPRECATED_LatentsPolymorphic" | "DEPRECATED_StringPolymorphic" | "DEPRECATED_UNet" | "DEPRECATED_Vae" | "DEPRECATED_CLIP" | "DEPRECATED_Collection" | "DEPRECATED_CollectionItem" | "DEPRECATED_Enum" | "DEPRECATED_WorkflowField" | "DEPRECATED_IsIntermediate" | "DEPRECATED_BoardField" | "DEPRECATED_MetadataItem" | "DEPRECATED_MetadataItemCollection" | "DEPRECATED_MetadataItemPolymorphic" | "DEPRECATED_MetadataDict";
|
||||
UIType: "MainModelField" | "CogView4MainModelField" | "FluxMainModelField" | "SD3MainModelField" | "SDXLMainModelField" | "SDXLRefinerModelField" | "ONNXModelField" | "VAEModelField" | "FluxVAEModelField" | "LoRAModelField" | "ControlNetModelField" | "IPAdapterModelField" | "T2IAdapterModelField" | "T5EncoderModelField" | "CLIPEmbedModelField" | "CLIPLEmbedModelField" | "CLIPGEmbedModelField" | "SpandrelImageToImageModelField" | "ControlLoRAModelField" | "SigLipModelField" | "FluxReduxModelField" | "LLaVAModelField" | "Imagen3ModelField" | "Imagen4ModelField" | "ChatGPT4oModelField" | "Gemini2_5ModelField" | "FluxKontextModelField" | "SchedulerField" | "AnyField" | "CollectionField" | "CollectionItemField" | "DEPRECATED_Boolean" | "DEPRECATED_Color" | "DEPRECATED_Conditioning" | "DEPRECATED_Control" | "DEPRECATED_Float" | "DEPRECATED_Image" | "DEPRECATED_Integer" | "DEPRECATED_Latents" | "DEPRECATED_String" | "DEPRECATED_BooleanCollection" | "DEPRECATED_ColorCollection" | "DEPRECATED_ConditioningCollection" | "DEPRECATED_ControlCollection" | "DEPRECATED_FloatCollection" | "DEPRECATED_ImageCollection" | "DEPRECATED_IntegerCollection" | "DEPRECATED_LatentsCollection" | "DEPRECATED_StringCollection" | "DEPRECATED_BooleanPolymorphic" | "DEPRECATED_ColorPolymorphic" | "DEPRECATED_ConditioningPolymorphic" | "DEPRECATED_ControlPolymorphic" | "DEPRECATED_FloatPolymorphic" | "DEPRECATED_ImagePolymorphic" | "DEPRECATED_IntegerPolymorphic" | "DEPRECATED_LatentsPolymorphic" | "DEPRECATED_StringPolymorphic" | "DEPRECATED_UNet" | "DEPRECATED_Vae" | "DEPRECATED_CLIP" | "DEPRECATED_Collection" | "DEPRECATED_CollectionItem" | "DEPRECATED_Enum" | "DEPRECATED_WorkflowField" | "DEPRECATED_IsIntermediate" | "DEPRECATED_BoardField" | "DEPRECATED_MetadataItem" | "DEPRECATED_MetadataItemCollection" | "DEPRECATED_MetadataItemPolymorphic" | "DEPRECATED_MetadataDict";
|
||||
/** UNetField */
|
||||
UNetField: {
|
||||
/** @description Info to load unet submodel */
|
||||
|
||||
@@ -99,6 +99,7 @@ export type ApiModelConfig = S['ApiModelConfig'];
|
||||
export type MainModelConfig = DiffusersModelConfig | CheckpointModelConfig | ApiModelConfig;
|
||||
export type FLUXKontextModelConfig = MainModelConfig;
|
||||
export type ChatGPT4oModelConfig = ApiModelConfig;
|
||||
export type Gemini2_5ModelConfig = ApiModelConfig;
|
||||
export type AnyModelConfig =
|
||||
| ControlLoRAModelConfig
|
||||
| LoRAModelConfig
|
||||
@@ -280,6 +281,10 @@ export const isFluxKontextModelConfig = (config: AnyModelConfig): config is FLUX
|
||||
return config.type === 'main' && config.base === 'flux' && config.name.toLowerCase().includes('kontext');
|
||||
};
|
||||
|
||||
export const isGemini2_5ModelConfig = (config: AnyModelConfig): config is ApiModelConfig => {
|
||||
return config.type === 'main' && config.base === 'gemini-2.5';
|
||||
};
|
||||
|
||||
export const isNonRefinerMainModelConfig = (config: AnyModelConfig): config is MainModelConfig => {
|
||||
return config.type === 'main' && config.base !== 'sdxl-refiner';
|
||||
};
|
||||
|
||||
@@ -173,7 +173,7 @@ const HFUnauthorizedToastDescription = () => {
|
||||
if (data === 'unknown') {
|
||||
return (
|
||||
<Text fontSize="md">
|
||||
{t('modelManager.hfTokenUnableToErrorMessage')}{' '}
|
||||
{t('modelManager.hfTokenUnableToVerifyErrorMessage')}{' '}
|
||||
<Button onClick={onClick} variant="link" color="base.50" flexGrow={0}>
|
||||
{t('modelManager.modelManager')}.
|
||||
</Button>
|
||||
@@ -181,6 +181,13 @@ const HFUnauthorizedToastDescription = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// data === 'valid' - should never happen!
|
||||
assert(false, 'Unexpected valid HF token with unauthorized error');
|
||||
// data === 'valid' - user may have a token but not authorized for model?
|
||||
return (
|
||||
<Text fontSize="md">
|
||||
{t('modelManager.hfTokenForbiddenErrorMessage')}{' '}
|
||||
<Button onClick={onClick} variant="link" color="base.50" flexGrow={0}>
|
||||
{t('modelManager.modelManager')}.
|
||||
</Button>
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "6.4.0rc2"
|
||||
__version__ = "6.5.1"
|
||||
|
||||
@@ -38,14 +38,13 @@ dependencies = [
|
||||
"compel==2.1.1",
|
||||
"diffusers[torch]==0.33.0",
|
||||
"gguf",
|
||||
"invisible-watermark==0.2.0", # needed to install SDXL base and refiner using their repo_ids
|
||||
"mediapipe==0.10.14", # needed for "mediapipeface" controlnet model
|
||||
"numpy<2.0.0",
|
||||
"onnx==1.16.1",
|
||||
"onnxruntime==1.19.2",
|
||||
"opencv-contrib-python",
|
||||
"safetensors",
|
||||
"sentencepiece",
|
||||
"sentencepiece==0.2.0", # 0.2.1 coredumps windows when loading t5 tokenizer
|
||||
"spandrel",
|
||||
"torch~=2.7.0", # torch and related dependencies are loosely pinned, will respect requirement of `diffusers[torch]`
|
||||
"torchsde", # diffusers needs this for SDE solvers, but it is not an explicit dep of diffusers
|
||||
@@ -74,6 +73,7 @@ dependencies = [
|
||||
"python-multipart",
|
||||
"requests",
|
||||
"semver~=3.0.1",
|
||||
"PyWavelets",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
@@ -234,6 +234,7 @@ exclude = [
|
||||
"invokeai/backend/image_util/mlsd/", # External code
|
||||
"invokeai/backend/image_util/normal_bae/", # External code
|
||||
"invokeai/backend/image_util/pidi/", # External code
|
||||
"invokeai/backend/image_util/imwatermark/", # External code
|
||||
]
|
||||
|
||||
[tool.ruff.lint]
|
||||
|
||||
Reference in New Issue
Block a user