Compare commits

...

111 Commits

Author SHA1 Message Date
Ryan Dick
e22f0f2203 Update DepthAnything post-processing logic to avoid artifacts caused by numerical overflow. 2024-11-27 14:54:30 +00:00
Ryan Dick
8cfb032051 Add utility ImagePanelLayoutInvocation for working with In-Context LoRA workflows. 2024-11-26 20:58:31 -08:00
Ryan Dick
06a9d4e2b2 Use a Textarea component for the FluxTextEncoderInvocation prompt field. 2024-11-26 20:58:31 -08:00
Brandon Rising
ed46acee79 fix: Fail scan on InvalidMagicError in picklescan, update default for read_checkpoint_meta to scan unless explicitly told not to 2024-11-26 16:17:12 -05:00
Mary Hipp
965cd76e33 lint fix 2024-11-26 11:25:53 -05:00
Mary Hipp
e5e8cbf34c shorten reference image mode descriptions; 2024-11-26 11:25:53 -05:00
Mary Hipp
3412a52594 (ui): updates various informational tooltips, adds descriptons to IP adapter method options 2024-11-26 11:25:53 -05:00
Kent Keirsey
059336258f Create SECURITY.md 2024-11-25 04:10:03 -08:00
Mary Hipp
271cc52c80 fix(ui): use token for download if its in store 2024-11-22 12:08:05 -05:00
psychedelicious
e44458609f chore: bump version to v5.4.3rc1 2024-11-21 10:32:43 -08:00
psychedelicious
69d86a7696 feat(ui): address feedback 2024-11-21 09:54:35 -08:00
Hippalectryon
56db1a9292 Use proxyrect and setEntityPosition to sync transformer position 2024-11-21 09:54:35 -08:00
Hippalectryon
cf50e5eeee Make sure the canvas is focused 2024-11-21 09:54:35 -08:00
Hippalectryon
c9c07968d2 lint 2024-11-21 09:54:35 -08:00
Hippalectryon
97d0757176 use $isInteractable instead of $isDisabled 2024-11-21 09:54:35 -08:00
Hippalectryon
0f51b677a9 refactor 2024-11-21 09:54:35 -08:00
Hippalectryon
56ca94c3a9 Don't move if the layer is disabled
Lint
2024-11-21 09:54:35 -08:00
Hippalectryon
28d169f859 Allow moving layers using the keyboard 2024-11-21 09:54:35 -08:00
psychedelicious
92f71d99ee tweak(ui): use X icon for rg ref image delete button 2024-11-21 08:50:39 -08:00
psychedelicious
0764c02b1d tweak(ui): code style 2024-11-21 08:50:39 -08:00
psychedelicious
081c7569fe feat(ui): add global ref image empty state 2024-11-21 08:50:39 -08:00
psychedelicious
20f6532ee8 feat(ui): add empty state for regional guidance ref image 2024-11-21 08:50:39 -08:00
Mary Hipp
b9e8910478 feat(ui): add actions for video modal clicks 2024-11-21 11:15:55 -05:00
Mary Hipp
ded8391e3c use nanostore for schema parsed instead 2024-11-20 20:13:31 -05:00
Mary Hipp
e9dd2c396a limit to one hook 2024-11-20 20:13:31 -05:00
Mary Hipp
0d86de0cb5 fix(ui): make sure schema has loaded before trying to load any workflows 2024-11-20 20:13:31 -05:00
psychedelicious
549f4e9794 feat(ui): set default infill method to lama 2024-11-20 11:19:17 -05:00
psychedelicious
ef8ededd2f fix(ui): disable width and height output on image batch output
There's a technical challenge with outputting these values directly. `ImageField` does not store them, so the batch's `ImageField` collection does not have width and height for each image.

In order to set up the batch and pass along width and height for each image, we'd need to make a network request for each image when the user clicks Invoke. It would often be cached, but this will eventually create a scaling issue and poor user experience.

As a very simple workaround, users can output the batch image output into an `Image Primitive` node to access the width and height.

This change is implemented by adding some simple special handling when parsing the output fields for the `image_batch` node.

I'll keep this situation in mind when extending the batching system to other field types.
2024-11-20 11:16:54 -05:00
Mary Hipp
1948ffe106 make sure Soft Edge Detection has preprocessor applied 2024-11-20 08:46:02 -05:00
psychedelicious
c70f4404c4 fix(ui): special node icon tooltip 2024-11-19 14:29:09 -08:00
psychedelicious
b157ae928c chore(ui): update what's new copy 2024-11-19 14:29:09 -08:00
psychedelicious
7a0871992d chore: bump version to v5.4.2 2024-11-19 14:29:09 -08:00
Hosted Weblate
b38e2e14f4 translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2024-11-19 14:12:00 -08:00
psychedelicious
7c0e70ec84 tweak(ui): "Watch on YouTube" -> "Watch" 2024-11-19 14:02:11 -08:00
psychedelicious
a89ae9d2bf feat(ui): add links to studio sessions/discord 2024-11-19 14:02:11 -08:00
psychedelicious
ad1fcb3f07 chore(ui): bump @invoke-ai/ui-library
Brings in a fix for `ExternalLink`
2024-11-19 14:02:11 -08:00
psychedelicious
87d74b910b feat(ui): support videos modal 2024-11-19 14:02:11 -08:00
psychedelicious
7ad1c297a4 feat(ui): add actions for reset canvas layers / generation settings to session menus 2024-11-19 13:55:16 -08:00
psychedelicious
fbc629faa6 feat(ui): change reset canvas button to new session menu 2024-11-19 13:55:16 -08:00
psychedelicious
7baa6b3c09 feat(ui): split up new from image into submenus
- `New Canvas from Image` -> `As Raster Layer`, `As Raster Layer (Resize)`, `As Control Layer`, `As Control Layer (Resize)`
- `New Layer from Image` -> (each layer type)
2024-11-19 10:34:00 -08:00
psychedelicious
53d482bade feat(ui): add image ctx menu new canvas without resize option 2024-11-19 10:34:00 -08:00
psychedelicious
5aca04b51b feat(ui): change reset canvas icon to "empty" 2024-11-19 09:56:25 -08:00
psychedelicious
ea8787c8ff feat(ui): update invoke button tooltip for batching
- Split up logic to determine reason why the user cannot invoke for each tab.
- Fix issue where the workflows tab would show reasons related to canvas/upscale tab. The tooltip now only shows information relevant to the current tab.
- Add calculation for batch size to the queue count prediction.
- Use a constant for the enqueue mutation's fixed cache key, instead of a string. Just some typo protection.
2024-11-19 09:53:59 -08:00
psychedelicious
cead2c4445 feat(ui): split up selector utils for useIsReadyToEnqueue 2024-11-19 09:53:59 -08:00
Mary Hipp
f76ac1808c fix(ui): simplify logic for non-local invocation progress alerts 2024-11-19 12:40:40 -05:00
psychedelicious
f01210861b chore: ruff 2024-11-19 07:02:37 -08:00
psychedelicious
f757f23ef0 chore(ui): typegen 2024-11-19 07:02:37 -08:00
psychedelicious
872a6ef209 tidy(nodes): extract slerp from lblend to util fn 2024-11-19 07:02:37 -08:00
psychedelicious
4267e5ffc4 tidy(nodes): bring masked blend latents masking logic into invoke core 2024-11-19 07:02:37 -08:00
Brandon Rising
a69c5ff9ef Add copyright notice for CIELab_to_UPLab.icc 2024-11-19 07:02:37 -08:00
Brandon Rising
3ebd8d7d1b Fix .icc asset file in pyproject.toml 2024-11-19 07:02:37 -08:00
Brandon Rising
1fd80d54a4 Run Ruff 2024-11-19 07:02:37 -08:00
Brandon Rising
991f63e455 Store CIELab_to_UPLab.icc within the repo 2024-11-19 07:02:37 -08:00
Brandon Rising
6a1efd3527 Add validation to some of the node inputs 2024-11-19 07:02:37 -08:00
Brandon Rising
0eadc0dd9e feat: Support a subset of composition nodes within base invokeai 2024-11-19 07:02:37 -08:00
youjayjeel
481423d678 translationBot(ui): update translation (Chinese (Simplified Han script))
Currently translated at 86.0% (1367 of 1588 strings)

Co-authored-by: youjayjeel <youjayjeel@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/zh_Hans/
Translation: InvokeAI/Web UI
2024-11-18 19:29:29 -08:00
Riccardo Giovanetti
89ede0aef3 translationBot(ui): update translation (Italian)
Currently translated at 99.3% (1578 of 1588 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2024-11-18 19:29:29 -08:00
gallegonovato
359bdee9c6 translationBot(ui): update translation (Spanish)
Currently translated at 42.3% (672 of 1588 strings)

translationBot(ui): update translation (Spanish)

Currently translated at 28.0% (445 of 1588 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/es/
Translation: InvokeAI/Web UI
2024-11-18 19:29:29 -08:00
psychedelicious
0e6fba3763 chore: bump version to v5.4.2rc1 2024-11-18 19:25:39 -08:00
psychedelicious
652502d7a6 fix(ui): add sd-3 grid size of 16px to grid util 2024-11-18 19:15:15 -08:00
psychedelicious
91d981a49e fix(ui): reactflow drag interactions with custom scrollbar 2024-11-18 19:12:27 -08:00
psychedelicious
24f61d21b2 feat(ui): make image field collection scrollable 2024-11-18 19:12:27 -08:00
psychedelicious
eb9a4177c5 feat(ui): allow removing individual images from batch 2024-11-18 19:12:27 -08:00
psychedelicious
3c43351a5b feat(ui): add reset to default value button to field title 2024-11-18 19:12:27 -08:00
psychedelicious
b1359b6dff feat(ui): update field validation logic to handle collection sizes 2024-11-18 19:12:27 -08:00
psychedelicious
bddccf6d2f feat(ui): add graph validation for image collection size 2024-11-18 19:12:27 -08:00
psychedelicious
21ffaab2a2 fix(ui): do not allow invoking when canvas is selectig object 2024-11-18 19:12:27 -08:00
psychedelicious
1e969f938f feat(ui): autosize image collection field grid 2024-11-18 19:12:27 -08:00
psychedelicious
9c6c86ee4f fix(ui): image field collection dnd adds instead of replaces 2024-11-18 19:12:27 -08:00
psychedelicious
6b53a48b48 fix(ui): zod schema refiners must return boolean 2024-11-18 19:12:27 -08:00
psychedelicious
c813fa3fc0 feat(ui): support min and max length for image collections 2024-11-18 19:12:27 -08:00
psychedelicious
a08e61184a chore(ui): typegen 2024-11-18 19:12:27 -08:00
psychedelicious
a0d62a5f41 feat(nodes): add minimum image count to ImageBatchInvocation 2024-11-18 19:12:27 -08:00
psychedelicious
616c0f11e1 feat(ui): image batching in workflows
- Add special handling for `ImageBatchInvocation`
- Add input component for image collections, supporting multi-image upload and dnd
- Minor rework of some hooks for accessing node data
2024-11-18 19:12:27 -08:00
psychedelicious
e1626a4e49 chore(ui): typegen 2024-11-18 19:12:27 -08:00
psychedelicious
6ab891a319 feat(nodes): add ImageBatchInvocation 2024-11-18 19:12:27 -08:00
psychedelicious
492de41316 feat(app): add Classification.Special, used for batch nodes 2024-11-18 19:12:27 -08:00
psychedelicious
c064efc866 feat(app): add ImageField as an allowed batching data type 2024-11-18 19:12:27 -08:00
Ryan Dick
1a0885bfb1 Update FLUX IP-Adapter starter model from XLabs v1 to XLabs v2. 2024-11-18 17:06:53 -08:00
Ryan Dick
e8b202d0a5 Update FLUX IP-Adapter graph construction to optimize for XLabs IP-Adapter v2 over v1. This results in degraded performance with v1 IP-Adapters. 2024-11-18 17:06:53 -08:00
Ryan Dick
c6fc82f756 Infer the clip_extra_context_tokens param from the state dict for FLUX XLabs IP-Adapter V2 models. 2024-11-18 17:06:53 -08:00
Ryan Dick
9a77e951d2 Add unit test for FLUX XLabs IP-Adapter V2 model format. 2024-11-18 17:06:53 -08:00
psychedelicious
8bd4207a27 docs(ui): add docstring to CanvasEntityStateGate 2024-11-18 13:40:08 -08:00
psychedelicious
0bb601aaf7 fix(ui): prevent entity not found errors
The canvas react components pass canvas entity identifiers around, then redux selectors are used to access that entity. This is good for perf - entity states may rapidly change. Passing only the identifiers allows components and other logic to have more granular state updates.

Unfortunately, this design opens the possibility for for an entity identifier to point to an entity that does not exist.

To get around this, I had created a redux selector `selectEntityOrThrow` for canvas entities. As the name implies, it throws if the entity is not found.

While it prevents components/hooks from needing to deal with missing entities, it results in mysterious errors if an entity is missing. Without sourcemaps, it's very difficult to determine what component or hook couldn't find the entity.

Refactoring the app to not depend on this behaviour is tricky. We could pass the entity state around directly as a prop or via context, but as mentioned, this could cause performance issues with rapidly changing entities.

As a workaround, I've made two changes:
- `<CanvasEntityStateGate/>` is a component that takes an entity identifier, returning its children if the entity state exists, or null if not. This component is wraps every usage of `selectEntityOrThrow`.  Theoretically, this should prevent the entity not found errors.
- Add a `caller: string` arg to `selectEntityOrThrow`. This string is now added to the error message when the assertion fails, so we can more easily track the source of the errors.

In the future we can work out a way to not use this throwing selector and retain perf. The app has changed quite a bit since that selector was created - so we may not have to worry about perf at all.
2024-11-18 13:40:08 -08:00
psychedelicious
2da25a0043 fix(ui): progress bar not throbbing when it should (#7332)
When we added more progress events during generation, we indirectly broke the logic that controls when the progress bar throbs.

Co-authored-by: Mary Hipp Rogers <maryhipp@gmail.com>
2024-11-18 14:02:20 +00:00
Mary Hipp
51d0931898 remove GPL-3 licensed package easing-functions 2024-11-18 08:55:17 -05:00
Riccardo Giovanetti
357b68d1ba translationBot(ui): update translation (Italian)
Currently translated at 99.3% (1577 of 1587 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2024-11-16 05:49:57 +11:00
Mary Hipp
d9ddb6c32e fix(ui): add padding to the metadata recall section so buttons are not blocked 2024-11-16 05:47:45 +11:00
Mary Hipp
ad02a99a83 fix(ui): ignore user setting for commercial, remove unused state 2024-11-16 05:21:30 +11:00
Mary Hipp
b707dafc7b translation 2024-11-16 05:21:30 +11:00
Mary Hipp
02906c8f5d feat(ui): deferred invocation progress details for model loading 2024-11-16 05:21:30 +11:00
psychedelicious
8538e508f1 chore(ui): lint 2024-11-15 12:59:30 +11:00
Hosted Weblate
8c333ffd14 translationBot(ui): update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2024-11-15 12:59:30 +11:00
Riccardo Giovanetti
72ace5fdff translationBot(ui): update translation (Italian)
Currently translated at 99.4% (1575 of 1583 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2024-11-15 12:59:30 +11:00
Gohsuke Shimada
9b7583fc84 translationBot(ui): update translation (Japanese)
Currently translated at 33.6% (533 of 1583 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 30.3% (481 of 1583 strings)

Co-authored-by: Gohsuke Shimada <ghoskay@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ja/
Translation: InvokeAI/Web UI
2024-11-15 12:59:30 +11:00
dakota2472
989eee338e translationBot(ui): update translation (Italian)
Currently translated at 99.8% (1580 of 1583 strings)

Co-authored-by: dakota2472 <gardaweb.net@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2024-11-15 12:59:30 +11:00
Linos
acc3d7b91b translationBot(ui): update translation (Vietnamese)
Currently translated at 100.0% (1583 of 1583 strings)

Co-authored-by: Linos <tt250208@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/vi/
Translation: InvokeAI/Web UI
2024-11-15 12:59:30 +11:00
Riccardo Giovanetti
49de868658 translationBot(ui): update translation (Italian)
Currently translated at 99.4% (1575 of 1583 strings)

translationBot(ui): update translation (Italian)

Currently translated at 99.4% (1573 of 1581 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2024-11-15 12:59:30 +11:00
Hosted Weblate
b1702c7d90 translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2024-11-15 12:59:30 +11:00
Riccardo Giovanetti
e49e19ea13 translationBot(ui): update translation (Italian)
Currently translated at 99.4% (1569 of 1577 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2024-11-15 12:59:30 +11:00
gallegonovato
c9f91f391e translationBot(ui): update translation (Spanish)
Currently translated at 17.6% (278 of 1575 strings)

translationBot(ui): update translation (Spanish)

Currently translated at 17.3% (274 of 1575 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/es/
Translation: InvokeAI/Web UI
2024-11-15 12:59:30 +11:00
Hosted Weblate
4cb6b2b701 translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2024-11-15 12:59:30 +11:00
Riccardo Giovanetti
7d132ea148 translationBot(ui): update translation (Italian)
Currently translated at 99.1% (1564 of 1577 strings)

translationBot(ui): update translation (Italian)

Currently translated at 99.4% (1566 of 1575 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2024-11-15 12:59:30 +11:00
Riku
1088accd91 translationBot(ui): update translation (German)
Currently translated at 71.8% (1131 of 1575 strings)

Co-authored-by: Riku <riku.block@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/
Translation: InvokeAI/Web UI
2024-11-15 12:59:30 +11:00
dakota2472
8d237d8f8b translationBot(ui): update translation (Italian)
Currently translated at 99.6% (1569 of 1575 strings)

Co-authored-by: dakota2472 <gardaweb.net@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2024-11-15 12:59:30 +11:00
Linos
0c86a3232d translationBot(ui): update translation (Vietnamese)
Currently translated at 100.0% (1581 of 1581 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 100.0% (1576 of 1576 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 100.0% (1575 of 1575 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 85.0% (1340 of 1575 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 78.7% (1240 of 1575 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 73.1% (1152 of 1575 strings)

translationBot(ui): update translation (English)

Currently translated at 99.9% (1574 of 1575 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 57.9% (913 of 1575 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 37.0% (584 of 1575 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 3.2% (51 of 1575 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 3.2% (51 of 1575 strings)

Co-authored-by: Linos <tt250208@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/en/
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/vi/
Translation: InvokeAI/Web UI
2024-11-15 12:59:30 +11:00
aidawanglion
dbfb0359cb translationBot(ui): update translation (Chinese (Simplified Han script))
Currently translated at 79.9% (1266 of 1583 strings)

translationBot(ui): update translation (Chinese (Simplified Han script))

Currently translated at 74.4% (1171 of 1573 strings)

Co-authored-by: aidawanglion <youjayjeel@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/zh_Hans/
Translation: InvokeAI/Web UI
2024-11-15 12:59:30 +11:00
Riccardo Giovanetti
b4c2aa596b translationBot(ui): update translation (Italian)
Currently translated at 99.6% (1569 of 1575 strings)

translationBot(ui): update translation (Italian)

Currently translated at 99.4% (1567 of 1575 strings)

translationBot(ui): update translation (Italian)

Currently translated at 99.4% (1565 of 1573 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2024-11-15 12:59:30 +11:00
psychedelicious
87e89b7995 fix(ui): remove progress message condition for canvas destinations 2024-11-15 12:55:46 +11:00
psychedelicious
9b089430e2 chore: bump version to v5.4.1 2024-11-15 11:51:06 +11:00
psychedelicious
f2b0025958 chore(ui): update what's new 2024-11-15 11:51:06 +11:00
138 changed files with 9812 additions and 1544 deletions

14
SECURITY.md Normal file
View File

@@ -0,0 +1,14 @@
# Security Policy
## Supported Versions
Only the latest version of Invoke will receive security updates.
We do not currently maintain multiple versions of the application with updates.
## Reporting a Vulnerability
To report a vulnerability, contact the Invoke team directly at security@invoke.ai
At this time, we do not maintain a formal bug bounty program.
You can also share identified security issues with our team on huntr.com

View File

@@ -99,7 +99,6 @@ their descriptions.
| Scale Latents | Scales latents by a given factor. |
| Segment Anything Processor | Applies segment anything processing to image |
| Show Image | Displays a provided image, and passes it forward in the pipeline. |
| Step Param Easing | Experimental per-step parameter easing for denoising steps |
| String Primitive Collection | A collection of string primitive values |
| String Primitive | A string primitive value |
| Subtract Integers | Subtracts two numbers |

View File

@@ -63,6 +63,7 @@ class Classification(str, Enum, metaclass=MetaEnum):
- `Prototype`: The invocation is not yet stable and may be removed from the application at any time. Workflows built around this invocation may break, and we are *not* committed to supporting this invocation.
- `Deprecated`: The invocation is deprecated and may be removed in a future version.
- `Internal`: The invocation is not intended for use by end-users. It may be changed or removed at any time, but is exposed for users to play with.
- `Special`: The invocation is a special case and does not fit into any of the other classifications.
"""
Stable = "stable"
@@ -70,6 +71,7 @@ class Classification(str, Enum, metaclass=MetaEnum):
Prototype = "prototype"
Deprecated = "deprecated"
Internal = "internal"
Special = "special"
class UIConfigBase(BaseModel):

View File

@@ -1,98 +1,120 @@
from typing import Any, Union
from typing import Optional, Union
import numpy as np
import numpy.typing as npt
import torch
import torchvision.transforms as T
from PIL import Image
from torchvision.transforms.functional import resize as tv_resize
from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, LatentsField
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField, LatentsField
from invokeai.app.invocations.primitives import LatentsOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
from invokeai.backend.util.devices import TorchDevice
def slerp(
t: Union[float, np.ndarray],
v0: Union[torch.Tensor, np.ndarray],
v1: Union[torch.Tensor, np.ndarray],
device: torch.device,
DOT_THRESHOLD: float = 0.9995,
):
"""
Spherical linear interpolation
Args:
t (float/np.ndarray): Float value between 0.0 and 1.0
v0 (np.ndarray): Starting vector
v1 (np.ndarray): Final vector
DOT_THRESHOLD (float): Threshold for considering the two vectors as
colineal. Not recommended to alter this.
Returns:
v2 (np.ndarray): Interpolation vector between v0 and v1
"""
inputs_are_torch = False
if not isinstance(v0, np.ndarray):
inputs_are_torch = True
v0 = v0.detach().cpu().numpy()
if not isinstance(v1, np.ndarray):
inputs_are_torch = True
v1 = v1.detach().cpu().numpy()
dot = np.sum(v0 * v1 / (np.linalg.norm(v0) * np.linalg.norm(v1)))
if np.abs(dot) > DOT_THRESHOLD:
v2 = (1 - t) * v0 + t * v1
else:
theta_0 = np.arccos(dot)
sin_theta_0 = np.sin(theta_0)
theta_t = theta_0 * t
sin_theta_t = np.sin(theta_t)
s0 = np.sin(theta_0 - theta_t) / sin_theta_0
s1 = sin_theta_t / sin_theta_0
v2 = s0 * v0 + s1 * v1
if inputs_are_torch:
v2 = torch.from_numpy(v2).to(device)
return v2
@invocation(
"lblend",
title="Blend Latents",
tags=["latents", "blend"],
tags=["latents", "blend", "mask"],
category="latents",
version="1.0.3",
version="1.1.0",
)
class BlendLatentsInvocation(BaseInvocation):
"""Blend two latents using a given alpha. Latents must have same size."""
"""Blend two latents using a given alpha. If a mask is provided, the second latents will be masked before blending.
Latents must have same size. Masking functionality added by @dwringer."""
latents_a: LatentsField = InputField(
description=FieldDescriptions.latents,
input=Input.Connection,
)
latents_b: LatentsField = InputField(
description=FieldDescriptions.latents,
input=Input.Connection,
)
alpha: float = InputField(default=0.5, description=FieldDescriptions.blend_alpha)
latents_a: LatentsField = InputField(description=FieldDescriptions.latents, input=Input.Connection)
latents_b: LatentsField = InputField(description=FieldDescriptions.latents, input=Input.Connection)
mask: Optional[ImageField] = InputField(default=None, description="Mask for blending in latents B")
alpha: float = InputField(ge=0, default=0.5, description=FieldDescriptions.blend_alpha)
def prep_mask_tensor(self, mask_image: Image.Image) -> torch.Tensor:
if mask_image.mode != "L":
mask_image = mask_image.convert("L")
mask_tensor = image_resized_to_grid_as_tensor(mask_image, normalize=False)
if mask_tensor.dim() == 3:
mask_tensor = mask_tensor.unsqueeze(0)
return mask_tensor
def replace_tensor_from_masked_tensor(
self, tensor: torch.Tensor, other_tensor: torch.Tensor, mask_tensor: torch.Tensor
):
output = tensor.clone()
mask_tensor = mask_tensor.expand(output.shape)
if output.dtype != torch.float16:
output = torch.add(output, mask_tensor * torch.sub(other_tensor, tensor))
else:
output = torch.add(output, mask_tensor.half() * torch.sub(other_tensor, tensor))
return output
def invoke(self, context: InvocationContext) -> LatentsOutput:
latents_a = context.tensors.load(self.latents_a.latents_name)
latents_b = context.tensors.load(self.latents_b.latents_name)
if self.mask is None:
mask_tensor = torch.zeros(latents_a.shape[-2:])
else:
mask_tensor = self.prep_mask_tensor(context.images.get_pil(self.mask.image_name))
mask_tensor = tv_resize(mask_tensor, latents_a.shape[-2:], T.InterpolationMode.BILINEAR, antialias=False)
latents_b = self.replace_tensor_from_masked_tensor(latents_b, latents_a, mask_tensor)
if latents_a.shape != latents_b.shape:
raise Exception("Latents to blend must be the same size.")
raise ValueError("Latents to blend must be the same size.")
device = TorchDevice.choose_torch_device()
def slerp(
t: Union[float, npt.NDArray[Any]], # FIXME: maybe use np.float32 here?
v0: Union[torch.Tensor, npt.NDArray[Any]],
v1: Union[torch.Tensor, npt.NDArray[Any]],
DOT_THRESHOLD: float = 0.9995,
) -> Union[torch.Tensor, npt.NDArray[Any]]:
"""
Spherical linear interpolation
Args:
t (float/np.ndarray): Float value between 0.0 and 1.0
v0 (np.ndarray): Starting vector
v1 (np.ndarray): Final vector
DOT_THRESHOLD (float): Threshold for considering the two vectors as
colineal. Not recommended to alter this.
Returns:
v2 (np.ndarray): Interpolation vector between v0 and v1
"""
inputs_are_torch = False
if not isinstance(v0, np.ndarray):
inputs_are_torch = True
v0 = v0.detach().cpu().numpy()
if not isinstance(v1, np.ndarray):
inputs_are_torch = True
v1 = v1.detach().cpu().numpy()
dot = np.sum(v0 * v1 / (np.linalg.norm(v0) * np.linalg.norm(v1)))
if np.abs(dot) > DOT_THRESHOLD:
v2 = (1 - t) * v0 + t * v1
else:
theta_0 = np.arccos(dot)
sin_theta_0 = np.sin(theta_0)
theta_t = theta_0 * t
sin_theta_t = np.sin(theta_t)
s0 = np.sin(theta_0 - theta_t) / sin_theta_0
s1 = sin_theta_t / sin_theta_0
v2 = s0 * v0 + s1 * v1
if inputs_are_torch:
v2_torch: torch.Tensor = torch.from_numpy(v2).to(device)
return v2_torch
else:
assert isinstance(v2, np.ndarray)
return v2
# blend
bl = slerp(self.alpha, latents_a, latents_b)
assert isinstance(bl, torch.Tensor)
blended_latents: torch.Tensor = bl # for type checking convenience
blended_latents = slerp(self.alpha, latents_a, latents_b, device)
# https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699
blended_latents = blended_latents.to("cpu")
TorchDevice.empty_cache()
torch.cuda.empty_cache()
name = context.tensors.save(tensor=blended_latents)
return LatentsOutput.build(latents_name=name, latents=blended_latents, seed=self.latents_a.seed)
return LatentsOutput.build(latents_name=name, latents=blended_latents)

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ import torch
from transformers import CLIPTextModel, CLIPTokenizer, T5EncoderModel, T5Tokenizer
from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, UIComponent
from invokeai.app.invocations.model import CLIPField, T5EncoderField
from invokeai.app.invocations.primitives import FluxConditioningOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
@@ -41,7 +41,10 @@ class FluxTextEncoderInvocation(BaseInvocation):
t5_max_seq_len: Literal[256, 512] = InputField(
description="Max sequence length for the T5 encoder. Expected to be 256 for FLUX schnell models and 512 for FLUX dev models."
)
prompt: str = InputField(description="Text prompt to encode.")
prompt: str = InputField(
description="Text prompt to encode.",
ui_component=UIComponent.Textarea,
)
@torch.no_grad()
def invoke(self, context: InvocationContext) -> FluxConditioningOutput:

View File

@@ -0,0 +1,59 @@
from pydantic import ValidationInfo, field_validator
from invokeai.app.invocations.baseinvocation import (
BaseInvocation,
BaseInvocationOutput,
Classification,
invocation,
invocation_output,
)
from invokeai.app.invocations.fields import InputField, OutputField
from invokeai.app.services.shared.invocation_context import InvocationContext
@invocation_output("image_panel_coordinate_output")
class ImagePanelCoordinateOutput(BaseInvocationOutput):
x_left: int = OutputField(description="The left x-coordinate of the panel.")
y_top: int = OutputField(description="The top y-coordinate of the panel.")
width: int = OutputField(description="The width of the panel.")
height: int = OutputField(description="The height of the panel.")
@invocation(
"image_panel_layout",
title="Image Panel Layout",
tags=["image", "panel", "layout"],
category="image",
version="1.0.0",
classification=Classification.Prototype,
)
class ImagePanelLayoutInvocation(BaseInvocation):
"""Get the coordinates of a single panel in a grid. (If the full image shape cannot be divided evenly into panels,
then the grid may not cover the entire image.)
"""
width: int = InputField(description="The width of the entire grid.")
height: int = InputField(description="The height of the entire grid.")
num_cols: int = InputField(ge=1, default=1, description="The number of columns in the grid.")
num_rows: int = InputField(ge=1, default=1, description="The number of rows in the grid.")
panel_col_idx: int = InputField(ge=0, default=0, description="The column index of the panel to be processed.")
panel_row_idx: int = InputField(ge=0, default=0, description="The row index of the panel to be processed.")
@field_validator("panel_col_idx")
def validate_panel_col_idx(cls, v: int, info: ValidationInfo) -> int:
if v < 0 or v >= info.data["num_cols"]:
raise ValueError(f"panel_col_idx must be between 0 and {info.data['num_cols'] - 1}")
return v
@field_validator("panel_row_idx")
def validate_panel_row_idx(cls, v: int, info: ValidationInfo) -> int:
if v < 0 or v >= info.data["num_rows"]:
raise ValueError(f"panel_row_idx must be between 0 and {info.data['num_rows'] - 1}")
return v
def invoke(self, context: InvocationContext) -> ImagePanelCoordinateOutput:
x_left = self.panel_col_idx * (self.width // self.num_cols)
y_top = self.panel_row_idx * (self.height // self.num_rows)
width = self.width // self.num_cols
height = self.height // self.num_rows
return ImagePanelCoordinateOutput(x_left=x_left, y_top=y_top, width=width, height=height)

View File

@@ -1,43 +1,4 @@
import io
from typing import Literal, Optional
import matplotlib.pyplot as plt
import numpy as np
import PIL.Image
from easing_functions import (
BackEaseIn,
BackEaseInOut,
BackEaseOut,
BounceEaseIn,
BounceEaseInOut,
BounceEaseOut,
CircularEaseIn,
CircularEaseInOut,
CircularEaseOut,
CubicEaseIn,
CubicEaseInOut,
CubicEaseOut,
ElasticEaseIn,
ElasticEaseInOut,
ElasticEaseOut,
ExponentialEaseIn,
ExponentialEaseInOut,
ExponentialEaseOut,
LinearInOut,
QuadEaseIn,
QuadEaseInOut,
QuadEaseOut,
QuarticEaseIn,
QuarticEaseInOut,
QuarticEaseOut,
QuinticEaseIn,
QuinticEaseInOut,
QuinticEaseOut,
SineEaseIn,
SineEaseInOut,
SineEaseOut,
)
from matplotlib.ticker import MaxNLocator
from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
from invokeai.app.invocations.fields import InputField
@@ -65,191 +26,3 @@ class FloatLinearRangeInvocation(BaseInvocation):
def invoke(self, context: InvocationContext) -> FloatCollectionOutput:
param_list = list(np.linspace(self.start, self.stop, self.steps))
return FloatCollectionOutput(collection=param_list)
EASING_FUNCTIONS_MAP = {
"Linear": LinearInOut,
"QuadIn": QuadEaseIn,
"QuadOut": QuadEaseOut,
"QuadInOut": QuadEaseInOut,
"CubicIn": CubicEaseIn,
"CubicOut": CubicEaseOut,
"CubicInOut": CubicEaseInOut,
"QuarticIn": QuarticEaseIn,
"QuarticOut": QuarticEaseOut,
"QuarticInOut": QuarticEaseInOut,
"QuinticIn": QuinticEaseIn,
"QuinticOut": QuinticEaseOut,
"QuinticInOut": QuinticEaseInOut,
"SineIn": SineEaseIn,
"SineOut": SineEaseOut,
"SineInOut": SineEaseInOut,
"CircularIn": CircularEaseIn,
"CircularOut": CircularEaseOut,
"CircularInOut": CircularEaseInOut,
"ExponentialIn": ExponentialEaseIn,
"ExponentialOut": ExponentialEaseOut,
"ExponentialInOut": ExponentialEaseInOut,
"ElasticIn": ElasticEaseIn,
"ElasticOut": ElasticEaseOut,
"ElasticInOut": ElasticEaseInOut,
"BackIn": BackEaseIn,
"BackOut": BackEaseOut,
"BackInOut": BackEaseInOut,
"BounceIn": BounceEaseIn,
"BounceOut": BounceEaseOut,
"BounceInOut": BounceEaseInOut,
}
EASING_FUNCTION_KEYS = Literal[tuple(EASING_FUNCTIONS_MAP.keys())]
# actually I think for now could just use CollectionOutput (which is list[Any]
@invocation(
"step_param_easing",
title="Step Param Easing",
tags=["step", "easing"],
category="step",
version="1.0.2",
)
class StepParamEasingInvocation(BaseInvocation):
"""Experimental per-step parameter easing for denoising steps"""
easing: EASING_FUNCTION_KEYS = InputField(default="Linear", description="The easing function to use")
num_steps: int = InputField(default=20, description="number of denoising steps")
start_value: float = InputField(default=0.0, description="easing starting value")
end_value: float = InputField(default=1.0, description="easing ending value")
start_step_percent: float = InputField(default=0.0, description="fraction of steps at which to start easing")
end_step_percent: float = InputField(default=1.0, description="fraction of steps after which to end easing")
# if None, then start_value is used prior to easing start
pre_start_value: Optional[float] = InputField(default=None, description="value before easing start")
# if None, then end value is used prior to easing end
post_end_value: Optional[float] = InputField(default=None, description="value after easing end")
mirror: bool = InputField(default=False, description="include mirror of easing function")
# FIXME: add alt_mirror option (alternative to default or mirror), or remove entirely
# alt_mirror: bool = InputField(default=False, description="alternative mirroring by dual easing")
show_easing_plot: bool = InputField(default=False, description="show easing plot")
def invoke(self, context: InvocationContext) -> FloatCollectionOutput:
log_diagnostics = False
# convert from start_step_percent to nearest step <= (steps * start_step_percent)
# start_step = int(np.floor(self.num_steps * self.start_step_percent))
start_step = int(np.round(self.num_steps * self.start_step_percent))
# convert from end_step_percent to nearest step >= (steps * end_step_percent)
# end_step = int(np.ceil((self.num_steps - 1) * self.end_step_percent))
end_step = int(np.round((self.num_steps - 1) * self.end_step_percent))
# end_step = int(np.ceil(self.num_steps * self.end_step_percent))
num_easing_steps = end_step - start_step + 1
# num_presteps = max(start_step - 1, 0)
num_presteps = start_step
num_poststeps = self.num_steps - (num_presteps + num_easing_steps)
prelist = list(num_presteps * [self.pre_start_value])
postlist = list(num_poststeps * [self.post_end_value])
if log_diagnostics:
context.logger.debug("start_step: " + str(start_step))
context.logger.debug("end_step: " + str(end_step))
context.logger.debug("num_easing_steps: " + str(num_easing_steps))
context.logger.debug("num_presteps: " + str(num_presteps))
context.logger.debug("num_poststeps: " + str(num_poststeps))
context.logger.debug("prelist size: " + str(len(prelist)))
context.logger.debug("postlist size: " + str(len(postlist)))
context.logger.debug("prelist: " + str(prelist))
context.logger.debug("postlist: " + str(postlist))
easing_class = EASING_FUNCTIONS_MAP[self.easing]
if log_diagnostics:
context.logger.debug("easing class: " + str(easing_class))
easing_list = []
if self.mirror: # "expected" mirroring
# if number of steps is even, squeeze duration down to (number_of_steps)/2
# and create reverse copy of list to append
# if number of steps is odd, squeeze duration down to ceil(number_of_steps/2)
# and create reverse copy of list[1:end-1]
# but if even then number_of_steps/2 === ceil(number_of_steps/2), so can just use ceil always
base_easing_duration = int(np.ceil(num_easing_steps / 2.0))
if log_diagnostics:
context.logger.debug("base easing duration: " + str(base_easing_duration))
even_num_steps = num_easing_steps % 2 == 0 # even number of steps
easing_function = easing_class(
start=self.start_value,
end=self.end_value,
duration=base_easing_duration - 1,
)
base_easing_vals = []
for step_index in range(base_easing_duration):
easing_val = easing_function.ease(step_index)
base_easing_vals.append(easing_val)
if log_diagnostics:
context.logger.debug("step_index: " + str(step_index) + ", easing_val: " + str(easing_val))
if even_num_steps:
mirror_easing_vals = list(reversed(base_easing_vals))
else:
mirror_easing_vals = list(reversed(base_easing_vals[0:-1]))
if log_diagnostics:
context.logger.debug("base easing vals: " + str(base_easing_vals))
context.logger.debug("mirror easing vals: " + str(mirror_easing_vals))
easing_list = base_easing_vals + mirror_easing_vals
# FIXME: add alt_mirror option (alternative to default or mirror), or remove entirely
# elif self.alt_mirror: # function mirroring (unintuitive behavior (at least to me))
# # half_ease_duration = round(num_easing_steps - 1 / 2)
# half_ease_duration = round((num_easing_steps - 1) / 2)
# easing_function = easing_class(start=self.start_value,
# end=self.end_value,
# duration=half_ease_duration,
# )
#
# mirror_function = easing_class(start=self.end_value,
# end=self.start_value,
# duration=half_ease_duration,
# )
# for step_index in range(num_easing_steps):
# if step_index <= half_ease_duration:
# step_val = easing_function.ease(step_index)
# else:
# step_val = mirror_function.ease(step_index - half_ease_duration)
# easing_list.append(step_val)
# if log_diagnostics: logger.debug(step_index, step_val)
#
else: # no mirroring (default)
easing_function = easing_class(
start=self.start_value,
end=self.end_value,
duration=num_easing_steps - 1,
)
for step_index in range(num_easing_steps):
step_val = easing_function.ease(step_index)
easing_list.append(step_val)
if log_diagnostics:
context.logger.debug("step_index: " + str(step_index) + ", easing_val: " + str(step_val))
if log_diagnostics:
context.logger.debug("prelist size: " + str(len(prelist)))
context.logger.debug("easing_list size: " + str(len(easing_list)))
context.logger.debug("postlist size: " + str(len(postlist)))
param_list = prelist + easing_list + postlist
if self.show_easing_plot:
plt.figure()
plt.xlabel("Step")
plt.ylabel("Param Value")
plt.title("Per-Step Values Based On Easing: " + self.easing)
plt.bar(range(len(param_list)), param_list)
# plt.plot(param_list)
ax = plt.gca()
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
buf = io.BytesIO()
plt.savefig(buf, format="png")
buf.seek(0)
im = PIL.Image.open(buf)
im.show()
buf.close()
# output array of size steps, each entry list[i] is param value for step i
return FloatCollectionOutput(collection=param_list)

View File

@@ -4,7 +4,13 @@ from typing import Optional
import torch
from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
from invokeai.app.invocations.baseinvocation import (
BaseInvocation,
BaseInvocationOutput,
Classification,
invocation,
invocation_output,
)
from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
from invokeai.app.invocations.fields import (
BoundingBoxField,
@@ -533,3 +539,23 @@ class BoundingBoxInvocation(BaseInvocation):
# endregion
@invocation(
"image_batch",
title="Image Batch",
tags=["primitives", "image", "batch", "internal"],
category="primitives",
version="1.0.0",
classification=Classification.Special,
)
class ImageBatchInvocation(BaseInvocation):
"""Create a batched generation, where the workflow is executed once for each image in the batch."""
images: list[ImageField] = InputField(min_length=1, description="The images to batch over", input=Input.Direct)
def __init__(self):
raise NotImplementedError("This class should never be executed or instantiated directly.")
def invoke(self, context: InvocationContext) -> ImageOutput:
raise NotImplementedError("This class should never be executed or instantiated directly.")

View File

@@ -86,7 +86,7 @@ class ModelLoadService(ModelLoadServiceBase):
def torch_load_file(checkpoint: Path) -> AnyModel:
scan_result = scan_file_path(checkpoint)
if scan_result.infected_files != 0:
if scan_result.infected_files != 0 or scan_result.scan_err:
raise Exception("The model at {checkpoint} is potentially infected by malware. Aborting load.")
result = torch_load(checkpoint, map_location="cpu")
return result

View File

@@ -16,6 +16,7 @@ from pydantic import (
from pydantic_core import to_jsonable_python
from invokeai.app.invocations.baseinvocation import BaseInvocation
from invokeai.app.invocations.fields import ImageField
from invokeai.app.services.shared.graph import Graph, GraphExecutionState, NodeNotFoundError
from invokeai.app.services.workflow_records.workflow_records_common import (
WorkflowWithoutID,
@@ -51,11 +52,7 @@ class SessionQueueItemNotFoundError(ValueError):
# region Batch
BatchDataType = Union[
StrictStr,
float,
int,
]
BatchDataType = Union[StrictStr, float, int, ImageField]
class NodeFieldValue(BaseModel):

View File

@@ -41,10 +41,12 @@ def infer_xlabs_ip_adapter_params_from_state_dict(state_dict: dict[str, torch.Te
hidden_dim = state_dict["double_blocks.0.processor.ip_adapter_double_stream_k_proj.weight"].shape[0]
context_dim = state_dict["double_blocks.0.processor.ip_adapter_double_stream_k_proj.weight"].shape[1]
clip_embeddings_dim = state_dict["ip_adapter_proj_model.proj.weight"].shape[1]
clip_extra_context_tokens = state_dict["ip_adapter_proj_model.proj.weight"].shape[0] // context_dim
return XlabsIpAdapterParams(
num_double_blocks=num_double_blocks,
context_dim=context_dim,
hidden_dim=hidden_dim,
clip_embeddings_dim=clip_embeddings_dim,
clip_extra_context_tokens=clip_extra_context_tokens,
)

View File

@@ -31,13 +31,16 @@ class XlabsIpAdapterParams:
hidden_dim: int
clip_embeddings_dim: int
clip_extra_context_tokens: int
class XlabsIpAdapterFlux(torch.nn.Module):
def __init__(self, params: XlabsIpAdapterParams):
super().__init__()
self.image_proj = ImageProjModel(
cross_attention_dim=params.context_dim, clip_embeddings_dim=params.clip_embeddings_dim
cross_attention_dim=params.context_dim,
clip_embeddings_dim=params.clip_embeddings_dim,
clip_extra_context_tokens=params.clip_extra_context_tokens,
)
self.ip_adapter_double_blocks = IPAdapterDoubleBlocks(
num_double_blocks=params.num_double_blocks, context_dim=params.context_dim, hidden_dim=params.hidden_dim

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -17,9 +17,23 @@ class DepthAnythingPipeline(RawModel):
self._pipeline = pipeline
def generate_depth(self, image: Image.Image) -> Image.Image:
depth_map = self._pipeline(image)["depth"]
assert isinstance(depth_map, Image.Image)
return depth_map
pipeline_result = self._pipeline(image)
predicted_depth = pipeline_result["predicted_depth"]
assert isinstance(predicted_depth, torch.Tensor)
# Convert to PIL Image.
# Note: The pipeline already returns a PIL Image (pipeline_result["depth"]), but it contains artifacts as
# described here: https://github.com/invoke-ai/InvokeAI/issues/7358.
# We implement custom post-processing logic to avoid the artifacts.
prediction = torch.nn.functional.interpolate(
predicted_depth.unsqueeze(1), size=image.size[::-1], mode="bilinear", align_corners=False
)
prediction = prediction / prediction.max()
output = prediction.squeeze().cpu().numpy()
output = (output * 255).clip(0, 255)
formatted = output.astype("uint8")
depth = Image.fromarray(formatted)
return depth
def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None):
if device is not None and device.type not in {"cpu", "cuda"}:

View File

@@ -469,7 +469,7 @@ class ModelProbe(object):
"""
# scan model
scan_result = scan_file_path(checkpoint)
if scan_result.infected_files != 0:
if scan_result.infected_files != 0 or scan_result.scan_err:
raise Exception("The model {model_name} is potentially infected by malware. Aborting import.")
@@ -485,6 +485,7 @@ MODEL_NAME_TO_PREPROCESSOR = {
"lineart anime": "lineart_anime_image_processor",
"lineart_anime": "lineart_anime_image_processor",
"lineart": "lineart_image_processor",
"soft": "hed_image_processor",
"softedge": "hed_image_processor",
"hed": "hed_image_processor",
"shuffle": "content_shuffle_image_processor",

View File

@@ -298,13 +298,12 @@ ip_adapter_sdxl = StarterModel(
previous_names=["IP Adapter SDXL"],
)
ip_adapter_flux = StarterModel(
name="Standard Reference (XLabs FLUX IP-Adapter)",
name="Standard Reference (XLabs FLUX IP-Adapter v2)",
base=BaseModelType.Flux,
source="https://huggingface.co/XLabs-AI/flux-ip-adapter/resolve/main/ip_adapter.safetensors",
source="https://huggingface.co/XLabs-AI/flux-ip-adapter-v2/resolve/main/ip_adapter.safetensors",
description="References images with a more generalized/looser degree of precision.",
type=ModelType.IPAdapter,
dependencies=[clip_vit_l_image_encoder],
previous_names=["XLabs FLUX IP-Adapter"],
)
# endregion
# region ControlNet

View File

@@ -44,7 +44,7 @@ def _fast_safetensors_reader(path: str) -> Dict[str, torch.Tensor]:
return checkpoint
def read_checkpoint_meta(path: Union[str, Path], scan: bool = False) -> Dict[str, torch.Tensor]:
def read_checkpoint_meta(path: Union[str, Path], scan: bool = True) -> Dict[str, torch.Tensor]:
if str(path).endswith(".safetensors"):
try:
path_str = path.as_posix() if isinstance(path, Path) else path
@@ -55,7 +55,7 @@ def read_checkpoint_meta(path: Union[str, Path], scan: bool = False) -> Dict[str
else:
if scan:
scan_result = scan_file_path(path)
if scan_result.infected_files != 0:
if scan_result.infected_files != 0 or scan_result.scan_err:
raise Exception(f'The model file "{path}" is potentially infected by malware. Aborting import.')
if str(path).endswith(".gguf"):
# The GGUF reader used here uses numpy memmap, so these tensors are not loaded into memory during this function

View File

@@ -58,7 +58,7 @@
"@dagrejs/dagre": "^1.1.4",
"@dagrejs/graphlib": "^2.2.4",
"@fontsource-variable/inter": "^5.1.0",
"@invoke-ai/ui-library": "^0.0.43",
"@invoke-ai/ui-library": "^0.0.44",
"@nanostores/react": "^0.7.3",
"@reduxjs/toolkit": "2.2.3",
"@roarr/browser-log-writer": "^1.3.0",

View File

@@ -24,8 +24,8 @@ dependencies:
specifier: ^5.1.0
version: 5.1.0
'@invoke-ai/ui-library':
specifier: ^0.0.43
version: 0.0.43(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.1.0)(@types/react@18.3.11)(i18next@23.15.1)(react-dom@18.3.1)(react@18.3.1)
specifier: ^0.0.44
version: 0.0.44(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.1.0)(@types/react@18.3.11)(i18next@23.15.1)(react-dom@18.3.1)(react@18.3.1)
'@nanostores/react':
specifier: ^0.7.3
version: 0.7.3(nanostores@0.11.3)(react@18.3.1)
@@ -515,8 +515,8 @@ packages:
resolution: {integrity: sha512-MV6D4VLRIHr4PkW4zMyqfrNS1mPlCTiCXwvYGtDFQYr+xHFfonhAuf9WjsSc0nyp2m0OdkSLnzmVKkZFLo25Tg==}
dev: false
/@chakra-ui/anatomy@2.3.4:
resolution: {integrity: sha512-fFIYN7L276gw0Q7/ikMMlZxP7mvnjRaWJ7f3Jsf9VtDOi6eAYIBRrhQe6+SZ0PGmoOkRaBc7gSE5oeIbgFFyrw==}
/@chakra-ui/anatomy@2.3.5:
resolution: {integrity: sha512-3im33cUOxCbISjaBlINE2u8BOwJSCdzpjCX0H+0JxK2xz26UaVA5xeI3NYHUoxDnr/QIrgfrllGxS0szYwOcyg==}
dev: false
/@chakra-ui/breakpoint-utils@2.0.8:
@@ -573,12 +573,12 @@ packages:
react: 18.3.1
dev: false
/@chakra-ui/hooks@2.4.2(react@18.3.1):
resolution: {integrity: sha512-LRKiVE1oA7afT5tbbSKAy7Uas2xFHE6IkrQdbhWCHmkHBUtPvjQQDgwtnd4IRZPmoEfNGwoJ/MQpwOM/NRTTwA==}
/@chakra-ui/hooks@2.4.3(react@18.3.1):
resolution: {integrity: sha512-Sr2zsoTZw3p7HbrUy4aLpTIkE2XXUelAUgg3NGwMzrmx75bE0qVyiuuTFOuyEzGxYVV2Fe8QtcKKilm6RwzTGg==}
peerDependencies:
react: '>=18'
dependencies:
'@chakra-ui/utils': 2.2.2(react@18.3.1)
'@chakra-ui/utils': 2.2.3(react@18.3.1)
'@zag-js/element-size': 0.31.1
copy-to-clipboard: 3.3.3
framesync: 6.1.2
@@ -596,13 +596,13 @@ packages:
react: 18.3.1
dev: false
/@chakra-ui/icons@2.2.4(@chakra-ui/react@2.10.2)(react@18.3.1):
/@chakra-ui/icons@2.2.4(@chakra-ui/react@2.10.4)(react@18.3.1):
resolution: {integrity: sha512-l5QdBgwrAg3Sc2BRqtNkJpfuLw/pWRDwwT58J6c4PqQT6wzXxyNa8Q0PForu1ltB5qEiFb1kxr/F/HO1EwNa6g==}
peerDependencies:
'@chakra-ui/react': '>=2.0.0'
react: '>=18'
dependencies:
'@chakra-ui/react': 2.10.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1)
'@chakra-ui/react': 2.10.4(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1)
react: 18.3.1
dev: false
@@ -825,8 +825,8 @@ packages:
react: 18.3.1
dev: false
/@chakra-ui/react@2.10.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-TfIHTqTlxTHYJZBtpiR5EZasPUrLYKJxdbHkdOJb5G1OQ+2c5kKl5XA7c2pMtsEptzb7KxAAIB62t3hxdfWp1w==}
/@chakra-ui/react@2.10.4(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-XyRWnuZ1Uw7Mlj5pKUGO5/WhnIHP/EOrpy6lGZC1yWlkd0eIfIpYMZ1ALTZx4KPEdbBaes48dgiMT2ROCqLhkA==}
peerDependencies:
'@emotion/react': '>=11'
'@emotion/styled': '>=11'
@@ -834,10 +834,10 @@ packages:
react: '>=18'
react-dom: '>=18'
dependencies:
'@chakra-ui/hooks': 2.4.2(react@18.3.1)
'@chakra-ui/styled-system': 2.11.2(react@18.3.1)
'@chakra-ui/theme': 3.4.6(@chakra-ui/styled-system@2.11.2)(react@18.3.1)
'@chakra-ui/utils': 2.2.2(react@18.3.1)
'@chakra-ui/hooks': 2.4.3(react@18.3.1)
'@chakra-ui/styled-system': 2.12.1(react@18.3.1)
'@chakra-ui/theme': 3.4.7(@chakra-ui/styled-system@2.12.1)(react@18.3.1)
'@chakra-ui/utils': 2.2.3(react@18.3.1)
'@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1)
'@emotion/styled': 11.13.0(@emotion/react@11.13.3)(@types/react@18.3.11)(react@18.3.1)
'@popperjs/core': 2.11.8
@@ -868,10 +868,10 @@ packages:
react: 18.3.1
dev: false
/@chakra-ui/styled-system@2.11.2(react@18.3.1):
resolution: {integrity: sha512-y++z2Uop+hjfZX9mbH88F1ikazPv32asD2er56zMJBemUAzweXnHTpiCQbluEDSUDhqmghVZAdb+5L4XLbsRxA==}
/@chakra-ui/styled-system@2.12.1(react@18.3.1):
resolution: {integrity: sha512-DQph1nDiCPtgze7nDe0a36530ByXb5VpPosKGyWMvKocVeZJcDtYG6XM0+V5a0wKuFBXsViBBRIFUTiUesJAcg==}
dependencies:
'@chakra-ui/utils': 2.2.2(react@18.3.1)
'@chakra-ui/utils': 2.2.3(react@18.3.1)
csstype: 3.1.3
transitivePeerDependencies:
- react
@@ -915,14 +915,14 @@ packages:
color2k: 2.0.3
dev: false
/@chakra-ui/theme-tools@2.2.6(@chakra-ui/styled-system@2.11.2)(react@18.3.1):
resolution: {integrity: sha512-3UhKPyzKbV3l/bg1iQN9PBvffYp+EBOoYMUaeTUdieQRPFzo2jbYR0lNCxqv8h5aGM/k54nCHU2M/GStyi9F2A==}
/@chakra-ui/theme-tools@2.2.7(@chakra-ui/styled-system@2.12.1)(react@18.3.1):
resolution: {integrity: sha512-K/VJd0QcnKik7m+qZTkggqNLep6+MPUu8IP5TUpHsnSM5R/RVjsJIR7gO8IZVAIMIGLLTIhGshHxeMekqv6LcQ==}
peerDependencies:
'@chakra-ui/styled-system': '>=2.0.0'
dependencies:
'@chakra-ui/anatomy': 2.3.4
'@chakra-ui/styled-system': 2.11.2(react@18.3.1)
'@chakra-ui/utils': 2.2.2(react@18.3.1)
'@chakra-ui/anatomy': 2.3.5
'@chakra-ui/styled-system': 2.12.1(react@18.3.1)
'@chakra-ui/utils': 2.2.3(react@18.3.1)
color2k: 2.0.3
transitivePeerDependencies:
- react
@@ -948,15 +948,15 @@ packages:
'@chakra-ui/theme-tools': 2.1.2(@chakra-ui/styled-system@2.9.2)
dev: false
/@chakra-ui/theme@3.4.6(@chakra-ui/styled-system@2.11.2)(react@18.3.1):
resolution: {integrity: sha512-ZwFBLfiMC3URwaO31ONXoKH9k0TX0OW3UjdPF3EQkQpYyrk/fm36GkkzajjtdpWEd7rzDLRsQjPmvwNaSoNDtg==}
/@chakra-ui/theme@3.4.7(@chakra-ui/styled-system@2.12.1)(react@18.3.1):
resolution: {integrity: sha512-pfewthgZTFNUYeUwGvhPQO/FTIyf375cFV1AT8N1y0aJiw4KDe7YTGm7p0aFy4AwAjH2ydMgeEx/lua4tx8qyQ==}
peerDependencies:
'@chakra-ui/styled-system': '>=2.8.0'
dependencies:
'@chakra-ui/anatomy': 2.3.4
'@chakra-ui/styled-system': 2.11.2(react@18.3.1)
'@chakra-ui/theme-tools': 2.2.6(@chakra-ui/styled-system@2.11.2)(react@18.3.1)
'@chakra-ui/utils': 2.2.2(react@18.3.1)
'@chakra-ui/anatomy': 2.3.5
'@chakra-ui/styled-system': 2.12.1(react@18.3.1)
'@chakra-ui/theme-tools': 2.2.7(@chakra-ui/styled-system@2.12.1)(react@18.3.1)
'@chakra-ui/utils': 2.2.3(react@18.3.1)
transitivePeerDependencies:
- react
dev: false
@@ -981,8 +981,8 @@ packages:
lodash.mergewith: 4.6.2
dev: false
/@chakra-ui/utils@2.2.2(react@18.3.1):
resolution: {integrity: sha512-jUPLT0JzRMWxpdzH6c+t0YMJYrvc5CLericgITV3zDSXblkfx3DsYXqU11DJTSGZI9dUKzM1Wd0Wswn4eJwvFQ==}
/@chakra-ui/utils@2.2.3(react@18.3.1):
resolution: {integrity: sha512-cldoCQuexZ6e07/9hWHKD4l1QXXlM1Nax9tuQOBvVf/EgwNZt3nZu8zZRDFlhAOKCTQDkmpLTTu+eXXjChNQOw==}
peerDependencies:
react: '>=16.8.0'
dependencies:
@@ -1675,20 +1675,20 @@ packages:
prettier: 3.3.3
dev: true
/@invoke-ai/ui-library@0.0.43(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.1.0)(@types/react@18.3.11)(i18next@23.15.1)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-t3fPYyks07ue3dEBPJuTHbeDLnDckDCOrtvc07mMDbLOnlPEZ0StaeiNGH+oO8qLzAuMAlSTdswgHfzTc2MmPw==}
/@invoke-ai/ui-library@0.0.44(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.1.0)(@types/react@18.3.11)(i18next@23.15.1)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-PDseHmdr8oi8cmrpx3UwIYHn4NduAJX2R0pM0pyM54xrCMPMgYiCbC/eOs8Gt4fBc2ziiPZ9UGoW4evnE3YJsg==}
peerDependencies:
'@fontsource-variable/inter': ^5.0.16
react: ^18.2.0
react-dom: ^18.2.0
dependencies:
'@chakra-ui/anatomy': 2.3.4
'@chakra-ui/icons': 2.2.4(@chakra-ui/react@2.10.2)(react@18.3.1)
'@chakra-ui/anatomy': 2.2.2
'@chakra-ui/icons': 2.2.4(@chakra-ui/react@2.10.4)(react@18.3.1)
'@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1)
'@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1)
'@chakra-ui/react': 2.10.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1)
'@chakra-ui/styled-system': 2.11.2(react@18.3.1)
'@chakra-ui/theme-tools': 2.2.6(@chakra-ui/styled-system@2.11.2)(react@18.3.1)
'@chakra-ui/react': 2.10.4(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(@types/react@18.3.11)(framer-motion@11.10.0)(react-dom@18.3.1)(react@18.3.1)
'@chakra-ui/styled-system': 2.9.2
'@chakra-ui/theme-tools': 2.1.2(@chakra-ui/styled-system@2.9.2)
'@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1)
'@emotion/styled': 11.13.0(@emotion/react@11.13.3)(@types/react@18.3.11)(react@18.3.1)
'@fontsource-variable/inter': 5.1.0

View File

@@ -768,7 +768,8 @@
"deletedPrivateBoardsCannotbeRestored": "Gelöschte Boards können nicht wiederhergestellt werden. Wenn Sie „Nur Board löschen“ wählen, werden die Bilder in einen privaten, nicht kategorisierten Status für den Ersteller des Bildes versetzt.",
"assetsWithCount_one": "{{count}} in der Sammlung",
"assetsWithCount_other": "{{count}} in der Sammlung",
"deletedBoardsCannotbeRestored": "Gelöschte Ordner können nicht wiederhergestellt werden. Die Auswahl von \"Nur Ordner löschen\" verschiebt Bilder in einen unkategorisierten Zustand."
"deletedBoardsCannotbeRestored": "Gelöschte Ordner können nicht wiederhergestellt werden. Die Auswahl von \"Nur Ordner löschen\" verschiebt Bilder in einen unkategorisierten Zustand.",
"updateBoardError": "Fehler beim Aktualisieren des Ordners"
},
"queue": {
"status": "Status",
@@ -871,7 +872,9 @@
"recallParameter": "{{label}} Abrufen",
"parsingFailed": "Parsing Fehlgeschlagen",
"canvasV2Metadata": "Leinwand",
"guidance": "Führung"
"guidance": "Führung",
"seamlessXAxis": "Nahtlose X Achse",
"seamlessYAxis": "Nahtlose Y Achse"
},
"popovers": {
"noiseUseCPU": {
@@ -1392,7 +1395,13 @@
"pullBboxIntoLayerOk": "Bbox in die Ebene gezogen",
"saveBboxToGallery": "Bbox in Galerie speichern",
"tool": {
"bbox": "Bbox"
"bbox": "Bbox",
"brush": "Pinsel",
"eraser": "Radiergummi",
"colorPicker": "Farbwähler",
"view": "Ansicht",
"rectangle": "Rechteck",
"move": "Verschieben"
},
"transform": {
"fitToBbox": "An Bbox anpassen",
@@ -1434,7 +1443,6 @@
"deleteReferenceImage": "Referenzbild löschen",
"referenceImage": "Referenzbild",
"opacity": "Opazität",
"resetCanvas": "Leinwand zurücksetzen",
"removeBookmark": "Lesezeichen entfernen",
"rasterLayer": "Raster-Ebene",
"rasterLayers_withCount_visible": "Raster-Ebenen ({{count}})",
@@ -1511,7 +1519,30 @@
"layer_one": "Ebene",
"layer_other": "Ebenen",
"layer_withCount_one": "Ebene ({{count}})",
"layer_withCount_other": "Ebenen ({{count}})"
"layer_withCount_other": "Ebenen ({{count}})",
"fill": {
"fillStyle": "Füllstil",
"diagonal": "Diagonal",
"vertical": "Vertikal",
"fillColor": "Füllfarbe",
"grid": "Raster",
"solid": "Solide",
"crosshatch": "Kreuzschraffur",
"horizontal": "Horizontal"
},
"filter": {
"apply": "Anwenden",
"reset": "Zurücksetzen",
"cancel": "Abbrechen",
"spandrel_filter": {
"label": "Bild-zu-Bild Modell",
"description": "Ein Bild-zu-Bild Modell auf der ausgewählten Ebene ausführen.",
"model": "Modell"
},
"filters": "Filter",
"filterType": "Filtertyp",
"filter": "Filter"
}
},
"upsell": {
"shareAccess": "Zugang teilen",

View File

@@ -122,6 +122,7 @@
"goTo": "Go to",
"hotkeysLabel": "Hotkeys",
"loadingImage": "Loading Image",
"loadingModel": "Loading Model",
"imageFailedToLoad": "Unable to Load Image",
"img2img": "Image To Image",
"inpaint": "inpaint",
@@ -262,7 +263,8 @@
"iterations_one": "Iteration",
"iterations_other": "Iterations",
"generations_one": "Generation",
"generations_other": "Generations"
"generations_other": "Generations",
"batchSize": "Batch Size"
},
"invocationCache": {
"invocationCache": "Invocation Cache",
@@ -976,6 +978,8 @@
"zoomOutNodes": "Zoom Out",
"betaDesc": "This invocation is in beta. Until it is stable, it may have breaking changes during app updates. We plan to support this invocation long-term.",
"prototypeDesc": "This invocation is a prototype. It may have breaking changes during app updates and may be removed at any time.",
"internalDesc": "This invocation is used internally by Invoke. It may have breaking changes during app updates and may be removed at any time.",
"specialDesc": "This invocation some special handling in the app. For example, Batch nodes are used to queue multiple graphs from a single workflow.",
"imageAccessError": "Unable to find image {{image_name}}, resetting to default",
"boardAccessError": "Unable to find board {{board_id}}, resetting to default",
"modelAccessError": "Unable to find model {{key}}, resetting to default",
@@ -1014,8 +1018,11 @@
"addingImagesTo": "Adding images to",
"invoke": "Invoke",
"missingFieldTemplate": "Missing field template",
"missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} missing input",
"missingInputForField": "{{nodeLabel}} -> {{fieldLabel}}: missing input",
"missingNodeTemplate": "Missing node template",
"collectionEmpty": "{{nodeLabel}} -> {{fieldLabel}} empty collection",
"collectionTooFewItems": "{{nodeLabel}} -> {{fieldLabel}}: too few items, minimum {{minItems}}",
"collectionTooManyItems": "{{nodeLabel}} -> {{fieldLabel}}: too many items, maximum {{maxItems}}",
"noModelSelected": "No model selected",
"noT5EncoderModelSelected": "No T5 Encoder model selected for FLUX generation",
"noFLUXVAEModelSelected": "No VAE model selected for FLUX generation",
@@ -1024,10 +1031,11 @@
"fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), bbox height is {{height}}",
"fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), scaled bbox width is {{width}}",
"fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), scaled bbox height is {{height}}",
"canvasIsFiltering": "Canvas is filtering",
"canvasIsTransforming": "Canvas is transforming",
"canvasIsRasterizing": "Canvas is rasterizing",
"canvasIsCompositing": "Canvas is compositing",
"canvasIsFiltering": "Canvas is busy (filtering)",
"canvasIsTransforming": "Canvas is busy (transforming)",
"canvasIsRasterizing": "Canvas is busy (rasterizing)",
"canvasIsCompositing": "Canvas is busy (compositing)",
"canvasIsSelectingObject": "Canvas is busy (selecting object)",
"noPrompts": "No prompts generated",
"noNodesInGraph": "No nodes in graph",
"systemDisconnected": "System disconnected",
@@ -1311,8 +1319,9 @@
"controlNetBeginEnd": {
"heading": "Begin / End Step Percentage",
"paragraphs": [
"The part of the of the denoising process that will have the Control Adapter applied.",
"Generally, Control Adapters applied at the start of the process guide composition, and Control Adapters applied at the end guide details."
"This setting determines which portion of the denoising (generation) process incorporates the guidance from this layer.",
"• Start Step (%): Specifies when to begin applying the guidance from this layer during the generation process.",
"• End Step (%): Specifies when to stop applying this layer's guidance and revert general guidance from the model and other settings."
]
},
"controlNetControlMode": {
@@ -1322,7 +1331,7 @@
"controlNetProcessor": {
"heading": "Processor",
"paragraphs": [
"Method of processing the input image to guide the generation process. Different processors will providedifferent effects or styles in your generated images."
"Method of processing the input image to guide the generation process. Different processors will provide different effects or styles in your generated images."
]
},
"controlNetResizeMode": {
@@ -1330,13 +1339,15 @@
"paragraphs": ["Method to fit Control Adapter's input image size to the output generation size."]
},
"ipAdapterMethod": {
"heading": "Method",
"paragraphs": ["Method by which to apply the current IP Adapter."]
"heading": "Mode",
"paragraphs": ["The mode defines how the reference image will guide the generation process."]
},
"controlNetWeight": {
"heading": "Weight",
"paragraphs": [
"Weight of the Control Adapter. Higher weight will lead to larger impacts on the final image."
"Adjusts how strongly the layer influences the generation process",
"• Higher Weight (.75-2): Creates a more significant impact on the final result.",
"• Lower Weight (0-.75): Creates a smaller impact on the final result."
]
},
"dynamicPrompts": {
@@ -1658,7 +1669,6 @@
"newControlLayerError": "Problem Creating Control Layer",
"newRasterLayerOk": "Created Raster Layer",
"newRasterLayerError": "Problem Creating Raster Layer",
"newFromImage": "New from Image",
"pullBboxIntoLayerOk": "Bbox Pulled Into Layer",
"pullBboxIntoLayerError": "Problem Pulling BBox Into Layer",
"pullBboxIntoReferenceImageOk": "Bbox Pulled Into ReferenceImage",
@@ -1671,7 +1681,7 @@
"mergingLayers": "Merging layers",
"clearHistory": "Clear History",
"bboxOverlay": "Show Bbox Overlay",
"resetCanvas": "Reset Canvas",
"newSession": "New Session",
"clearCaches": "Clear Caches",
"recalculateRects": "Recalculate Rects",
"clipToBbox": "Clip Strokes to Bbox",
@@ -1703,8 +1713,10 @@
"controlLayer": "Control Layer",
"inpaintMask": "Inpaint Mask",
"regionalGuidance": "Regional Guidance",
"canvasAsRasterLayer": "$t(controlLayers.canvas) as $t(controlLayers.rasterLayer)",
"canvasAsControlLayer": "$t(controlLayers.canvas) as $t(controlLayers.controlLayer)",
"asRasterLayer": "As $t(controlLayers.rasterLayer)",
"asRasterLayerResize": "As $t(controlLayers.rasterLayer) (Resize)",
"asControlLayer": "As $t(controlLayers.controlLayer)",
"asControlLayerResize": "As $t(controlLayers.controlLayer) (Resize)",
"referenceImage": "Reference Image",
"regionalReferenceImage": "Regional Reference Image",
"globalReferenceImage": "Global Reference Image",
@@ -1772,6 +1784,7 @@
"pullBboxIntoLayer": "Pull Bbox into Layer",
"pullBboxIntoReferenceImage": "Pull Bbox into Reference Image",
"showProgressOnCanvas": "Show Progress on Canvas",
"useImage": "Use Image",
"prompt": "Prompt",
"negativePrompt": "Negative Prompt",
"beginEndStepPercentShort": "Begin/End %",
@@ -1780,8 +1793,11 @@
"newGallerySessionDesc": "This will clear the canvas and all settings except for your model selection. Generations will be sent to the gallery.",
"newCanvasSession": "New Canvas Session",
"newCanvasSessionDesc": "This will clear the canvas and all settings except for your model selection. Generations will be staged on the canvas.",
"resetCanvasLayers": "Reset Canvas Layers",
"resetGenerationSettings": "Reset Generation Settings",
"replaceCurrent": "Replace Current",
"controlLayerEmptyState": "<UploadButton>Upload an image</UploadButton>, drag an image from the <GalleryButton>gallery</GalleryButton> onto this layer, or draw on the canvas to get started.",
"referenceImageEmptyState": "<UploadButton>Upload an image</UploadButton> or drag an image from the <GalleryButton>gallery</GalleryButton> onto this layer to get started.",
"controlMode": {
"controlMode": "Control Mode",
"balanced": "Balanced (recommended)",
@@ -1790,10 +1806,13 @@
"megaControl": "Mega Control"
},
"ipAdapterMethod": {
"ipAdapterMethod": "IP Adapter Method",
"ipAdapterMethod": "Mode",
"full": "Style and Composition",
"fullDesc": "Applies visual style (colors, textures) & composition (layout, structure).",
"style": "Style Only",
"composition": "Composition Only"
"styleDesc": "Applies visual style (colors, textures) without considering its layout.",
"composition": "Composition Only",
"compositionDesc": "Replicates layout & structure while ignoring the reference's style."
},
"fill": {
"fillColor": "Fill Color",
@@ -2109,11 +2128,73 @@
"whatsNew": {
"whatsNewInInvoke": "What's New in Invoke",
"items": [
"<StrongComponent>SD 3.5</StrongComponent>: Support for Text-to-Image in Workflows with SD 3.5 Medium and Large.",
"<StrongComponent>Canvas</StrongComponent>: Streamlined Control Layer processing and improved default Control settings."
"<StrongComponent>Workflows</StrongComponent>: Run a workflow for a collection of images using the new <StrongComponent>Image Batch</StrongComponent> node.",
"<StrongComponent>FLUX</StrongComponent>: Support for XLabs IP Adapter v2."
],
"readReleaseNotes": "Read Release Notes",
"watchRecentReleaseVideos": "Watch Recent Release Videos",
"watchUiUpdatesOverview": "Watch UI Updates Overview"
},
"supportVideos": {
"supportVideos": "Support Videos",
"gettingStarted": "Getting Started",
"controlCanvas": "Control Canvas",
"watch": "Watch",
"studioSessionsDesc1": "Check out the <StudioSessionsPlaylistLink /> for Invoke deep dives.",
"studioSessionsDesc2": "Join our <DiscordLink /> to participate in the live sessions and ask questions. Sessions are uploaded to the playlist the following week.",
"videos": {
"creatingYourFirstImage": {
"title": "Creating Your First Image",
"description": "Introduction to creating an image from scratch using Invoke's tools."
},
"usingControlLayersAndReferenceGuides": {
"title": "Using Control Layers and Reference Guides",
"description": "Learn how to guide your image creation with control layers and reference images."
},
"understandingImageToImageAndDenoising": {
"title": "Understanding Image-to-Image and Denoising",
"description": "Overview of image-to-image transformations and denoising in Invoke."
},
"exploringAIModelsAndConceptAdapters": {
"title": "Exploring AI Models and Concept Adapters",
"description": "Dive into AI models and how to use concept adapters for creative control."
},
"creatingAndComposingOnInvokesControlCanvas": {
"title": "Creating and Composing on Invoke's Control Canvas",
"description": "Learn to compose images using Invoke's control canvas."
},
"upscaling": {
"title": "Upscaling",
"description": "How to upscale images with Invoke's tools to enhance resolution."
},
"howDoIGenerateAndSaveToTheGallery": {
"title": "How Do I Generate and Save to the Gallery?",
"description": "Steps to generate and save images to the gallery."
},
"howDoIEditOnTheCanvas": {
"title": "How Do I Edit on the Canvas?",
"description": "Guide to editing images directly on the canvas."
},
"howDoIDoImageToImageTransformation": {
"title": "How Do I Do Image-to-Image Transformation?",
"description": "Tutorial on performing image-to-image transformations in Invoke."
},
"howDoIUseControlNetsAndControlLayers": {
"title": "How Do I Use Control Nets and Control Layers?",
"description": "Learn to apply control layers and controlnets to your images."
},
"howDoIUseGlobalIPAdaptersAndReferenceImages": {
"title": "How Do I Use Global IP Adapters and Reference Images?",
"description": "Introduction to adding reference images and global IP adapters."
},
"howDoIUseInpaintMasks": {
"title": "How Do I Use Inpaint Masks?",
"description": "How to apply inpaint masks for image correction and variation."
},
"howDoIOutpaint": {
"title": "How Do I Outpaint?",
"description": "Guide to outpainting beyond the original image borders."
}
}
}
}

View File

@@ -13,7 +13,7 @@
"discordLabel": "Discord",
"back": "Atrás",
"loading": "Cargando",
"postprocessing": "Postprocesado",
"postprocessing": "Postprocesamiento",
"txt2img": "De texto a imagen",
"accept": "Aceptar",
"cancel": "Cancelar",
@@ -64,7 +64,7 @@
"prevPage": "Página Anterior",
"red": "Rojo",
"alpha": "Transparencia",
"outputs": "Salidas",
"outputs": "Resultados",
"learnMore": "Aprende más",
"enabled": "Activado",
"disabled": "Desactivado",
@@ -73,7 +73,32 @@
"created": "Creado",
"save": "Guardar",
"unknownError": "Error Desconocido",
"blue": "Azul"
"blue": "Azul",
"clipboard": "Portapapeles",
"loadingImage": "Cargando la imagen",
"inpaint": "inpaint",
"ipAdapter": "Adaptador IP",
"t2iAdapter": "Adaptador T2I",
"apply": "Aplicar",
"openInViewer": "Abrir en el visor",
"off": "Apagar",
"generating": "Generando",
"ok": "De acuerdo",
"placeholderSelectAModel": "Seleccionar un modelo",
"reset": "Restablecer",
"none": "Ninguno",
"new": "Nuevo",
"dontShowMeThese": "No mostrar estos",
"loadingModel": "Cargando el modelo",
"view": "Ver",
"edit": "Editar",
"safetensors": "Safetensors",
"toResolve": "Para resolver",
"localSystem": "Sistema local",
"notInstalled": "No $t(common.installed)",
"outpaint": "outpaint",
"simple": "Sencillo",
"close": "Cerrar"
},
"gallery": {
"galleryImageSize": "Tamaño de la imagen",
@@ -85,7 +110,63 @@
"deleteImage_other": "Eliminar {{count}} Imágenes",
"deleteImagePermanent": "Las imágenes eliminadas no se pueden restaurar.",
"assets": "Activos",
"autoAssignBoardOnClick": "Asignación automática de tableros al hacer clic"
"autoAssignBoardOnClick": "Asignar automática tableros al hacer clic",
"gallery": "Galería",
"noImageSelected": "Sin imágenes seleccionadas",
"bulkDownloadRequestFailed": "Error al preparar la descarga",
"oldestFirst": "La más antigua primero",
"sideBySide": "conjuntamente",
"selectForCompare": "Seleccionar para comparar",
"alwaysShowImageSizeBadge": "Mostrar siempre las dimensiones de la imagen",
"currentlyInUse": "Esta imagen se utiliza actualmente con las siguientes funciones:",
"unableToLoad": "No se puede cargar la galería",
"selectAllOnPage": "Seleccionar todo en la página",
"selectAnImageToCompare": "Seleccione una imagen para comparar",
"bulkDownloadFailed": "Error en la descarga",
"compareHelp2": "Presione <Kbd> M </Kbd> para recorrer los modos de comparación.",
"move": "Mover",
"copy": "Copiar",
"drop": "Gota",
"displayBoardSearch": "Tablero de búsqueda",
"deleteSelection": "Borrar selección",
"downloadSelection": "Descargar selección",
"openInViewer": "Abrir en el visor",
"searchImages": "Búsqueda por metadatos",
"swapImages": "Intercambiar imágenes",
"sortDirection": "Orden de clasificación",
"showStarredImagesFirst": "Mostrar imágenes destacadas primero",
"go": "Ir",
"bulkDownloadRequested": "Preparando la descarga",
"image": "imagen",
"compareHelp4": "Presione <Kbd> Z </Kbd> o <Kbd> Esc </Kbd> para salir.",
"viewerImage": "Ver imagen",
"dropOrUpload": "$t(gallery.drop) o cargar",
"displaySearch": "Buscar imagen",
"download": "Descargar",
"exitBoardSearch": "Finalizar búsqueda",
"exitSearch": "Salir de la búsqueda de imágenes",
"featuresWillReset": "Si elimina esta imagen, dichas funciones se restablecerán inmediatamente.",
"jump": "Omitir",
"loading": "Cargando",
"newestFirst": "La más nueva primero",
"unstarImage": "Dejar de ser favorita",
"bulkDownloadRequestedDesc": "Su solicitud de descarga se está preparando. Esto puede tardar unos minutos.",
"hover": "Desplazar",
"compareHelp1": "Mantenga presionada la tecla <Kbd> Alt </Kbd> mientras hace clic en una imagen de la galería o utiliza las teclas de flecha para cambiar la imagen de comparación.",
"stretchToFit": "Estirar para encajar",
"exitCompare": "Salir de la comparación",
"starImage": "Imágenes favoritas",
"dropToUpload": "$t(gallery.drop) para cargar",
"slider": "Deslizador",
"assetsTab": "Archivos que has cargado para utilizarlos en tus proyectos.",
"imagesTab": "Imágenes que ha creado y guardado en Invoke.",
"compareImage": "Comparar imagen",
"boardsSettings": "Ajustes de los tableros",
"imagesSettings": "Configuración de imágenes de la galería",
"compareHelp3": "Presione <Kbd> C </Kbd> para intercambiar las imágenes comparadas.",
"showArchivedBoards": "Mostrar paneles archivados",
"closeViewer": "Cerrar visor",
"openViewer": "Abrir visor"
},
"modelManager": {
"modelManager": "Gestor de Modelos",
@@ -131,7 +212,13 @@
"modelDeleted": "Modelo eliminado",
"modelDeleteFailed": "Error al borrar el modelo",
"settings": "Ajustes",
"syncModels": "Sincronizar las plantillas"
"syncModels": "Sincronizar las plantillas",
"clipEmbed": "Incrustar CLIP",
"addModels": "Añadir modelos",
"advanced": "Avanzado",
"clipGEmbed": "Incrustar CLIP-G",
"cancel": "Cancelar",
"clipLEmbed": "Incrustar CLIP-L"
},
"parameters": {
"images": "Imágenes",
@@ -158,19 +245,19 @@
"useSeed": "Usar Semilla",
"useAll": "Usar Todo",
"info": "Información",
"showOptionsPanel": "Mostrar panel de opciones",
"showOptionsPanel": "Mostrar panel lateral (O o T)",
"symmetry": "Simetría",
"copyImage": "Copiar la imagen",
"general": "General",
"denoisingStrength": "Intensidad de la eliminación del ruido",
"seamlessXAxis": "Eje x",
"seamlessYAxis": "Eje y",
"seamlessXAxis": "Eje X sin juntas",
"seamlessYAxis": "Eje Y sin juntas",
"scheduler": "Programador",
"positivePromptPlaceholder": "Prompt Positivo",
"negativePromptPlaceholder": "Prompt Negativo",
"controlNetControlMode": "Modo de control",
"clipSkip": "Omitir el CLIP",
"maskBlur": "Difuminar",
"maskBlur": "Desenfoque de máscara",
"patchmatchDownScaleSize": "Reducir a escala",
"coherenceMode": "Modo"
},
@@ -202,16 +289,19 @@
"serverError": "Error en el servidor",
"canceled": "Procesando la cancelación",
"connected": "Conectado al servidor",
"uploadFailedInvalidUploadDesc": "Debe ser una sola imagen PNG o JPEG",
"parameterSet": "Conjunto de parámetros",
"parameterNotSet": "Parámetro no configurado",
"uploadFailedInvalidUploadDesc": "Deben ser imágenes PNG o JPEG.",
"parameterSet": "Parámetro recuperado",
"parameterNotSet": "Parámetro no recuperado",
"problemCopyingImage": "No se puede copiar la imagen",
"errorCopied": "Error al copiar",
"baseModelChanged": "Modelo base cambiado",
"addedToBoard": "Añadido al tablero",
"addedToBoard": "Se agregó a los activos del panel {{name}}",
"baseModelChangedCleared_one": "Borrado o desactivado {{count}} submodelo incompatible",
"baseModelChangedCleared_many": "Borrados o desactivados {{count}} submodelos incompatibles",
"baseModelChangedCleared_other": "Borrados o desactivados {{count}} submodelos incompatibles"
"baseModelChangedCleared_other": "Borrados o desactivados {{count}} submodelos incompatibles",
"addedToUncategorized": "Añadido a los activos del tablero $t(boards.uncategorized)",
"imagesWillBeAddedTo": "Las imágenes subidas se añadirán a los activos del panel {{boardName}}.",
"layerCopiedToClipboard": "Capa copiada en el portapapeles"
},
"accessibility": {
"invokeProgressBar": "Activar la barra de progreso",
@@ -226,7 +316,8 @@
"mode": "Modo",
"submitSupportTicket": "Enviar Ticket de Soporte",
"toggleRightPanel": "Activar o desactivar el panel derecho (G)",
"toggleLeftPanel": "Activar o desactivar el panel izquierdo (T)"
"toggleLeftPanel": "Activar o desactivar el panel izquierdo (T)",
"uploadImages": "Cargar imagen(es)"
},
"nodes": {
"zoomInNodes": "Acercar",
@@ -238,7 +329,8 @@
"showMinimapnodes": "Mostrar el minimapa",
"reloadNodeTemplates": "Recargar las plantillas de nodos",
"loadWorkflow": "Cargar el flujo de trabajo",
"downloadWorkflow": "Descargar el flujo de trabajo en un archivo JSON"
"downloadWorkflow": "Descargar el flujo de trabajo en un archivo JSON",
"boardAccessError": "No se puede encontrar el panel {{board_id}}, se está restableciendo al valor predeterminado"
},
"boards": {
"autoAddBoard": "Agregar panel automáticamente",
@@ -255,7 +347,7 @@
"bottomMessage": "Al eliminar este panel y las imágenes que contiene, se restablecerán las funciones que los estén utilizando actualmente.",
"deleteBoardAndImages": "Borrar el panel y las imágenes",
"loading": "Cargando...",
"deletedBoardsCannotbeRestored": "Los paneles eliminados no se pueden restaurar. Al Seleccionar 'Borrar Solo el Panel' transferirá las imágenes a un estado sin categorizar.",
"deletedBoardsCannotbeRestored": "Los paneles eliminados no se pueden restaurar. Al Seleccionar 'Borrar solo el panel' transferirá las imágenes a un estado sin categorizar.",
"move": "Mover",
"menuItemAutoAdd": "Agregar automáticamente a este panel",
"searchBoard": "Buscando paneles…",
@@ -263,29 +355,33 @@
"downloadBoard": "Descargar panel",
"deleteBoardOnly": "Borrar solo el panel",
"myBoard": "Mi panel",
"noMatching": "No hay paneles que coincidan",
"noMatching": "Sin paneles coincidentes",
"imagesWithCount_one": "{{count}} imagen",
"imagesWithCount_many": "{{count}} imágenes",
"imagesWithCount_other": "{{count}} imágenes",
"assetsWithCount_one": "{{count}} activo",
"assetsWithCount_many": "{{count}} activos",
"assetsWithCount_other": "{{count}} activos",
"hideBoards": "Ocultar Paneles",
"addPrivateBoard": "Agregar un tablero privado",
"addSharedBoard": "Agregar Panel Compartido",
"hideBoards": "Ocultar paneles",
"addPrivateBoard": "Agregar un panel privado",
"addSharedBoard": "Añadir panel compartido",
"boards": "Paneles",
"archiveBoard": "Archivar Panel",
"archiveBoard": "Archivar panel",
"archived": "Archivado",
"selectedForAutoAdd": "Seleccionado para agregar automáticamente",
"unarchiveBoard": "Desarchivar el tablero",
"noBoards": "No hay tableros {{boardType}}",
"shared": "Carpetas compartidas",
"deletedPrivateBoardsCannotbeRestored": "Los tableros eliminados no se pueden restaurar. Al elegir \"Eliminar solo tablero\", las imágenes se colocan en un estado privado y sin categoría para el creador de la imagen."
"unarchiveBoard": "Desarchivar el panel",
"noBoards": "No hay paneles {{boardType}}",
"shared": "Paneles compartidos",
"deletedPrivateBoardsCannotbeRestored": "Los paneles eliminados no se pueden restaurar. Al elegir \"Eliminar solo el panel\", las imágenes se colocan en un estado privado y sin categoría para el creador de la imagen.",
"viewBoards": "Ver paneles",
"private": "Paneles privados",
"updateBoardError": "No se pudo actualizar el panel"
},
"accordions": {
"compositing": {
"title": "Composición",
"infillTab": "Relleno"
"infillTab": "Relleno",
"coherenceTab": "Parámetros de la coherencia"
},
"generation": {
"title": "Generación"
@@ -309,7 +405,10 @@
"workflows": "Flujos de trabajo",
"models": "Modelos",
"modelsTab": "$t(ui.tabs.models) $t(common.tab)",
"workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)"
"workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)",
"upscaling": "Upscaling",
"gallery": "Galería",
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)"
}
},
"queue": {
@@ -317,12 +416,81 @@
"front": "Delante",
"batchQueuedDesc_one": "Se agregó {{count}} sesión a {{direction}} la cola",
"batchQueuedDesc_many": "Se agregaron {{count}} sesiones a {{direction}} la cola",
"batchQueuedDesc_other": "Se agregaron {{count}} sesiones a {{direction}} la cola"
"batchQueuedDesc_other": "Se agregaron {{count}} sesiones a {{direction}} la cola",
"clearQueueAlertDialog": "Al vaciar la cola se cancela inmediatamente cualquier elemento de procesamiento y se vaciará la cola por completo. Los filtros pendientes se cancelarán.",
"time": "Tiempo",
"clearFailed": "Error al vaciar la cola",
"cancelFailed": "Error al cancelar el elemento",
"resumeFailed": "Error al reanudar el proceso",
"pause": "Pausar",
"pauseTooltip": "Pausar el proceso",
"cancelBatchSucceeded": "Lote cancelado",
"pruneSucceeded": "Se purgaron {{item_count}} elementos completados de la cola",
"pruneFailed": "Error al purgar la cola",
"cancelBatchFailed": "Error al cancelar los lotes",
"pauseFailed": "Error al pausar el proceso",
"status": "Estado",
"origin": "Origen",
"destination": "Destino",
"generations_one": "Generación",
"generations_many": "Generaciones",
"generations_other": "Generaciones",
"resume": "Reanudar",
"queueEmpty": "Cola vacía",
"cancelItem": "Cancelar elemento",
"cancelBatch": "Cancelar lote",
"openQueue": "Abrir la cola",
"completed": "Completado",
"enqueueing": "Añadir lotes a la cola",
"clear": "Limpiar",
"pauseSucceeded": "Proceso pausado",
"resumeSucceeded": "Proceso reanudado",
"resumeTooltip": "Reanudar proceso",
"cancel": "Cancelar",
"cancelTooltip": "Cancelar artículo actual",
"pruneTooltip": "Purgar {{item_count}} elementos completados",
"batchQueued": "Lote en cola",
"pending": "Pendiente",
"item": "Elemento",
"total": "Total",
"in_progress": "En proceso",
"failed": "Fallido",
"completedIn": "Completado en",
"upscaling": "Upscaling",
"canvas": "Lienzo",
"generation": "Generación",
"workflows": "Flujo de trabajo",
"other": "Otro",
"queueFront": "Añadir al principio de la cola",
"gallery": "Galería",
"batchFieldValues": "Valores de procesamiento por lotes",
"session": "Sesión",
"notReady": "La cola aún no está lista",
"graphQueued": "Gráfico en cola",
"clearQueueAlertDialog2": "¿Estás seguro que deseas vaciar la cola?",
"next": "Siguiente",
"iterations_one": "Interacción",
"iterations_many": "Interacciones",
"iterations_other": "Interacciones",
"current": "Actual",
"queue": "Cola",
"queueBack": "Añadir a la cola",
"cancelSucceeded": "Elemento cancelado",
"clearTooltip": "Cancelar y limpiar todos los elementos",
"clearSucceeded": "Cola vaciada",
"canceled": "Cancelado",
"batch": "Lote",
"graphFailedToQueue": "Error al poner el gráfico en cola",
"batchFailedToQueue": "Error al poner en cola el lote",
"prompts_one": "Prompt",
"prompts_many": "Prompts",
"prompts_other": "Prompts",
"prune": "Eliminar"
},
"upsell": {
"inviteTeammates": "Invitar compañeros de equipo",
"shareAccess": "Compartir acceso",
"professionalUpsell": "Disponible en la edición profesional de Invoke. Haz clic aquí o visita invoke.com/pricing para obtener más detalles."
"professionalUpsell": "Disponible en la edición profesional de Invoke. Haga clic aquí o visite invoke.com/pricing para obtener más detalles."
},
"controlLayers": {
"layer_one": "Capa",
@@ -330,6 +498,415 @@
"layer_other": "Capas",
"layer_withCount_one": "({{count}}) capa",
"layer_withCount_many": "({{count}}) capas",
"layer_withCount_other": "({{count}}) capas"
"layer_withCount_other": "({{count}}) capas",
"copyToClipboard": "Copiar al portapapeles"
},
"whatsNew": {
"readReleaseNotes": "Leer las notas de la versión",
"watchRecentReleaseVideos": "Ver videos de versiones recientes",
"watchUiUpdatesOverview": "Descripción general de las actualizaciones de la interfaz de usuario de Watch",
"whatsNewInInvoke": "Novedades en Invoke",
"items": [
"<StrongComponent>SD 3.5</StrongComponent>: compatibilidad con SD 3.5 Medium y Large.",
"<StrongComponent>Lienzo</StrongComponent>: Se ha simplificado el procesamiento de la capa de control y se ha mejorado la configuración predeterminada del control."
]
},
"invocationCache": {
"enableFailed": "Error al activar la cache",
"cacheSize": "Tamaño de la caché",
"hits": "Accesos a la caché",
"invocationCache": "Caché",
"misses": "Errores de la caché",
"clear": "Limpiar",
"maxCacheSize": "Tamaño máximo de la caché",
"enableSucceeded": "Cache activada",
"clearFailed": "Error al borrar la cache",
"enable": "Activar",
"useCache": "Uso de la caché",
"disableSucceeded": "Caché desactivada",
"clearSucceeded": "Caché borrada",
"disable": "Desactivar",
"disableFailed": "Error al desactivar la caché"
},
"hrf": {
"hrf": "Solución de alta resolución",
"enableHrf": "Activar corrección de alta resolución",
"metadata": {
"enabled": "Corrección de alta resolución activada",
"strength": "Forzar la corrección de alta resolución",
"method": "Método de corrección de alta resolución"
},
"upscaleMethod": "Método de expansión"
},
"prompt": {
"addPromptTrigger": "Añadir activador de los avisos",
"compatibleEmbeddings": "Incrustaciones compatibles",
"noMatchingTriggers": "No hay activadores coincidentes"
},
"hotkeys": {
"hotkeys": "Atajo del teclado",
"canvas": {
"selectViewTool": {
"desc": "Selecciona la herramienta de Visualización.",
"title": "Visualización"
},
"cancelFilter": {
"title": "Cancelar el filtro",
"desc": "Cancelar el filtro pendiente."
},
"applyTransform": {
"title": "Aplicar la transformación",
"desc": "Aplicar la transformación pendiente a la capa seleccionada."
},
"applyFilter": {
"desc": "Aplicar el filtro pendiente a la capa seleccionada.",
"title": "Aplicar filtro"
},
"selectBrushTool": {
"title": "Pincel",
"desc": "Selecciona la herramienta pincel."
},
"selectBboxTool": {
"desc": "Seleccionar la herramienta de selección del marco.",
"title": "Selección del marco"
},
"selectMoveTool": {
"desc": "Selecciona la herramienta Mover.",
"title": "Mover"
},
"selectRectTool": {
"title": "Rectángulo",
"desc": "Selecciona la herramienta Rectángulo."
},
"decrementToolWidth": {
"title": "Reducir el ancho de la herramienta",
"desc": "Disminuye la anchura de la herramienta pincel o goma de borrar, según la que esté seleccionada."
},
"incrementToolWidth": {
"title": "Incrementar la anchura de la herramienta",
"desc": "Aumenta la anchura de la herramienta pincel o goma de borrar, según la que esté seleccionada."
},
"fitBboxToCanvas": {
"title": "Ajustar bordes al lienzo",
"desc": "Escala y posiciona la vista para ajustarla a los bodes."
},
"fitLayersToCanvas": {
"title": "Ajustar capas al lienzo",
"desc": "Escala y posiciona la vista para que se ajuste a todas las capas visibles."
},
"setFillToWhite": {
"title": "Establecer color en blanco",
"desc": "Establece el color actual de la herramienta en blanco."
},
"resetSelected": {
"title": "Restablecer capa",
"desc": "Restablecer la capa seleccionada. Solo se aplica a Máscara de retoque y Guía regional."
},
"setZoomTo400Percent": {
"desc": "Ajuste la aplicación del lienzo al 400%.",
"title": "Ampliar al 400%"
},
"transformSelected": {
"desc": "Transformar la capa seleccionada.",
"title": "Transformar"
},
"selectColorPickerTool": {
"title": "Selector de color",
"desc": "Seleccione la herramienta de selección de color."
},
"selectEraserTool": {
"title": "Borrador",
"desc": "Selecciona la herramienta Borrador."
},
"setZoomTo100Percent": {
"title": "Ampliar al 100%",
"desc": "Ajuste ampliar el lienzo al 100%."
},
"undo": {
"title": "Deshacer",
"desc": "Deshacer la última acción en el lienzo."
},
"nextEntity": {
"desc": "Seleccione la siguiente capa de la lista.",
"title": "Capa siguiente"
},
"redo": {
"title": "Rehacer",
"desc": "Rehacer la última acción en el lienzo."
},
"prevEntity": {
"title": "Capa anterior",
"desc": "Seleccione la capa anterior de la lista."
},
"title": "Lienzo",
"setZoomTo200Percent": {
"title": "Ampliar al 200%",
"desc": "Ajuste la ampliación del lienzo al 200%."
},
"setZoomTo800Percent": {
"title": "Ampliar al 800%",
"desc": "Ajuste la ampliación del lienzo al 800%."
},
"filterSelected": {
"desc": "Filtra la capa seleccionada. Solo se aplica a las capas Ráster y Control.",
"title": "Filtrar"
},
"cancelTransform": {
"title": "Cancelar transformación",
"desc": "Cancelar la transformación pendiente."
},
"deleteSelected": {
"title": "Borrar la capa",
"desc": "Borrar la capa seleccionada."
},
"quickSwitch": {
"desc": "Cambiar entre las dos últimas capas seleccionadas. Si una capa está seleccionada, cambia siempre entre ella y la última capa no seleccionada.",
"title": "Cambio rápido de capa"
}
},
"app": {
"selectModelsTab": {
"title": "Seleccione la pestaña Modelos",
"desc": "Selecciona la pestaña Modelos."
},
"focusPrompt": {
"desc": "Mueve el foco del cursor a la indicación positiva.",
"title": "Enfoque"
},
"toggleLeftPanel": {
"title": "Alternar panel izquierdo",
"desc": "Mostrar u ocultar el panel izquierdo."
},
"selectQueueTab": {
"title": "Seleccione la pestaña Cola",
"desc": "Seleccione la pestaña Cola."
},
"selectCanvasTab": {
"title": "Seleccione la pestaña Lienzo",
"desc": "Selecciona la pestaña Lienzo."
},
"clearQueue": {
"title": "Vaciar cola",
"desc": "Cancelar y variar todos los elementos de la cola."
},
"selectUpscalingTab": {
"title": "Selecciona la pestaña Ampliar",
"desc": "Selecciona la pestaña Aumento de escala."
},
"togglePanels": {
"desc": "Muestra u oculta los paneles izquierdo y derecho a la vez.",
"title": "Alternar paneles"
},
"toggleRightPanel": {
"title": "Alternar panel derecho",
"desc": "Mostrar u ocultar el panel derecho."
},
"invokeFront": {
"desc": "Pone en cola la solicitud de compilación y la agrega al principio de la cola.",
"title": "Invocar (frente)"
},
"cancelQueueItem": {
"title": "Cancelar",
"desc": "Cancelar el elemento de la cola que se está procesando."
},
"invoke": {
"desc": "Pone en cola la solicitud de compilación y la agrega al final de la cola.",
"title": "Invocar"
},
"title": "Aplicación",
"selectWorkflowsTab": {
"title": "Seleccione la pestaña Flujos de trabajo",
"desc": "Selecciona la pestaña Flujos de trabajo."
},
"resetPanelLayout": {
"title": "Reiniciar la posición del panel",
"desc": "Restablece los paneles izquierdo y derecho a su tamaño y disposición por defecto."
}
},
"workflows": {
"addNode": {
"title": "Añadir nodo",
"desc": "Abrir añadir nodo."
},
"selectAll": {
"title": "Seleccionar todo",
"desc": "Seleccione todos los nodos y enlaces."
},
"deleteSelection": {
"desc": "Borrar todos los nodos y enlaces seleccionados.",
"title": "Borrar"
},
"undo": {
"desc": "Deshaga la última acción.",
"title": "Deshacer"
},
"redo": {
"desc": "Rehacer la última acción.",
"title": "Rehacer"
},
"pasteSelection": {
"desc": "Pegar nodos y bordes copiados.",
"title": "Pegar"
},
"title": "Flujos de trabajo",
"copySelection": {
"desc": "Copiar nodos y bordes seleccionados.",
"title": "Copiar"
},
"pasteSelectionWithEdges": {
"desc": "Pega los nodos copiados, los enlaces y todos los enlaces conectados a los nodos copiados.",
"title": "Pegar con enlaces"
}
},
"viewer": {
"useSize": {
"title": "Usar dimensiones",
"desc": "Utiliza las dimensiones de la imagen actual como el tamaño del borde."
},
"remix": {
"title": "Remezcla",
"desc": "Recupera todos los metadatos excepto la semilla de la imagen actual."
},
"loadWorkflow": {
"desc": "Carga el flujo de trabajo guardado de la imagen actual (si tiene uno).",
"title": "Cargar flujo de trabajo"
},
"recallAll": {
"desc": "Recupera todos los metadatos de la imagen actual.",
"title": "Recuperar todos los metadatos"
},
"recallPrompts": {
"desc": "Recuerde las indicaciones positivas y negativas de la imagen actual.",
"title": "Recordatorios"
},
"recallSeed": {
"title": "Recuperar semilla",
"desc": "Recupera la semilla de la imagen actual."
},
"runPostprocessing": {
"title": "Ejecutar posprocesamiento",
"desc": "Ejecutar el posprocesamiento seleccionado en la imagen actual."
},
"toggleMetadata": {
"title": "Mostrar/ocultar los metadatos",
"desc": "Mostrar u ocultar la superposición de metadatos de la imagen actual."
},
"nextComparisonMode": {
"desc": "Desplácese por los modos de comparación.",
"title": "Siguiente comparación"
},
"title": "Visor de imágenes",
"toggleViewer": {
"title": "Mostrar/Ocultar el visor de imágenes",
"desc": "Mostrar u ocultar el visor de imágenes. Solo disponible en la pestaña Lienzo."
},
"swapImages": {
"title": "Intercambiar imágenes en la comparación",
"desc": "Intercambia las imágenes que se están comparando."
}
},
"gallery": {
"clearSelection": {
"title": "Limpiar selección",
"desc": "Borrar la selección actual, si hay alguna."
},
"galleryNavUp": {
"title": "Subir",
"desc": "Navega hacia arriba en la cuadrícula de la galería y selecciona esa imagen. Si estás en la parte superior de la página, ve a la página anterior."
},
"galleryNavLeft": {
"title": "Izquierda",
"desc": "Navegue hacia la izquierda en la rejilla de la galería, seleccionando esa imagen. Si está en la primera imagen de la fila, vaya a la fila anterior. Si está en la primera imagen de la página, vaya a la página anterior."
},
"galleryNavDown": {
"title": "Bajar",
"desc": "Navegue hacia abajo en la parrilla de la galería, seleccionando esa imagen. Si se encuentra al final de la página, vaya a la página siguiente."
},
"galleryNavRight": {
"title": "A la derecha",
"desc": "Navegue hacia la derecha en la rejilla de la galería, seleccionando esa imagen. Si está en la última imagen de la fila, vaya a la fila siguiente. Si está en la última imagen de la página, vaya a la página siguiente."
},
"galleryNavUpAlt": {
"desc": "Igual que arriba, pero selecciona la imagen de comparación, abriendo el modo de comparación si no está ya abierto.",
"title": "Arriba (Comparar imagen)"
},
"deleteSelection": {
"desc": "Borrar todas las imágenes seleccionadas. Por defecto, se le pedirá que confirme la eliminación. Si las imágenes están actualmente en uso en la aplicación, se te avisará.",
"title": "Borrar"
},
"title": "Galería",
"selectAllOnPage": {
"title": "Seleccionar todo en la página",
"desc": "Seleccionar todas las imágenes en la página actual."
}
},
"searchHotkeys": "Buscar teclas de acceso rápido",
"noHotkeysFound": "Sin teclas de acceso rápido",
"clearSearch": "Limpiar la búsqueda"
},
"metadata": {
"guidance": "Orientación",
"createdBy": "Creado por",
"noImageDetails": "Sin detalles en la imagen",
"cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)",
"height": "Altura",
"imageDimensions": "Dimensiones de la imagen",
"seamlessXAxis": "Eje X sin juntas",
"seamlessYAxis": "Eje Y sin juntas",
"generationMode": "Modo de generación",
"scheduler": "Programador",
"width": "Ancho",
"Threshold": "Umbral de ruido",
"canvasV2Metadata": "Lienzo",
"metadata": "Metadatos",
"model": "Modelo",
"allPrompts": "Todas las indicaciones",
"cfgScale": "Escala CFG",
"imageDetails": "Detalles de la imagen",
"negativePrompt": "Indicación negativa",
"noMetaData": "Sin metadatos",
"parameterSet": "Parámetro {{parameter}} establecido",
"vae": "Autocodificador",
"workflow": "Flujo de trabajo",
"seed": "Semilla",
"strength": "Forzar imagen a imagen",
"recallParameters": "Parámetros de recuperación",
"recallParameter": "Recuperar {{label}}",
"steps": "Pasos",
"noRecallParameters": "Sin parámetros para recuperar",
"parsingFailed": "Error al analizar"
},
"system": {
"logLevel": {
"debug": "Depurar",
"info": "Información",
"warn": "Advertir",
"fatal": "Grave",
"error": "Error",
"trace": "Rastro",
"logLevel": "Nivel del registro"
},
"enableLogging": "Activar registro",
"logNamespaces": {
"workflows": "Flujos de trabajo",
"system": "Sistema",
"metadata": "Metadatos",
"gallery": "Galería",
"logNamespaces": "Espacios para los nombres de registro",
"generation": "Generación",
"events": "Eventos",
"canvas": "Lienzo",
"config": "Ajustes",
"models": "Modelos",
"queue": "Cola"
}
},
"newUserExperience": {
"downloadStarterModels": "Descargar modelos de inicio",
"toGetStarted": "Para empezar, introduzca un mensaje en el cuadro y haga clic en <StrongComponent>Invocar</StrongComponent> para generar su primera imagen. Seleccione una plantilla para mejorar los resultados. Puede elegir guardar sus imágenes directamente en <StrongComponent>Galería</StrongComponent> o editarlas en <StrongComponent>Lienzo</StrongComponent>.",
"importModels": "Importar modelos",
"noModelsInstalled": "Parece que no tienes ningún modelo instalado",
"gettingStartedSeries": "¿Desea más orientación? Consulte nuestra <LinkComponent>Serie de introducción</LinkComponent> para obtener consejos sobre cómo aprovechar todo el potencial de Invoke Studio.",
"toGetStartedLocal": "Para empezar, asegúrate de descargar o importar los modelos necesarios para ejecutar Invoke. A continuación, introduzca un mensaje en el cuadro y haga clic en <StrongComponent>Invocar</StrongComponent> para generar su primera imagen. Seleccione una plantilla para mejorar los resultados. Puede elegir guardar sus imágenes directamente en <StrongComponent>Galería</StrongComponent> o editarlas en el <StrongComponent>Lienzo</StrongComponent>."
}
}

View File

@@ -1985,7 +1985,6 @@
"inpaintMask_withCount_many": "Remplir les masques",
"inpaintMask_withCount_other": "Remplir les masques",
"newImg2ImgCanvasFromImage": "Nouvelle Img2Img à partir de l'image",
"resetCanvas": "Réinitialiser la Toile",
"bboxOverlay": "Afficher la superposition des Bounding Box",
"moveToFront": "Déplacer vers le permier plan",
"moveToBack": "Déplacer vers l'arrière plan",
@@ -2034,7 +2033,6 @@
"help2": "Commencez par un point <Bold>Inclure</Bold> au sein de l'objet cible. Ajoutez d'autres points pour affiner la sélection. Moins de points produisent généralement de meilleurs résultats.",
"help3": "Inversez la sélection pour sélectionner tout sauf l'objet cible."
},
"canvasAsControlLayer": "$t(controlLayers.canvas) en tant que $t(controlLayers.controlLayer)",
"convertRegionalGuidanceTo": "Convertir $t(controlLayers.regionalGuidance) vers",
"copyRasterLayerTo": "Copier $t(controlLayers.rasterLayer) vers",
"newControlLayer": "Nouveau $t(controlLayers.controlLayer)",
@@ -2044,8 +2042,7 @@
"convertInpaintMaskTo": "Convertir $t(controlLayers.inpaintMask) vers",
"copyControlLayerTo": "Copier $t(controlLayers.controlLayer) vers",
"newInpaintMask": "Nouveau $t(controlLayers.inpaintMask)",
"newRasterLayer": "Nouveau $t(controlLayers.rasterLayer)",
"canvasAsRasterLayer": "$t(controlLayers.canvas) en tant que $t(controlLayers.rasterLayer)"
"newRasterLayer": "Nouveau $t(controlLayers.rasterLayer)"
},
"upscaling": {
"exceedsMaxSizeDetails": "La limite maximale d'agrandissement est de {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixels. Veuillez essayer une image plus petite ou réduire votre sélection d'échelle.",

View File

@@ -94,7 +94,9 @@
"view": "Vista",
"close": "Chiudi",
"clipboard": "Appunti",
"ok": "Ok"
"ok": "Ok",
"generating": "Generazione",
"loadingModel": "Caricamento del modello"
},
"gallery": {
"galleryImageSize": "Dimensione dell'immagine",
@@ -597,7 +599,18 @@
"huggingFace": "HuggingFace",
"huggingFaceRepoID": "HuggingFace Repository ID",
"clipEmbed": "CLIP Embed",
"t5Encoder": "T5 Encoder"
"t5Encoder": "T5 Encoder",
"hfTokenInvalidErrorMessage": "Gettone HuggingFace non valido o mancante.",
"hfTokenRequired": "Stai tentando di scaricare un modello che richiede un gettone HuggingFace valido.",
"hfTokenUnableToVerifyErrorMessage": "Impossibile verificare il gettone HuggingFace. Ciò è probabilmente dovuto a un errore di rete. Riprova più tardi.",
"hfTokenHelperText": "Per utilizzare alcuni modelli è necessario un gettone HF. Fai clic qui per creare o ottenere il tuo gettone.",
"hfTokenInvalid": "Gettone HF non valido o mancante",
"hfTokenUnableToVerify": "Impossibile verificare il gettone HF",
"hfTokenSaved": "Gettone HF salvato",
"hfForbidden": "Non hai accesso a questo modello HF",
"hfTokenLabel": "Gettone HuggingFace (richiesto per alcuni modelli)",
"hfForbiddenErrorMessage": "Consigliamo di visitare la pagina del repository su HuggingFace.com. Il proprietario potrebbe richiedere l'accettazione dei termini per poter effettuare il download.",
"hfTokenInvalidErrorMessage2": "Aggiornalo in "
},
"parameters": {
"images": "Immagini",
@@ -699,7 +712,9 @@
"staged": "Maschera espansa",
"optimizedImageToImage": "Immagine-a-immagine ottimizzata",
"sendToCanvas": "Invia alla Tela",
"coherenceMinDenoise": "Riduzione minima del rumore"
"coherenceMinDenoise": "Min rid. rumore",
"recallMetadata": "Richiama i metadati",
"disabledNoRasterContent": "Disabilitato (nessun contenuto Raster)"
},
"settings": {
"models": "Modelli",
@@ -737,7 +752,8 @@
"confirmOnNewSession": "Conferma su nuova sessione",
"enableModelDescriptions": "Abilita le descrizioni dei modelli nei menu a discesa",
"modelDescriptionsDisabled": "Descrizioni dei modelli nei menu a discesa disabilitate",
"modelDescriptionsDisabledDesc": "Le descrizioni dei modelli nei menu a discesa sono state disabilitate. Abilitale nelle Impostazioni."
"modelDescriptionsDisabledDesc": "Le descrizioni dei modelli nei menu a discesa sono state disabilitate. Abilitale nelle Impostazioni.",
"showDetailedInvocationProgress": "Mostra dettagli avanzamento"
},
"toast": {
"uploadFailed": "Caricamento fallito",
@@ -1263,8 +1279,9 @@
},
"paramDenoisingStrength": {
"paragraphs": [
"Quanto rumore viene aggiunto all'immagine in ingresso.",
"0 risulterà in un'immagine identica, mentre 1 risulterà in un'immagine completamente nuova."
"Controlla la differenza tra l'immagine generata e il/i livello/i raster.",
"Una forza inferiore rimane più vicina ai livelli raster visibili combinati. Una forza superiore si basa maggiormente sul prompt globale.",
"Se non sono presenti livelli raster con contenuto visibile, questa impostazione viene ignorata."
],
"heading": "Forza di riduzione del rumore"
},
@@ -1276,7 +1293,7 @@
},
"infillMethod": {
"paragraphs": [
"Metodo di riempimento durante il processo di Outpainting o Inpainting."
"Metodo di riempimento durante il processo di Outpaint o Inpaint."
],
"heading": "Metodo di riempimento"
},
@@ -1444,7 +1461,7 @@
"heading": "Livello minimo di riduzione del rumore",
"paragraphs": [
"Intensità minima di riduzione rumore per la modalità di Coerenza",
"L'intensità minima di riduzione del rumore per la regione di coerenza durante l'inpainting o l'outpainting"
"L'intensità minima di riduzione del rumore per la regione di coerenza durante l'inpaint o l'outpaint"
]
},
"compositingMaskBlur": {
@@ -1498,7 +1515,7 @@
"optimizedDenoising": {
"heading": "Immagine-a-immagine ottimizzata",
"paragraphs": [
"Abilita 'Immagine-a-immagine ottimizzata' per una scala di riduzione del rumore più graduale per le trasformazioni da immagine a immagine e di inpainting con modelli Flux. Questa impostazione migliora la capacità di controllare la quantità di modifica applicata a un'immagine, ma può essere disattivata se preferisci usare la scala di riduzione rumore standard. Questa impostazione è ancora in fase di messa a punto ed è in stato beta."
"Abilita 'Immagine-a-immagine ottimizzata' per una scala di riduzione del rumore più graduale per le trasformazioni da immagine a immagine e di inpaint con modelli Flux. Questa impostazione migliora la capacità di controllare la quantità di modifica applicata a un'immagine, ma può essere disattivata se preferisci usare la scala di riduzione rumore standard. Questa impostazione è ancora in fase di messa a punto ed è in stato beta."
]
},
"paramGuidance": {
@@ -1733,8 +1750,7 @@
"newRegionalReferenceImageError": "Problema nella creazione dell'immagine di riferimento regionale",
"newControlLayerOk": "Livello di controllo creato",
"bboxOverlay": "Mostra sovrapposizione riquadro",
"resetCanvas": "Reimposta la tela",
"outputOnlyMaskedRegions": "Solo regioni mascherate in uscita",
"outputOnlyMaskedRegions": "In uscita solo le regioni generate",
"enableAutoNegative": "Abilita Auto Negativo",
"disableAutoNegative": "Disabilita Auto Negativo",
"showHUD": "Mostra HUD",
@@ -1771,7 +1787,7 @@
"globalReferenceImage_withCount_many": "Immagini di riferimento Globali",
"globalReferenceImage_withCount_other": "Immagini di riferimento Globali",
"controlMode": {
"balanced": "Bilanciato",
"balanced": "Bilanciato (consigliato)",
"controlMode": "Modalità di controllo",
"prompt": "Prompt",
"control": "Controllo",
@@ -1782,7 +1798,7 @@
"beginEndStepPercentShort": "Inizio/Fine %",
"stagingOnCanvas": "Genera immagini nella",
"ipAdapterMethod": {
"full": "Completo",
"full": "Stile e Composizione",
"style": "Solo Stile",
"composition": "Solo Composizione",
"ipAdapterMethod": "Metodo Adattatore IP"
@@ -1881,7 +1897,10 @@
"lineart_anime_edge_detection": {
"description": "Genera una mappa dei bordi dal livello selezionato utilizzando il modello di rilevamento dei bordi Lineart Anime.",
"label": "Rilevamento bordi Lineart Anime"
}
},
"forMoreControl": "Per un maggiore controllo, fare clic su Avanzate qui sotto.",
"advanced": "Avanzate",
"processingLayerWith": "Elaborazione del livello con il filtro {{type}}."
},
"controlLayers_withCount_hidden": "Livelli di controllo ({{count}} nascosti)",
"regionalGuidance_withCount_hidden": "Guida regionale ({{count}} nascosti)",
@@ -2016,8 +2035,6 @@
"convertControlLayerTo": "Converti $t(controlLayers.controlLayer) in",
"newRasterLayer": "Nuovo $t(controlLayers.rasterLayer)",
"newRegionalGuidance": "Nuova $t(controlLayers.regionalGuidance)",
"canvasAsRasterLayer": "$t(controlLayers.canvas) come $t(controlLayers.rasterLayer)",
"canvasAsControlLayer": "$t(controlLayers.canvas) come $t(controlLayers.controlLayer)",
"convertInpaintMaskTo": "Converti $t(controlLayers.inpaintMask) in",
"copyRegionalGuidanceTo": "Copia $t(controlLayers.regionalGuidance) in",
"convertRasterLayerTo": "Converti $t(controlLayers.rasterLayer) in",
@@ -2025,7 +2042,9 @@
"newControlLayer": "Nuovo $t(controlLayers.controlLayer)",
"newInpaintMask": "Nuova $t(controlLayers.inpaintMask)",
"replaceCurrent": "Sostituisci corrente",
"mergeDown": "Unire in basso"
"mergeDown": "Unire in basso",
"mergingLayers": "Unione dei livelli",
"controlLayerEmptyState": "<UploadButton>Carica un'immagine</UploadButton>, trascina un'immagine dalla <GalleryButton>galleria</GalleryButton> su questo livello oppure disegna sulla tela per iniziare."
},
"ui": {
"tabs": {
@@ -2057,7 +2076,9 @@
"postProcessingMissingModelWarning": "Visita <LinkComponent>Gestione modelli</LinkComponent> per installare un modello di post-elaborazione (da immagine a immagine).",
"exceedsMaxSize": "Le impostazioni di ampliamento superano il limite massimo delle dimensioni",
"exceedsMaxSizeDetails": "Il limite massimo di ampliamento è {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixel. Prova un'immagine più piccola o diminuisci la scala selezionata.",
"upscale": "Amplia"
"upscale": "Amplia",
"incompatibleBaseModel": "Architettura del modello principale non supportata per l'ampliamento",
"incompatibleBaseModelDesc": "L'ampliamento è supportato solo per i modelli di architettura SD1.5 e SDXL. Cambia il modello principale per abilitare l'ampliamento."
},
"upsell": {
"inviteTeammates": "Invita collaboratori",
@@ -2119,12 +2140,13 @@
},
"whatsNew": {
"whatsNewInInvoke": "Novità in Invoke",
"line2": "Supporto Flux esteso, ora con immagini di riferimento globali",
"line3": "Tooltip e menu contestuali migliorati",
"readReleaseNotes": "Leggi le note di rilascio",
"watchRecentReleaseVideos": "Guarda i video su questa versione",
"line1": "Strumento <ItalicComponent>Seleziona oggetto</ItalicComponent> per la selezione e la modifica precise degli oggetti",
"watchUiUpdatesOverview": "Guarda le novità dell'interfaccia"
"watchUiUpdatesOverview": "Guarda le novità dell'interfaccia",
"items": [
"<StrongComponent>SD 3.5</StrongComponent>: supporto per SD 3.5 Medium e Large.",
"<StrongComponent>Tela</StrongComponent>: elaborazione semplificata del livello di controllo e impostazioni di controllo predefinite migliorate."
]
},
"system": {
"logLevel": {

View File

@@ -16,7 +16,7 @@
"discordLabel": "Discord",
"nodes": "ワークフロー",
"txt2img": "txt2img",
"postprocessing": "Post Processing",
"postprocessing": "ポストプロセス",
"t2iAdapter": "T2I アダプター",
"communityLabel": "コミュニティ",
"dontAskMeAgain": "次回から確認しない",
@@ -71,8 +71,8 @@
"orderBy": "並び順:",
"enabled": "有効",
"notInstalled": "未インストール",
"positivePrompt": "プロンプト",
"negativePrompt": "除外する要素",
"positivePrompt": "ポジティブプロンプト",
"negativePrompt": "ネガティブプロンプト",
"selected": "選択済み",
"aboutDesc": "Invokeを業務で利用する場合はマークしてください:",
"beta": "ベータ",
@@ -80,7 +80,20 @@
"editor": "エディタ",
"safetensors": "Safetensors",
"tab": "タブ",
"toResolve": "解決方法"
"toResolve": "解決方法",
"openInViewer": "ビューアで開く",
"placeholderSelectAModel": "モデルを選択",
"clipboard": "クリップボード",
"apply": "適用",
"loadingImage": "画像をロード中",
"off": "オフ",
"view": "ビュー",
"edit": "編集",
"ok": "OK",
"reset": "リセット",
"none": "なし",
"new": "新規",
"close": "閉じる"
},
"gallery": {
"galleryImageSize": "画像のサイズ",
@@ -125,12 +138,114 @@
"compareHelp1": "<Kbd>Alt</Kbd> キーを押しながらギャラリー画像をクリックするか、矢印キーを使用して比較画像を変更します。",
"compareHelp3": "<Kbd>C</Kbd>を押して、比較した画像を入れ替えます。",
"compareHelp4": "<Kbd>[Z</Kbd>]または<Kbd>[Esc</Kbd>]を押して終了します。",
"compareHelp2": "<Kbd>M</Kbd> キーを押して比較モードを切り替えます。"
"compareHelp2": "<Kbd>M</Kbd> キーを押して比較モードを切り替えます。",
"move": "移動",
"openViewer": "ビューアを開く",
"closeViewer": "ビューアを閉じる",
"exitSearch": "画像検索を終了",
"oldestFirst": "最古から",
"showStarredImagesFirst": "スター付き画像を最初に",
"exitBoardSearch": "ボード検索を終了",
"showArchivedBoards": "アーカイブされたボードを表示",
"searchImages": "メタデータで検索",
"gallery": "ギャラリー",
"newestFirst": "最新から",
"jump": "ジャンプ",
"go": "進む",
"sortDirection": "並び替え順",
"displayBoardSearch": "ボード検索",
"displaySearch": "画像を検索",
"boardsSettings": "ボード設定",
"imagesSettings": "ギャラリー画像設定"
},
"hotkeys": {
"searchHotkeys": "ホットキーを検索",
"clearSearch": "検索をクリア",
"noHotkeysFound": "ホットキーが見つかりません"
"noHotkeysFound": "ホットキーが見つかりません",
"viewer": {
"runPostprocessing": {
"title": "ポストプロセスを実行"
},
"useSize": {
"title": "サイズを使用"
},
"recallPrompts": {
"title": "プロンプトを再使用"
},
"recallAll": {
"title": "全てのメタデータを再使用"
},
"recallSeed": {
"title": "シード値を再使用"
}
},
"canvas": {
"redo": {
"title": "やり直し"
},
"transformSelected": {
"title": "変形"
},
"undo": {
"title": "取り消し"
},
"selectEraserTool": {
"title": "消しゴムツール"
},
"cancelTransform": {
"title": "変形をキャンセル"
},
"resetSelected": {
"title": "レイヤーをリセット"
},
"applyTransform": {
"title": "変形を適用"
},
"selectColorPickerTool": {
"title": "スポイトツール"
},
"fitBboxToCanvas": {
"title": "バウンディングボックスをキャンバスにフィット"
},
"selectBrushTool": {
"title": "ブラシツール"
},
"selectMoveTool": {
"title": "移動ツール"
},
"selectBboxTool": {
"title": "バウンディングボックスツール"
},
"title": "キャンバス",
"fitLayersToCanvas": {
"title": "レイヤーをキャンバスにフィット"
}
},
"workflows": {
"undo": {
"title": "取り消し"
},
"redo": {
"title": "やり直し"
}
},
"app": {
"toggleLeftPanel": {
"title": "左パネルをトグル",
"desc": "左パネルを表示または非表示。"
},
"title": "アプリケーション",
"invoke": {
"title": "Invoke"
},
"cancelQueueItem": {
"title": "キャンセル"
},
"clearQueue": {
"title": "キューをクリア"
}
},
"hotkeys": "ホットキー"
},
"modelManager": {
"modelManager": "モデルマネージャ",
@@ -165,7 +280,7 @@
"convertToDiffusers": "ディフューザーに変換",
"alpha": "アルファ",
"modelConverted": "モデル変換が完了しました",
"predictionType": "予測タイプ(安定したディフュージョン 2.x モデルおよび一部の安定したディフュージョン 1.x モデル用)",
"predictionType": "予測タイプ(SD 2.x モデルおよび一部のSD 1.x モデル用)",
"selectModel": "モデルを選択",
"advanced": "高度な設定",
"modelDeleted": "モデルが削除されました",
@@ -178,7 +293,9 @@
"convertToDiffusersHelpText1": "このモデルは 🧨 Diffusers フォーマットに変換されます。",
"convertToDiffusersHelpText3": "チェックポイントファイルは、InvokeAIルートフォルダ内にある場合、ディスクから削除されます。カスタムロケーションにある場合は、削除されません。",
"convertToDiffusersHelpText4": "これは一回限りのプロセスです。コンピュータの仕様によっては、約30秒から60秒かかる可能性があります。",
"cancel": "キャンセル"
"cancel": "キャンセル",
"uploadImage": "画像をアップロード",
"addModels": "モデルを追加"
},
"parameters": {
"images": "画像",
@@ -200,7 +317,19 @@
"info": "情報",
"showOptionsPanel": "オプションパネルを表示",
"iterations": "生成回数",
"general": "基本設定"
"general": "基本設定",
"setToOptimalSize": "サイズをモデルに最適化",
"invoke": {
"addingImagesTo": "画像の追加先"
},
"aspect": "縦横比",
"lockAspectRatio": "縦横比を固定",
"scheduler": "スケジューラー",
"sendToUpscale": "アップスケーラーに転送",
"useSize": "サイズを使用",
"postProcessing": "ポストプロセス (Shift + U)",
"denoisingStrength": "ノイズ除去強度",
"recallMetadata": "メタデータを再使用"
},
"settings": {
"models": "モデル",
@@ -213,7 +342,11 @@
},
"toast": {
"uploadFailed": "アップロード失敗",
"imageCopied": "画像をコピー"
"imageCopied": "画像をコピー",
"imageUploadFailed": "画像のアップロードに失敗しました",
"uploadFailedInvalidUploadDesc": "画像はPNGかJPGである必要があります。",
"sentToUpscale": "アップスケーラーに転送しました",
"imageUploaded": "画像をアップロードしました"
},
"accessibility": {
"invokeProgressBar": "進捗バー",
@@ -226,7 +359,10 @@
"resetUI": "$t(accessibility.reset) UI",
"mode": "モード:",
"about": "Invoke について",
"submitSupportTicket": "サポート依頼を送信する"
"submitSupportTicket": "サポート依頼を送信する",
"uploadImages": "画像をアップロード",
"toggleLeftPanel": "左パネルをトグル (T)",
"toggleRightPanel": "右パネルをトグル (G)"
},
"metadata": {
"Threshold": "ノイズ閾値",
@@ -237,7 +373,8 @@
"scheduler": "スケジューラー",
"positivePrompt": "ポジティブプロンプト",
"strength": "Image to Image 強度",
"recallParameters": "パラメータを呼び出す"
"recallParameters": "パラメータを再使用",
"recallParameter": "{{label}} を再使用"
},
"queue": {
"queueEmpty": "キューが空です",
@@ -297,14 +434,22 @@
"prune": "刈り込み",
"prompts_other": "プロンプト",
"iterations_other": "繰り返し",
"generations_other": "生成"
"generations_other": "生成",
"canvas": "キャンバス",
"workflows": "ワークフロー",
"upscaling": "アップスケール",
"generation": "生成",
"other": "その他",
"gallery": "ギャラリー"
},
"models": {
"noMatchingModels": "一致するモデルがありません",
"loading": "読み込み中",
"noMatchingLoRAs": "一致するLoRAがありません",
"noModelsAvailable": "使用可能なモデルがありません",
"selectModel": "モデルを選択してください"
"selectModel": "モデルを選択してください",
"concepts": "コンセプト",
"addLora": "LoRAを追加"
},
"nodes": {
"addNode": "ノードを追加",
@@ -339,7 +484,8 @@
"cannotConnectOutputToOutput": "出力から出力には接続できません",
"cannotConnectToSelf": "自身のノードには接続できません",
"colorCodeEdges": "カラー-Code Edges",
"loadingNodes": "ノードを読み込み中..."
"loadingNodes": "ノードを読み込み中...",
"scheduler": "スケジューラー"
},
"boards": {
"autoAddBoard": "自動追加するボード",
@@ -362,7 +508,18 @@
"deleteBoardAndImages": "ボードと画像の削除",
"deleteBoardOnly": "ボードのみ削除",
"deletedBoardsCannotbeRestored": "削除されたボードは復元できません",
"movingImagesToBoard_other": "{{count}} の画像をボードに移動:"
"movingImagesToBoard_other": "{{count}} の画像をボードに移動:",
"hideBoards": "ボードを隠す",
"assetsWithCount_other": "{{count}} のアセット",
"addPrivateBoard": "プライベートボードを追加",
"addSharedBoard": "共有ボードを追加",
"boards": "ボード",
"private": "プライベートボード",
"shared": "共有ボード",
"archiveBoard": "ボードをアーカイブ",
"archived": "アーカイブ完了",
"unarchiveBoard": "アーカイブされていないボード",
"imagesWithCount_other": "{{count}} の画像"
},
"invocationCache": {
"invocationCache": "呼び出しキャッシュ",
@@ -387,6 +544,33 @@
"paragraphs": [
"生成された画像の縦横比。"
]
},
"regionalGuidanceAndReferenceImage": {
"heading": "領域ガイダンスと領域参照画像"
},
"regionalReferenceImage": {
"heading": "領域参照画像"
},
"paramScheduler": {
"heading": "スケジューラー"
},
"regionalGuidance": {
"heading": "領域ガイダンス"
},
"rasterLayer": {
"heading": "ラスターレイヤー"
},
"globalReferenceImage": {
"heading": "全域参照画像"
},
"paramUpscaleMethod": {
"heading": "アップスケール手法"
},
"upscaleModel": {
"heading": "アップスケールモデル"
},
"paramAspect": {
"heading": "縦横比"
}
},
"accordions": {
@@ -427,5 +611,79 @@
"tabs": {
"queue": "キュー"
}
},
"controlLayers": {
"globalReferenceImage_withCount_other": "全域参照画像",
"regionalReferenceImage": "領域参照画像",
"saveLayerToAssets": "レイヤーをアセットに保存",
"global": "全域",
"inpaintMasks_withCount_hidden": "インペイントマスク ({{count}} hidden)",
"opacity": "透明度",
"canvasContextMenu": {
"newRegionalGuidance": "新規領域ガイダンス",
"bboxGroup": "バウンディングボックスから作成",
"cropCanvasToBbox": "キャンバスをバウンディングボックスでクロップ",
"newGlobalReferenceImage": "新規全域参照画像",
"newRegionalReferenceImage": "新規領域参照画像"
},
"regionalGuidance": "領域ガイダンス",
"globalReferenceImage": "全域参照画像",
"moveForward": "前面へ移動",
"copyInpaintMaskTo": "$t(controlLayers.inpaintMask) をコピー",
"transform": {
"fitToBbox": "バウンディングボックスにフィット",
"transform": "変形",
"apply": "適用",
"cancel": "キャンセル",
"reset": "リセット"
},
"cropLayerToBbox": "レイヤーをバウンディングボックスでクロップ",
"convertInpaintMaskTo": "$t(controlLayers.inpaintMask)を変換",
"regionalGuidance_withCount_other": "領域ガイダンス",
"tool": {
"colorPicker": "スポイト",
"brush": "ブラシ",
"rectangle": "矩形",
"move": "移動",
"eraser": "消しゴム"
},
"saveCanvasToGallery": "キャンバスをギャラリーに保存",
"saveBboxToGallery": "バウンディングボックスをギャラリーへ保存",
"moveToBack": "最背面へ移動",
"duplicate": "複製",
"addLayer": "レイヤーを追加",
"rasterLayer": "ラスターレイヤー",
"inpaintMasks_withCount_visible": "({{count}}) インペイントマスク",
"regional": "領域",
"rectangle": "矩形",
"moveBackward": "背面へ移動",
"moveToFront": "最前面へ移動",
"mergeDown": "レイヤーを統合",
"inpaintMask_withCount_other": "インペイントマスク",
"canvas": "キャンバス",
"fitBboxToLayers": "バウンディングボックスをレイヤーにフィット",
"removeBookmark": "ブックマークを外す",
"savedToGalleryOk": "ギャラリーに保存しました"
},
"stylePresets": {
"clearTemplateSelection": "選択したテンプレートをクリア",
"choosePromptTemplate": "プロンプトテンプレートを選択",
"myTemplates": "自分のテンプレート",
"flatten": "選択中のテンプレートをプロンプトに展開",
"uploadImage": "画像をアップロード",
"defaultTemplates": "デフォルトテンプレート",
"createPromptTemplate": "プロンプトテンプレートを作成",
"promptTemplateCleared": "プロンプトテンプレートをクリアしました",
"searchByName": "名前で検索",
"toggleViewMode": "表示モードを切り替え"
},
"upscaling": {
"upscaleModel": "アップスケールモデル",
"postProcessingModel": "ポストプロセスモデル",
"upscale": "アップスケール"
},
"sdxl": {
"denoisingStrength": "ノイズ除去強度",
"scheduler": "スケジューラー"
}
}

View File

@@ -1660,7 +1660,6 @@
"clearCaches": "Очистить кэши",
"recalculateRects": "Пересчитать прямоугольники",
"saveBboxToGallery": "Сохранить рамку в галерею",
"resetCanvas": "Сбросить холст",
"canvas": "Холст",
"global": "Глобальный",
"newGlobalReferenceImageError": "Проблема с созданием глобального эталонного изображения",

File diff suppressed because it is too large Load Diff

View File

@@ -96,7 +96,9 @@
"view": "视图",
"alpha": "透明度通道",
"openInViewer": "在查看器中打开",
"clipboard": "剪贴板"
"clipboard": "剪贴板",
"loadingModel": "加载模型",
"generating": "生成中"
},
"gallery": {
"galleryImageSize": "预览大小",
@@ -587,7 +589,27 @@
"huggingFace": "HuggingFace",
"hfTokenInvalid": "HF 令牌无效或缺失",
"hfTokenLabel": "HuggingFace 令牌(某些模型所需)",
"hfTokenHelperText": "使用某些模型需要 HF 令牌。点击这里创建或获取你的令牌。"
"hfTokenHelperText": "使用某些模型需要 HF 令牌。点击这里创建或获取你的令牌。",
"includesNModels": "包括 {{n}} 个模型及其依赖项",
"starterBundles": "启动器包",
"learnMoreAboutSupportedModels": "了解更多关于我们支持的模型的信息",
"hfForbidden": "您没有权限访问这个 HF 模型",
"hfTokenInvalidErrorMessage": "无效或缺失 HuggingFace 令牌。",
"hfTokenRequired": "您正在尝试下载一个需要有效 HuggingFace 令牌的模型。",
"hfTokenSaved": "HF 令牌已保存",
"hfForbiddenErrorMessage": "我们建议访问 HuggingFace.com 上的仓库页面。所有者可能要求您接受条款才能下载。",
"hfTokenUnableToVerifyErrorMessage": "无法验证 HuggingFace 令牌。这可能是由于网络错误导致的。请稍后再试。",
"hfTokenInvalidErrorMessage2": "在这里更新它。 ",
"hfTokenUnableToVerify": "无法验证 HF 令牌",
"skippingXDuplicates_other": "跳过 {{count}} 个重复项",
"starterBundleHelpText": "轻松安装所有用于启动基础模型所需的模型包括主模型、ControlNets、IP适配器等。选择一个安装包时会跳过已安装的模型。",
"installingBundle": "正在安装模型包",
"installingModel": "正在安装模型",
"installingXModels_other": "正在安装 {{count}} 个模型",
"t5Encoder": "T5 编码器",
"clipLEmbed": "CLIP-L 嵌入",
"clipGEmbed": "CLIP-G 嵌入",
"loraModels": "LoRAs低秩适配"
},
"parameters": {
"images": "图像",
@@ -646,8 +668,23 @@
"controlAdapterIncompatibleBaseModel": "Control Adapter的基础模型不兼容",
"ipAdapterIncompatibleBaseModel": "IP Adapter的基础模型不兼容",
"ipAdapterNoImageSelected": "未选择IP Adapter图像",
"rgNoRegion": "未选择区域"
}
"rgNoRegion": "未选择区域",
"t2iAdapterIncompatibleBboxWidth": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}},边界框宽度为 {{width}}",
"t2iAdapterIncompatibleScaledBboxHeight": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}},缩放后的边界框高度为 {{height}}",
"t2iAdapterIncompatibleBboxHeight": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}},边界框高度为 {{height}}",
"t2iAdapterIncompatibleScaledBboxWidth": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}},缩放后的边界框宽度为 {{width}}"
},
"canvasIsFiltering": "画布正在过滤",
"fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16),缩放后的边界框高度为 {{height}}",
"noCLIPEmbedModelSelected": "未为FLUX生成选择CLIP嵌入模型",
"noFLUXVAEModelSelected": "未为FLUX生成选择VAE模型",
"canvasIsRasterizing": "画布正在栅格化",
"canvasIsCompositing": "画布正在合成",
"fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16),边界框宽度为 {{width}}",
"fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16),缩放后的边界框宽度为 {{width}}",
"noT5EncoderModelSelected": "未为FLUX生成选择T5编码器模型",
"fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16),边界框高度为 {{height}}",
"canvasIsTransforming": "画布正在变换"
},
"patchmatchDownScaleSize": "缩小",
"clipSkip": "CLIP 跳过层",
@@ -669,7 +706,15 @@
"sendToUpscale": "发送到放大",
"processImage": "处理图像",
"infillColorValue": "填充颜色",
"coherenceMinDenoise": "最小去噪"
"coherenceMinDenoise": "最小去噪",
"sendToCanvas": "发送到画布",
"disabledNoRasterContent": "已禁用(无栅格内容)",
"optimizedImageToImage": "优化的图生图",
"guidance": "引导",
"gaussianBlur": "高斯模糊",
"recallMetadata": "调用元数据",
"boxBlur": "方框模糊",
"staged": "已分阶段处理"
},
"settings": {
"models": "模型",
@@ -699,7 +744,12 @@
"enableInformationalPopovers": "启用信息弹窗",
"reloadingIn": "重新加载中",
"informationalPopoversDisabled": "信息提示框已禁用",
"informationalPopoversDisabledDesc": "信息提示框已被禁用.请在设置中重新启用."
"informationalPopoversDisabledDesc": "信息提示框已被禁用.请在设置中重新启用.",
"enableModelDescriptions": "在下拉菜单中启用模型描述",
"confirmOnNewSession": "新会话时确认",
"modelDescriptionsDisabledDesc": "下拉菜单中的模型描述已被禁用。可在设置中启用。",
"modelDescriptionsDisabled": "下拉菜单中的模型描述已禁用",
"showDetailedInvocationProgress": "显示进度详情"
},
"toast": {
"uploadFailed": "上传失败",
@@ -740,7 +790,24 @@
"errorCopied": "错误信息已复制",
"modelImportCanceled": "模型导入已取消",
"importFailed": "导入失败",
"importSuccessful": "导入成功"
"importSuccessful": "导入成功",
"layerSavedToAssets": "图层已保存到资产",
"sentToUpscale": "已发送到放大处理",
"addedToUncategorized": "已添加到看板 $t(boards.uncategorized) 的资产中",
"linkCopied": "链接已复制",
"uploadFailedInvalidUploadDesc_withCount_other": "最多只能上传 {{count}} 张 PNG 或 JPEG 图像。",
"problemSavingLayer": "无法保存图层",
"unableToLoadImage": "无法加载图像",
"imageNotLoadedDesc": "无法找到图像",
"unableToLoadStylePreset": "无法加载样式预设",
"stylePresetLoaded": "样式预设已加载",
"problemCopyingLayer": "无法复制图层",
"sentToCanvas": "已发送到画布",
"unableToLoadImageMetadata": "无法加载图像元数据",
"imageSaved": "图像已保存",
"imageSavingFailed": "图像保存失败",
"layerCopiedToClipboard": "图层已复制到剪贴板",
"imagesWillBeAddedTo": "上传的图像将添加到看板 {{boardName}} 的资产中。"
},
"accessibility": {
"invokeProgressBar": "Invoke 进度条",
@@ -890,7 +957,12 @@
"clearWorkflow": "清除工作流",
"imageAccessError": "无法找到图像 {{image_name}},正在恢复默认设置",
"boardAccessError": "无法找到面板 {{board_id}},正在恢复默认设置",
"modelAccessError": "无法找到模型 {{key}},正在恢复默认设置"
"modelAccessError": "无法找到模型 {{key}},正在恢复默认设置",
"noWorkflows": "无工作流程",
"workflowHelpText": "需要帮助?请查看我们的《<LinkComponent>工作流程入门指南</LinkComponent>》。",
"noMatchingWorkflows": "无匹配的工作流程",
"saveToGallery": "保存到图库",
"singleFieldType": "{{name}}(单一模型)"
},
"queue": {
"status": "状态",
@@ -1221,7 +1293,8 @@
"heading": "去噪强度",
"paragraphs": [
"为输入图像添加的噪声量。",
"输入 0 会导致结果图像和输入完全相同,输入 1 则会生成全新的图像。"
"输入 0 会导致结果图像和输入完全相同,输入 1 则会生成全新的图像。",
"当没有具有可见内容的栅格图层时,此设置将被忽略。"
]
},
"paramSeed": {
@@ -1410,7 +1483,8 @@
"paragraphs": [
"控制提示对生成过程的影响程度.",
"与生成CFG Scale相似."
]
],
"heading": "CFG比例"
},
"structure": {
"heading": "结构",
@@ -1441,6 +1515,62 @@
"paragraphs": [
"比例控制决定了输出图像的大小,它是基于输入图像分辨率的倍数来计算的.例如对一张1024x1024的图像进行2倍上采样将会得到一张2048x2048的输出图像."
]
},
"globalReferenceImage": {
"heading": "全局参考图像",
"paragraphs": [
"应用参考图像以影响整个生成过程。"
]
},
"rasterLayer": {
"paragraphs": [
"画布的基于像素的内容,用于图像生成过程。"
],
"heading": "栅格图层"
},
"regionalGuidanceAndReferenceImage": {
"paragraphs": [
"对于区域引导,使用画笔引导全局提示中的元素应出现的位置。",
"对于区域参考图像,使用画笔将参考图像应用到特定区域。"
],
"heading": "区域引导与区域参考图像"
},
"regionalReferenceImage": {
"heading": "区域参考图像",
"paragraphs": [
"使用画笔将参考图像应用到特定区域。"
]
},
"optimizedDenoising": {
"heading": "优化的图生图",
"paragraphs": [
"启用‘优化的图生图’功能,可在使用 Flux 模型进行图生图和图像修复转换时提供更平滑的降噪强度调节。此设置可以提高对图像变化程度的控制能力,但如果您更倾向于使用标准的降噪强度调节方式,也可以关闭此功能。该设置仍在优化中,目前处于测试阶段。"
]
},
"inpainting": {
"paragraphs": [
"控制由降噪强度引导的修改区域。"
],
"heading": "图像重绘"
},
"regionalGuidance": {
"heading": "区域引导",
"paragraphs": [
"使用画笔引导全局提示中的元素应出现的位置。"
]
},
"fluxDevLicense": {
"heading": "非商业许可",
"paragraphs": [
"FLUX.1 [dev] 模型受 FLUX [dev] 非商业许可协议的约束。如需在 Invoke 中将此模型类型用于商业目的,请访问我们的网站了解更多信息。"
]
},
"paramGuidance": {
"paragraphs": [
"控制提示对生成过程的影响程度。",
"较高的引导值可能导致过度饱和而过高或过低的引导值可能导致生成结果失真。引导仅适用于FLUX DEV模型。"
],
"heading": "引导"
}
},
"invocationCache": {
@@ -1503,7 +1633,18 @@
"convertGraph": "转换图表",
"loadWorkflow": "$t(common.load) 工作流",
"loadFromGraph": "从图表加载工作流",
"autoLayout": "自动布局"
"autoLayout": "自动布局",
"edit": "编辑",
"copyShareLinkForWorkflow": "复制工作流程的分享链接",
"delete": "删除",
"download": "下载",
"defaultWorkflows": "默认工作流程",
"userWorkflows": "用户工作流程",
"projectWorkflows": "项目工作流程",
"copyShareLink": "复制分享链接",
"chooseWorkflowFromLibrary": "从库中选择工作流程",
"uploadAndSaveWorkflow": "上传到库",
"deleteWorkflow2": "您确定要删除此工作流程吗?此操作无法撤销。"
},
"accordions": {
"compositing": {
@@ -1542,7 +1683,108 @@
"addPositivePrompt": "添加 $t(controlLayers.prompt)",
"addNegativePrompt": "添加 $t(controlLayers.negativePrompt)",
"rectangle": "矩形",
"opacity": "透明度"
"opacity": "透明度",
"canvas": "画布",
"fitBboxToLayers": "将边界框适配到图层",
"cropLayerToBbox": "将图层裁剪到边界框",
"saveBboxToGallery": "将边界框保存到图库",
"savedToGalleryOk": "已保存到图库",
"saveLayerToAssets": "将图层保存到资产",
"removeBookmark": "移除书签",
"regional": "区域",
"saveCanvasToGallery": "将画布保存到图库",
"global": "全局",
"bookmark": "添加书签以快速切换",
"regionalReferenceImage": "局部参考图像",
"mergingLayers": "正在合并图层",
"newControlLayerError": "创建控制层时出现问题",
"pullBboxIntoReferenceImageError": "将边界框导入参考图像时出现问题",
"mergeVisibleOk": "已合并图层",
"maskFill": "遮罩填充",
"newCanvasFromImage": "从图像创建新画布",
"pullBboxIntoReferenceImageOk": "边界框已导入到参考图像",
"globalReferenceImage_withCount_other": "全局参考图像",
"addInpaintMask": "添加 $t(controlLayers.inpaintMask)",
"referenceImage": "参考图像",
"globalReferenceImage": "全局参考图像",
"newRegionalGuidance": "新建 $t(controlLayers.regionalGuidance)",
"savedToGalleryError": "保存到图库时出错",
"copyRasterLayerTo": "复制 $t(controlLayers.rasterLayer) 到",
"clearHistory": "清除历史记录",
"inpaintMask": "修复遮罩",
"regionalGuidance_withCount_visible": "区域引导({{count}} 个)",
"inpaintMasks_withCount_hidden": "修复遮罩({{count}} 个已隐藏)",
"enableAutoNegative": "启用自动负面提示",
"disableAutoNegative": "禁用自动负面提示",
"deleteReferenceImage": "删除参考图像",
"sendToCanvas": "发送到画布",
"controlLayers_withCount_visible": "控制图层({{count}} 个)",
"rasterLayers_withCount_visible": "栅格图层({{count}} 个)",
"convertRegionalGuidanceTo": "将 $t(controlLayers.regionalGuidance) 转换为",
"newInpaintMask": "新建 $t(controlLayers.inpaintMask)",
"regionIsEmpty": "选定区域为空",
"mergeVisible": "合并可见图层",
"showHUD": "显示 HUD抬头显示",
"newLayerFromImage": "从图像创建新图层",
"layer_other": "图层",
"transparency": "透明度",
"addRasterLayer": "添加 $t(controlLayers.rasterLayer)",
"newRasterLayerOk": "已创建栅格层",
"newRasterLayerError": "创建栅格层时出现问题",
"inpaintMasks_withCount_visible": "修复遮罩({{count}} 个)",
"convertRasterLayerTo": "将 $t(controlLayers.rasterLayer) 转换为",
"copyControlLayerTo": "复制 $t(controlLayers.controlLayer) 到",
"copyInpaintMaskTo": "复制 $t(controlLayers.inpaintMask) 到",
"copyRegionalGuidanceTo": "复制 $t(controlLayers.regionalGuidance) 到",
"newRasterLayer": "新建 $t(controlLayers.rasterLayer)",
"newControlLayer": "新建 $t(controlLayers.controlLayer)",
"newImg2ImgCanvasFromImage": "从图像创建新的图生图",
"rasterLayer": "栅格层",
"controlLayer": "控制层",
"outputOnlyMaskedRegions": "仅输出生成的区域",
"addControlLayer": "添加 $t(controlLayers.controlLayer)",
"newGlobalReferenceImageOk": "已创建全局参考图像",
"newGlobalReferenceImageError": "创建全局参考图像时出现问题",
"newRegionalReferenceImageOk": "已创建局部参考图像",
"newControlLayerOk": "已创建控制层",
"mergeVisibleError": "合并图层时出错",
"bboxOverlay": "显示边界框覆盖层",
"clipToBbox": "将Clip限制到边界框",
"width": "宽度",
"addGlobalReferenceImage": "添加 $t(controlLayers.globalReferenceImage)",
"inpaintMask_withCount_other": "修复遮罩",
"regionalGuidance_withCount_other": "区域引导",
"newRegionalReferenceImageError": "创建局部参考图像时出现问题",
"pullBboxIntoLayerError": "将边界框导入图层时出现问题",
"pullBboxIntoLayerOk": "边界框已导入到图层",
"sendToCanvasDesc": "按下“Invoke”按钮会将您的工作进度暂存到画布上。",
"sendToGallery": "发送到图库",
"sendToGalleryDesc": "按下“Invoke”键会生成并保存一张唯一的图像到您的图库中。",
"rasterLayer_withCount_other": "栅格图层",
"mergeDown": "向下合并",
"clearCaches": "清除缓存",
"recalculateRects": "重新计算矩形",
"duplicate": "复制",
"regionalGuidance_withCount_hidden": "区域引导({{count}} 个已隐藏)",
"convertControlLayerTo": "将 $t(controlLayers.controlLayer) 转换为",
"convertInpaintMaskTo": "将 $t(controlLayers.inpaintMask) 转换为",
"viewProgressInViewer": "在 <Btn>图像查看器</Btn> 中查看进度和输出结果。",
"viewProgressOnCanvas": "在 <Btn>画布</Btn> 上查看进度和暂存的输出内容。",
"sendingToGallery": "将生成内容发送到图库",
"copyToClipboard": "复制到剪贴板",
"controlLayer_withCount_other": "控制图层",
"sendingToCanvas": "在画布上准备生成",
"addReferenceImage": "添加 $t(controlLayers.referenceImage)",
"addRegionalGuidance": "添加 $t(controlLayers.regionalGuidance)",
"controlLayers_withCount_hidden": "控制图层({{count}} 个已隐藏)",
"rasterLayers_withCount_hidden": "栅格图层({{count}} 个已隐藏)",
"globalReferenceImages_withCount_hidden": "全局参考图像({{count}} 个已隐藏)",
"globalReferenceImages_withCount_visible": "全局参考图像({{count}} 个)",
"layer_withCount_other": "图层({{count}} 个)",
"enableTransparencyEffect": "启用透明效果",
"disableTransparencyEffect": "禁用透明效果",
"hidingType": "隐藏 {{type}}",
"showingType": "显示 {{type}}"
},
"ui": {
"tabs": {

View File

@@ -27,6 +27,7 @@ import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/Cl
import { DeleteStylePresetDialog } from 'features/stylePresets/components/DeleteStylePresetDialog';
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal';
import { VideosModal } from 'features/system/components/VideosModal/VideosModal';
import { configChanged } from 'features/system/store/configSlice';
import { selectLanguage } from 'features/system/store/systemSelectors';
import { AppContent } from 'features/ui/components/AppContent';
@@ -108,6 +109,7 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
<NewCanvasSessionDialog />
<ImageContextMenu />
<FullscreenDropzone />
<VideosModal />
</ErrorBoundary>
);
};

View File

@@ -1,3 +1,4 @@
import { useStore } from '@nanostores/react';
import { useAppStore } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { withResultAsync } from 'common/util/result';
@@ -9,6 +10,7 @@ import { imageDTOToImageObject } from 'features/controlLayers/store/util';
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { sentImageToCanvas } from 'features/gallery/store/actions';
import { parseAndRecallAllMetadata } from 'features/metadata/util/handlers';
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
import { $isWorkflowListMenuIsOpen } from 'features/nodes/store/workflowListMenu';
import { $isStylePresetsMenuOpen, activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
import { toast } from 'features/toast/toast';
@@ -51,6 +53,7 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
const { t } = useTranslation();
// Use a ref to ensure that we only perform the action once
const didInit = useRef(false);
const didParseOpenAPISchema = useStore($hasTemplates);
const store = useAppStore();
const { getAndLoadWorkflow } = useGetAndLoadLibraryWorkflow();
@@ -174,7 +177,7 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
);
useEffect(() => {
if (didInit.current || !action) {
if (didInit.current || !action || !didParseOpenAPISchema) {
return;
}
@@ -187,22 +190,29 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
case 'selectStylePreset':
handleSelectStylePreset(action.data.stylePresetId);
break;
case 'sendToCanvas':
handleSendToCanvas(action.data.imageName);
break;
case 'useAllParameters':
handleUseAllMetadata(action.data.imageName);
break;
case 'goToDestination':
handleGoToDestination(action.data.destination);
break;
default:
break;
}
}, [
handleSendToCanvas,
handleUseAllMetadata,
action,
handleLoadWorkflow,
handleSelectStylePreset,
handleGoToDestination,
handleLoadWorkflow,
didParseOpenAPISchema,
]);
};

View File

@@ -4,7 +4,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
import { buildAdHocPostProcessingGraph } from 'features/nodes/util/graph/buildAdHocPostProcessingGraph';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { queueApi } from 'services/api/endpoints/queue';
import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue';
import type { BatchConfig, ImageDTO } from 'services/api/types';
import type { JsonObject } from 'type-fest';
@@ -32,9 +32,7 @@ export const addAdHocPostProcessingRequestedListener = (startAppListening: AppSt
try {
const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, {
fixedCacheKey: 'enqueueBatch',
})
queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, enqueueMutationFixedCacheKeyOptions)
);
const enqueueResult = await req.unwrap();

View File

@@ -13,7 +13,7 @@ import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGr
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import { toast } from 'features/toast/toast';
import { serializeError } from 'serialize-error';
import { queueApi } from 'services/api/endpoints/queue';
import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue';
import type { Invocation } from 'services/api/types';
import { assert, AssertionError } from 'tsafe';
import type { JsonObject } from 'type-fest';
@@ -91,9 +91,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
}
const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(prepareBatchResult.value, {
fixedCacheKey: 'enqueueBatch',
})
queueApi.endpoints.enqueueBatch.initiate(prepareBatchResult.value, enqueueMutationFixedCacheKeyOptions)
);
req.reset();

View File

@@ -1,10 +1,15 @@
import { logger } from 'app/logging/logger';
import { enqueueRequested } from 'app/store/actions';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { isImageFieldCollectionInputInstance } from 'features/nodes/types/field';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { buildNodesGraph } from 'features/nodes/util/graph/buildNodesGraph';
import { buildWorkflowWithValidation } from 'features/nodes/util/workflow/buildWorkflow';
import { queueApi } from 'services/api/endpoints/queue';
import type { BatchConfig } from 'services/api/types';
import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue';
import type { Batch, BatchConfig } from 'services/api/types';
const log = logger('workflows');
export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) => {
startAppListening({
@@ -26,6 +31,33 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) =
delete builtWorkflow.id;
}
const data: Batch['data'] = [];
// Skip edges from batch nodes - these should not be in the graph, they exist only in the UI
const imageBatchNodes = nodes.nodes.filter(isInvocationNode).filter((node) => node.data.type === 'image_batch');
for (const node of imageBatchNodes) {
const images = node.data.inputs['images'];
if (!isImageFieldCollectionInputInstance(images)) {
log.warn({ nodeId: node.id }, 'Image batch images field is not an image collection');
break;
}
const edgesFromImageBatch = nodes.edges.filter((e) => e.source === node.id && e.sourceHandle === 'image');
const batchDataCollectionItem: NonNullable<Batch['data']>[number] = [];
for (const edge of edgesFromImageBatch) {
if (!edge.targetHandle) {
break;
}
batchDataCollectionItem.push({
node_path: edge.target,
field_name: edge.targetHandle,
items: images.value,
});
}
if (batchDataCollectionItem.length > 0) {
data.push(batchDataCollectionItem);
}
}
const batchConfig: BatchConfig = {
batch: {
graph,
@@ -33,15 +65,12 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) =
runs: state.params.iterations,
origin: 'workflows',
destination: 'gallery',
data,
},
prepend: action.payload.prepend,
};
const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {
fixedCacheKey: 'enqueueBatch',
})
);
const req = dispatch(queueApi.endpoints.enqueueBatch.initiate(batchConfig, enqueueMutationFixedCacheKeyOptions));
try {
await req.unwrap();
} finally {

View File

@@ -2,7 +2,7 @@ import { enqueueRequested } from 'app/store/actions';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildMultidiffusionUpscaleGraph } from 'features/nodes/util/graph/buildMultidiffusionUpscaleGraph';
import { queueApi } from 'services/api/endpoints/queue';
import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue';
export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening) => {
startAppListening({
@@ -16,11 +16,7 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening)
const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond, 'upscaling', 'gallery');
const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {
fixedCacheKey: 'enqueueBatch',
})
);
const req = dispatch(queueApi.endpoints.enqueueBatch.initiate(batchConfig, enqueueMutationFixedCacheKeyOptions));
try {
await req.unwrap();
} finally {

View File

@@ -25,9 +25,7 @@ export type AppFeature =
| 'invocationCache'
| 'bulkDownload'
| 'starterModels'
| 'hfToken'
| 'invocationProgressAlert';
| 'hfToken';
/**
* A disable-able Stable Diffusion feature
*/

View File

@@ -2,6 +2,7 @@ import { deepClone } from 'common/util/deepClone';
import { merge } from 'lodash-es';
import { ClickScrollPlugin, OverlayScrollbars } from 'overlayscrollbars';
import type { UseOverlayScrollbarsParams } from 'overlayscrollbars-react';
import type { CSSProperties } from 'react';
OverlayScrollbars.plugin(ClickScrollPlugin);
@@ -27,3 +28,8 @@ export const getOverlayScrollbarsParams = (
merge(params, { options: { overflow: { y: overflowY, x: overflowX } } });
return params;
};
export const overlayScrollbarsStyles: CSSProperties = {
height: '100%',
width: '100%',
};

View File

@@ -0,0 +1,42 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import {
useNewCanvasSession,
useNewGallerySession,
} from 'features/controlLayers/components/NewSessionConfirmationAlertDialog';
import { canvasReset } from 'features/controlLayers/store/actions';
import { paramsReset } from 'features/controlLayers/store/paramsSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowsCounterClockwiseBold, PiFilePlusBold } from 'react-icons/pi';
export const SessionMenuItems = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { newGallerySessionWithDialog } = useNewGallerySession();
const { newCanvasSessionWithDialog } = useNewCanvasSession();
const resetCanvasLayers = useCallback(() => {
dispatch(canvasReset());
}, [dispatch]);
const resetGenerationSettings = useCallback(() => {
dispatch(paramsReset());
}, [dispatch]);
return (
<>
<MenuItem icon={<PiFilePlusBold />} onClick={newGallerySessionWithDialog}>
{t('controlLayers.newGallerySession')}
</MenuItem>
<MenuItem icon={<PiFilePlusBold />} onClick={newCanvasSessionWithDialog}>
{t('controlLayers.newCanvasSession')}
</MenuItem>
<MenuItem icon={<PiArrowsCounterClockwiseBold />} onClick={resetCanvasLayers}>
{t('controlLayers.resetCanvasLayers')}
</MenuItem>
<MenuItem icon={<PiArrowsCounterClockwiseBold />} onClick={resetGenerationSettings}>
{t('controlLayers.resetGenerationSettings')}
</MenuItem>
</>
);
});
SessionMenuItems.displayName = 'SessionMenuItems';

View File

@@ -46,7 +46,7 @@ const REGION_TARGETS: Record<FocusRegionName, Set<HTMLElement>> = {
/**
* The currently-focused region or `null` if no region is focused.
*/
const $focusedRegion = atom<FocusRegionName | null>(null);
export const $focusedRegion = atom<FocusRegionName | null>(null);
/**
* A map of focus regions to atoms that indicate if that region is focused.

View File

@@ -141,11 +141,9 @@ export const useImageUploadButton = ({ onUpload, isDisabled, allowMultiple }: Us
};
const sx = {
borderColor: 'error.500',
borderStyle: 'solid',
borderWidth: 0,
borderRadius: 'base',
'&[data-error=true]': {
borderColor: 'error.500',
borderStyle: 'solid',
borderWidth: 1,
},
} satisfies SystemStyleObject;
@@ -164,7 +162,34 @@ export const UploadImageButton = ({
<>
<IconButton
aria-label="Upload image"
variant="ghost"
variant="outline"
sx={sx}
data-error={isError}
icon={<PiUploadBold />}
isLoading={uploadApi.request.isLoading}
{...rest}
{...uploadApi.getUploadButtonProps()}
/>
<input {...uploadApi.getUploadInputProps()} />
</>
);
};
export const UploadMultipleImageButton = ({
isDisabled = false,
onUpload,
isError = false,
...rest
}: {
onUpload?: (imageDTOs: ImageDTO[]) => void;
isError?: boolean;
} & SetOptional<IconButtonProps, 'aria-label'>) => {
const uploadApi = useImageUploadButton({ isDisabled, allowMultiple: true, onUpload });
return (
<>
<IconButton
aria-label="Upload image"
variant="outline"
sx={sx}
data-error={isError}
icon={<PiUploadBold />}

View File

@@ -1,331 +0,0 @@
import { useStore } from '@nanostores/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { $true } from 'app/store/nanostores/util';
import { useAppSelector } from 'app/store/storeHooks';
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import { $templates } from 'features/nodes/store/nodesSlice';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import type { Templates } from 'features/nodes/store/types';
import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
import { selectConfigSlice } from 'features/system/store/configSlice';
import { selectSystemSlice } from 'features/system/store/systemSlice';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import i18n from 'i18next';
import { forEach, upperFirst } from 'lodash-es';
import { useMemo } from 'react';
import { getConnectedEdges } from 'reactflow';
import { $isConnected } from 'services/events/stores';
const LAYER_TYPE_TO_TKEY = {
reference_image: 'controlLayers.referenceImage',
inpaint_mask: 'controlLayers.inpaintMask',
regional_guidance: 'controlLayers.regionalGuidance',
raster_layer: 'controlLayers.rasterLayer',
control_layer: 'controlLayers.controlLayer',
} as const;
const createSelector = (
templates: Templates,
isConnected: boolean,
canvasIsFiltering: boolean,
canvasIsTransforming: boolean,
canvasIsRasterizing: boolean,
canvasIsCompositing: boolean
) =>
createMemoizedSelector(
[
selectSystemSlice,
selectNodesSlice,
selectWorkflowSettingsSlice,
selectDynamicPromptsSlice,
selectCanvasSlice,
selectParamsSlice,
selectUpscaleSlice,
selectConfigSlice,
selectActiveTab,
],
(system, nodes, workflowSettings, dynamicPrompts, canvas, params, upscale, config, activeTabName) => {
const { bbox } = canvas;
const { model, positivePrompt } = params;
const reasons: { prefix?: string; content: string }[] = [];
// Cannot generate if not connected
if (!isConnected) {
reasons.push({ content: i18n.t('parameters.invoke.systemDisconnected') });
}
if (activeTabName === 'workflows') {
if (workflowSettings.shouldValidateGraph) {
if (!nodes.nodes.length) {
reasons.push({ content: i18n.t('parameters.invoke.noNodesInGraph') });
}
nodes.nodes.forEach((node) => {
if (!isInvocationNode(node)) {
return;
}
const nodeTemplate = templates[node.data.type];
if (!nodeTemplate) {
// Node type not found
reasons.push({ content: i18n.t('parameters.invoke.missingNodeTemplate') });
return;
}
const connectedEdges = getConnectedEdges([node], nodes.edges);
forEach(node.data.inputs, (field) => {
const fieldTemplate = nodeTemplate.inputs[field.name];
const hasConnection = connectedEdges.some(
(edge) => edge.target === node.id && edge.targetHandle === field.name
);
if (!fieldTemplate) {
reasons.push({ content: i18n.t('parameters.invoke.missingFieldTemplate') });
return;
}
if (fieldTemplate.required && field.value === undefined && !hasConnection) {
reasons.push({
content: i18n.t('parameters.invoke.missingInputForField', {
nodeLabel: node.data.label || nodeTemplate.title,
fieldLabel: field.label || fieldTemplate.title,
}),
});
return;
}
});
});
}
} else if (activeTabName === 'upscaling') {
if (!upscale.upscaleInitialImage) {
reasons.push({ content: i18n.t('upscaling.missingUpscaleInitialImage') });
} else if (config.maxUpscaleDimension) {
const { width, height } = upscale.upscaleInitialImage;
const { scale } = upscale;
const maxPixels = config.maxUpscaleDimension ** 2;
const upscaledPixels = width * scale * height * scale;
if (upscaledPixels > maxPixels) {
reasons.push({ content: i18n.t('upscaling.exceedsMaxSize') });
}
}
if (model && !['sd-1', 'sdxl'].includes(model.base)) {
// When we are using an upsupported model, do not add the other warnings
reasons.push({ content: i18n.t('upscaling.incompatibleBaseModel') });
} else {
// Using a compatible model, add all warnings
if (!model) {
reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') });
}
if (!upscale.upscaleModel) {
reasons.push({ content: i18n.t('upscaling.missingUpscaleModel') });
}
if (!upscale.tileControlnetModel) {
reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') });
}
}
} else {
if (canvasIsFiltering) {
reasons.push({ content: i18n.t('parameters.invoke.canvasIsFiltering') });
}
if (canvasIsTransforming) {
reasons.push({ content: i18n.t('parameters.invoke.canvasIsTransforming') });
}
if (canvasIsRasterizing) {
reasons.push({ content: i18n.t('parameters.invoke.canvasIsRasterizing') });
}
if (canvasIsCompositing) {
reasons.push({ content: i18n.t('parameters.invoke.canvasIsCompositing') });
}
if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) {
reasons.push({ content: i18n.t('parameters.invoke.noPrompts') });
}
if (!model) {
reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') });
}
if (model?.base === 'flux') {
if (!params.t5EncoderModel) {
reasons.push({ content: i18n.t('parameters.invoke.noT5EncoderModelSelected') });
}
if (!params.clipEmbedModel) {
reasons.push({ content: i18n.t('parameters.invoke.noCLIPEmbedModelSelected') });
}
if (!params.fluxVAE) {
reasons.push({ content: i18n.t('parameters.invoke.noFLUXVAEModelSelected') });
}
if (bbox.scaleMethod === 'none') {
if (bbox.rect.width % 16 !== 0) {
reasons.push({
content: i18n.t('parameters.invoke.fluxModelIncompatibleBboxWidth', { width: bbox.rect.width }),
});
}
if (bbox.rect.height % 16 !== 0) {
reasons.push({
content: i18n.t('parameters.invoke.fluxModelIncompatibleBboxHeight', { height: bbox.rect.height }),
});
}
} else {
if (bbox.scaledSize.width % 16 !== 0) {
reasons.push({
content: i18n.t('parameters.invoke.fluxModelIncompatibleScaledBboxWidth', {
width: bbox.scaledSize.width,
}),
});
}
if (bbox.scaledSize.height % 16 !== 0) {
reasons.push({
content: i18n.t('parameters.invoke.fluxModelIncompatibleScaledBboxHeight', {
height: bbox.scaledSize.height,
}),
});
}
}
}
canvas.controlLayers.entities
.filter((controlLayer) => controlLayer.isEnabled)
.forEach((controlLayer, i) => {
const layerLiteral = i18n.t('controlLayers.layer_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY['control_layer']);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = [];
// Must have model
if (!controlLayer.controlAdapter.model) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected'));
}
// Model base must match
if (controlLayer.controlAdapter.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel'));
}
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
canvas.referenceImages.entities
.filter((entity) => entity.isEnabled)
.forEach((entity, i) => {
const layerLiteral = i18n.t('controlLayers.layer_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = [];
// Must have model
if (!entity.ipAdapter.model) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
}
// Model base must match
if (entity.ipAdapter.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
}
// Must have an image
if (!entity.ipAdapter.image) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
}
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
canvas.regionalGuidance.entities
.filter((entity) => entity.isEnabled)
.forEach((entity, i) => {
const layerLiteral = i18n.t('controlLayers.layer_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = [];
// Must have a region
if (entity.objects.length === 0) {
problems.push(i18n.t('parameters.invoke.layer.rgNoRegion'));
}
// Must have at least 1 prompt or IP Adapter
if (
entity.positivePrompt === null &&
entity.negativePrompt === null &&
entity.referenceImages.length === 0
) {
problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters'));
}
entity.referenceImages.forEach(({ ipAdapter }) => {
// Must have model
if (!ipAdapter.model) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
}
// Model base must match
if (ipAdapter.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
}
// Must have an image
if (!ipAdapter.image) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
}
});
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
canvas.rasterLayers.entities
.filter((entity) => entity.isEnabled)
.forEach((entity, i) => {
const layerLiteral = i18n.t('controlLayers.layer_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = [];
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
}
return { isReady: !reasons.length, reasons };
}
);
export const useIsReadyToEnqueue = () => {
const templates = useStore($templates);
const isConnected = useStore($isConnected);
const canvasManager = useCanvasManagerSafe();
const canvasIsFiltering = useStore(canvasManager?.stateApi.$isFiltering ?? $true);
const canvasIsTransforming = useStore(canvasManager?.stateApi.$isTransforming ?? $true);
const canvasIsRasterizing = useStore(canvasManager?.stateApi.$isRasterizing ?? $true);
const canvasIsCompositing = useStore(canvasManager?.compositor.$isBusy ?? $true);
const selector = useMemo(
() =>
createSelector(
templates,
isConnected,
canvasIsFiltering,
canvasIsTransforming,
canvasIsRasterizing,
canvasIsCompositing
),
[templates, isConnected, canvasIsFiltering, canvasIsTransforming, canvasIsRasterizing, canvasIsCompositing]
);
const value = useAppSelector(selector);
return value;
};

View File

@@ -1,13 +1,14 @@
import { Alert, AlertDescription, AlertIcon, AlertTitle } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useDeferredModelLoadingInvocationProgressMessage } from 'features/controlLayers/hooks/useDeferredModelLoadingInvocationProgressMessage';
import { selectIsLocal } from 'features/system/store/configSlice';
import { selectSystemShouldShowInvocationProgressDetail } from 'features/system/store/systemSlice';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { $invocationProgressMessage } from 'services/events/stores';
const CanvasAlertsInvocationProgressContent = memo(() => {
const CanvasAlertsInvocationProgressContentLocal = memo(() => {
const { t } = useTranslation();
const invocationProgressMessage = useStore($invocationProgressMessage);
@@ -23,23 +24,38 @@ const CanvasAlertsInvocationProgressContent = memo(() => {
</Alert>
);
});
CanvasAlertsInvocationProgressContent.displayName = 'CanvasAlertsInvocationProgressContent';
CanvasAlertsInvocationProgressContentLocal.displayName = 'CanvasAlertsInvocationProgressContentLocal';
export const CanvasAlertsInvocationProgress = memo(() => {
const isProgressMessageAlertEnabled = useFeatureStatus('invocationProgressAlert');
const shouldShowInvocationProgressDetail = useAppSelector(selectSystemShouldShowInvocationProgressDetail);
const CanvasAlertsInvocationProgressContentCommercial = memo(() => {
const message = useDeferredModelLoadingInvocationProgressMessage();
// The alert is disabled at the system level
if (!isProgressMessageAlertEnabled) {
if (!message) {
return null;
}
// The alert is disabled at the user level
return (
<Alert status="loading" borderRadius="base" fontSize="sm" shadow="md" w="fit-content">
<AlertIcon />
<AlertDescription>{message}</AlertDescription>
</Alert>
);
});
CanvasAlertsInvocationProgressContentCommercial.displayName = 'CanvasAlertsInvocationProgressContentCommercial';
export const CanvasAlertsInvocationProgress = memo(() => {
const shouldShowInvocationProgressDetail = useAppSelector(selectSystemShouldShowInvocationProgressDetail);
const isLocal = useAppSelector(selectIsLocal);
if (!isLocal) {
return <CanvasAlertsInvocationProgressContentCommercial />;
}
// OSS user setting
if (!shouldShowInvocationProgressDetail) {
return null;
}
return <CanvasAlertsInvocationProgressContent />;
return <CanvasAlertsInvocationProgressContentLocal />;
});
CanvasAlertsInvocationProgress.displayName = 'CanvasAlertsInvocationProgress';

View File

@@ -3,6 +3,7 @@ import { Alert, AlertIcon, AlertTitle } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate';
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useEntityTitle } from 'features/controlLayers/hooks/useEntityTitle';
import { useEntityTypeIsHidden } from 'features/controlLayers/hooks/useEntityTypeIsHidden';
@@ -29,17 +30,23 @@ type AlertData = {
title: string;
};
const buildSelectIsEnabled = (entityIdentifier: CanvasEntityIdentifier) =>
createSelector(
selectCanvasSlice,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'CanvasAlertsSelectedEntityStatusContent').isEnabled
);
const buildSelectIsLocked = (entityIdentifier: CanvasEntityIdentifier) =>
createSelector(
selectCanvasSlice,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'CanvasAlertsSelectedEntityStatusContent').isLocked
);
const CanvasAlertsSelectedEntityStatusContent = memo(({ entityIdentifier, adapter }: ContentProps) => {
const { t } = useTranslation();
const title = useEntityTitle(entityIdentifier);
const selectIsEnabled = useMemo(
() => createSelector(selectCanvasSlice, (canvas) => selectEntityOrThrow(canvas, entityIdentifier).isEnabled),
[entityIdentifier]
);
const selectIsLocked = useMemo(
() => createSelector(selectCanvasSlice, (canvas) => selectEntityOrThrow(canvas, entityIdentifier).isLocked),
[entityIdentifier]
);
const selectIsEnabled = useMemo(() => buildSelectIsEnabled(entityIdentifier), [entityIdentifier]);
const selectIsLocked = useMemo(() => buildSelectIsLocked(entityIdentifier), [entityIdentifier]);
const isEnabled = useAppSelector(selectIsEnabled);
const isLocked = useAppSelector(selectIsLocked);
const isHidden = useEntityTypeIsHidden(entityIdentifier.type);
@@ -115,7 +122,11 @@ export const CanvasAlertsSelectedEntityStatus = memo(() => {
return null;
}
return <CanvasAlertsSelectedEntityStatusContent entityIdentifier={selectedEntityIdentifier} adapter={adapter} />;
return (
<CanvasEntityStateGate entityIdentifier={selectedEntityIdentifier}>
<CanvasAlertsSelectedEntityStatusContent entityIdentifier={selectedEntityIdentifier} adapter={adapter} />
</CanvasEntityStateGate>
);
});
CanvasAlertsSelectedEntityStatus.displayName = 'CanvasAlertsSelectedEntityStatus';

View File

@@ -5,6 +5,7 @@ import { InpaintMaskMenuItems } from 'features/controlLayers/components/InpaintM
import { IPAdapterMenuItems } from 'features/controlLayers/components/IPAdapter/IPAdapterMenuItems';
import { RasterLayerMenuItems } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItems';
import { RegionalGuidanceMenuItems } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems';
import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate';
import {
EntityIdentifierContext,
useEntityIdentifierContext,
@@ -40,6 +41,15 @@ const CanvasContextMenuSelectedEntityMenuItemsContent = memo(() => {
CanvasContextMenuSelectedEntityMenuItemsContent.displayName = 'CanvasContextMenuSelectedEntityMenuItemsContent';
const CanvasContextMenuSelectedEntityMenuGroup = memo((props: PropsWithChildren) => {
const entityIdentifier = useEntityIdentifierContext();
const title = useEntityTypeString(entityIdentifier.type);
return <MenuGroup title={title}>{props.children}</MenuGroup>;
});
CanvasContextMenuSelectedEntityMenuGroup.displayName = 'CanvasContextMenuSelectedEntityMenuGroup';
export const CanvasContextMenuSelectedEntityMenuItems = memo(() => {
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
@@ -49,20 +59,13 @@ export const CanvasContextMenuSelectedEntityMenuItems = memo(() => {
return (
<EntityIdentifierContext.Provider value={selectedEntityIdentifier}>
<CanvasContextMenuSelectedEntityMenuGroup>
<CanvasContextMenuSelectedEntityMenuItemsContent />
</CanvasContextMenuSelectedEntityMenuGroup>
<CanvasEntityStateGate entityIdentifier={selectedEntityIdentifier}>
<CanvasContextMenuSelectedEntityMenuGroup>
<CanvasContextMenuSelectedEntityMenuItemsContent />
</CanvasContextMenuSelectedEntityMenuGroup>
</CanvasEntityStateGate>
</EntityIdentifierContext.Provider>
);
});
CanvasContextMenuSelectedEntityMenuItems.displayName = 'CanvasContextMenuSelectedEntityMenuItems';
const CanvasContextMenuSelectedEntityMenuGroup = memo((props: PropsWithChildren) => {
const entityIdentifier = useEntityIdentifierContext();
const title = useEntityTypeString(entityIdentifier.type);
return <MenuGroup title={title}>{props.children}</MenuGroup>;
});
CanvasContextMenuSelectedEntityMenuGroup.displayName = 'CanvasContextMenuSelectedEntityMenuGroup';

View File

@@ -7,6 +7,7 @@ import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/c
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
import { ControlLayerBadges } from 'features/controlLayers/components/ControlLayer/ControlLayerBadges';
import { ControlLayerSettings } from 'features/controlLayers/components/ControlLayer/ControlLayerSettings';
import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate';
import { ControlLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
@@ -36,24 +37,26 @@ export const ControlLayer = memo(({ id }: Props) => {
return (
<EntityIdentifierContext.Provider value={entityIdentifier}>
<ControlLayerAdapterGate>
<CanvasEntityContainer>
<CanvasEntityHeader>
<CanvasEntityPreviewImage />
<CanvasEntityEditableTitle />
<Spacer />
<ControlLayerBadges />
<CanvasEntityHeaderCommonActions />
</CanvasEntityHeader>
<CanvasEntitySettingsWrapper>
<ControlLayerSettings />
</CanvasEntitySettingsWrapper>
<DndDropTarget
dndTarget={replaceCanvasEntityObjectsWithImageDndTarget}
dndTargetData={dndTargetData}
label={t('controlLayers.replaceLayer')}
isDisabled={isBusy}
/>
</CanvasEntityContainer>
<CanvasEntityStateGate entityIdentifier={entityIdentifier}>
<CanvasEntityContainer>
<CanvasEntityHeader>
<CanvasEntityPreviewImage />
<CanvasEntityEditableTitle />
<Spacer />
<ControlLayerBadges />
<CanvasEntityHeaderCommonActions />
</CanvasEntityHeader>
<CanvasEntitySettingsWrapper>
<ControlLayerSettings />
</CanvasEntitySettingsWrapper>
<DndDropTarget
dndTarget={replaceCanvasEntityObjectsWithImageDndTarget}
dndTargetData={dndTargetData}
label={t('controlLayers.replaceLayer')}
isDisabled={isBusy}
/>
</CanvasEntityContainer>
</CanvasEntityStateGate>
</ControlLayerAdapterGate>
</EntityIdentifierContext.Provider>
);

View File

@@ -1,26 +1,47 @@
import { Badge } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
export const ControlLayerBadges = memo(() => {
const entityIdentifier = useEntityIdentifierContext('control_layer');
const { t } = useTranslation();
const withTransparencyEffect = useAppSelector(
(s) => selectEntityOrThrow(selectCanvasSlice(s), entityIdentifier).withTransparencyEffect
const buildSelectWithTransparencyEffect = (entityIdentifier: CanvasEntityIdentifier<'control_layer'>) =>
createSelector(
selectCanvasSlice,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'ControlLayerBadgesContent').withTransparencyEffect
);
const ControlLayerBadgesContent = memo(() => {
const entityIdentifier = useEntityIdentifierContext('control_layer');
const { t } = useTranslation();
const selectWithTransparencyEffect = useMemo(
() => buildSelectWithTransparencyEffect(entityIdentifier),
[entityIdentifier]
);
const withTransparencyEffect = useAppSelector(selectWithTransparencyEffect);
if (!withTransparencyEffect) {
return null;
}
return (
<>
{withTransparencyEffect && (
<Badge color="base.300" bg="transparent" borderWidth={1} userSelect="none">
{t('controlLayers.transparency')}
</Badge>
)}
</>
<Badge color="base.300" bg="transparent" borderWidth={1} userSelect="none">
{t('controlLayers.transparency')}
</Badge>
);
});
ControlLayerBadgesContent.displayName = 'ControlLayerBadgesContent';
export const ControlLayerBadges = memo(() => {
const entityIdentifier = useEntityIdentifierContext('control_layer');
return (
<CanvasEntityStateGate entityIdentifier={entityIdentifier}>
<ControlLayerBadgesContent />
</CanvasEntityStateGate>
);
});
ControlLayerBadges.displayName = 'ControlLayerBadges';

View File

@@ -28,24 +28,18 @@ import { useTranslation } from 'react-i18next';
import { PiBoundingBoxBold, PiShootingStarFill, PiUploadBold } from 'react-icons/pi';
import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types';
const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier<'control_layer'>) => {
const selectControlAdapter = useMemo(
() =>
createMemoizedAppSelector(selectCanvasSlice, (canvas) => {
const layer = selectEntityOrThrow(canvas, entityIdentifier);
return layer.controlAdapter;
}),
[entityIdentifier]
);
const controlAdapter = useAppSelector(selectControlAdapter);
return controlAdapter;
};
const buildSelectControlAdapter = (entityIdentifier: CanvasEntityIdentifier<'control_layer'>) =>
createMemoizedAppSelector(selectCanvasSlice, (canvas) => {
const layer = selectEntityOrThrow(canvas, entityIdentifier, 'ControlLayerControlAdapter');
return layer.controlAdapter;
});
export const ControlLayerControlAdapter = memo(() => {
const { t } = useTranslation();
const { dispatch, getState } = useAppStore();
const entityIdentifier = useEntityIdentifierContext('control_layer');
const controlAdapter = useControlLayerControlAdapter(entityIdentifier);
const selectControlAdapter = useMemo(() => buildSelectControlAdapter(entityIdentifier), [entityIdentifier]);
const controlAdapter = useAppSelector(selectControlAdapter);
const filter = useEntityFilter(entityIdentifier);
const isFLUX = useAppSelector(selectIsFLUX);
const adapter = useEntityAdapterContext('control_layer');

View File

@@ -5,21 +5,25 @@ import { useEntityIdentifierContext } from 'features/controlLayers/contexts/Enti
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import { controlLayerWithTransparencyEffectToggled } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDropHalfBold } from 'react-icons/pi';
const buildSelectWithTransparencyEffect = (entityIdentifier: CanvasEntityIdentifier<'control_layer'>) =>
createSelector(
selectCanvasSlice,
(canvas) =>
selectEntityOrThrow(canvas, entityIdentifier, 'ControlLayerMenuItemsTransparencyEffect').withTransparencyEffect
);
export const ControlLayerMenuItemsTransparencyEffect = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('control_layer');
const isLocked = useEntityIsLocked(entityIdentifier);
const selectWithTransparencyEffect = useMemo(
() =>
createSelector(selectCanvasSlice, (canvas) => {
const entity = selectEntityOrThrow(canvas, entityIdentifier);
return entity.withTransparencyEffect;
}),
() => buildSelectWithTransparencyEffect(entityIdentifier),
[entityIdentifier]
);
const withTransparencyEffect = useAppSelector(selectWithTransparencyEffect);

View File

@@ -4,6 +4,7 @@ import { CanvasEntityHeader } from 'features/controlLayers/components/common/Can
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
import { IPAdapterSettings } from 'features/controlLayers/components/IPAdapter/IPAdapterSettings';
import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useMemo } from 'react';
@@ -17,14 +18,16 @@ export const IPAdapter = memo(({ id }: Props) => {
return (
<EntityIdentifierContext.Provider value={entityIdentifier}>
<CanvasEntityContainer>
<CanvasEntityHeader ps={4} py={5}>
<CanvasEntityEditableTitle />
<Spacer />
<CanvasEntityHeaderCommonActions />
</CanvasEntityHeader>
<IPAdapterSettings />
</CanvasEntityContainer>
<CanvasEntityStateGate entityIdentifier={entityIdentifier}>
<CanvasEntityContainer>
<CanvasEntityHeader ps={4} py={5}>
<CanvasEntityEditableTitle />
<Spacer />
<CanvasEntityHeaderCommonActions />
</CanvasEntityHeader>
<IPAdapterSettings />
</CanvasEntityContainer>
</CanvasEntityStateGate>
</EntityIdentifierContext.Provider>
);
});

View File

@@ -1,8 +1,10 @@
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import type { IPMethodV2 } from 'features/controlLayers/store/types';
import { isIPMethodV2 } from 'features/controlLayers/store/types';
import { selectSystemShouldEnableModelDescriptions } from 'features/system/store/systemSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { assert } from 'tsafe';
@@ -14,13 +16,27 @@ type Props = {
export const IPAdapterMethod = memo(({ method, onChange }: Props) => {
const { t } = useTranslation();
const shouldShowModelDescriptions = useAppSelector(selectSystemShouldEnableModelDescriptions);
const options: { label: string; value: IPMethodV2 }[] = useMemo(
() => [
{ label: t('controlLayers.ipAdapterMethod.full'), value: 'full' },
{ label: t('controlLayers.ipAdapterMethod.style'), value: 'style' },
{ label: t('controlLayers.ipAdapterMethod.composition'), value: 'composition' },
{
label: t('controlLayers.ipAdapterMethod.full'),
value: 'full',
description: shouldShowModelDescriptions ? t('controlLayers.ipAdapterMethod.fullDesc') : undefined,
},
{
label: t('controlLayers.ipAdapterMethod.style'),
value: 'style',
description: shouldShowModelDescriptions ? t('controlLayers.ipAdapterMethod.styleDesc') : undefined,
},
{
label: t('controlLayers.ipAdapterMethod.composition'),
value: 'composition',
description: shouldShowModelDescriptions ? t('controlLayers.ipAdapterMethod.compositionDesc') : undefined,
},
],
[t]
[t, shouldShowModelDescriptions]
);
const _onChange = useCallback<ComboboxOnChange>(
(v) => {

View File

@@ -5,6 +5,7 @@ import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginE
import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper';
import { Weight } from 'features/controlLayers/components/common/Weight';
import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod';
import { IPAdapterSettingsEmptyState } from 'features/controlLayers/components/IPAdapter/IPAdapterSettingsEmptyState';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { usePullBboxIntoGlobalReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
@@ -17,8 +18,8 @@ import {
referenceImageIPAdapterWeightChanged,
} from 'features/controlLayers/store/canvasSlice';
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import { selectCanvasSlice, selectEntity, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
import { memo, useCallback, useMemo } from 'react';
@@ -29,14 +30,17 @@ import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
import { IPAdapterImagePreview } from './IPAdapterImagePreview';
import { IPAdapterModel } from './IPAdapterModel';
export const IPAdapterSettings = memo(() => {
const buildSelectIPAdapter = (entityIdentifier: CanvasEntityIdentifier<'reference_image'>) =>
createSelector(
selectCanvasSlice,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'IPAdapterSettings').ipAdapter
);
const IPAdapterSettingsContent = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('reference_image');
const selectIPAdapter = useMemo(
() => createSelector(selectCanvasSlice, (s) => selectEntityOrThrow(s, entityIdentifier).ipAdapter),
[entityIdentifier]
);
const selectIPAdapter = useMemo(() => buildSelectIPAdapter(entityIdentifier), [entityIdentifier]);
const ipAdapter = useAppSelector(selectIPAdapter);
const onChangeBeginEndStepPct = useCallback(
@@ -131,4 +135,25 @@ export const IPAdapterSettings = memo(() => {
);
});
IPAdapterSettingsContent.displayName = 'IPAdapterSettingsContent';
const buildSelectIPAdapterHasImage = (entityIdentifier: CanvasEntityIdentifier<'reference_image'>) =>
createSelector(selectCanvasSlice, (canvas) => {
const referenceImage = selectEntity(canvas, entityIdentifier);
return !!referenceImage && referenceImage.ipAdapter.image !== null;
});
export const IPAdapterSettings = memo(() => {
const entityIdentifier = useEntityIdentifierContext('reference_image');
const selectIPAdapterHasImage = useMemo(() => buildSelectIPAdapterHasImage(entityIdentifier), [entityIdentifier]);
const hasImage = useAppSelector(selectIPAdapterHasImage);
if (!hasImage) {
return <IPAdapterSettingsEmptyState />;
}
return <IPAdapterSettingsContent />;
});
IPAdapterSettings.displayName = 'IPAdapterSettings';

View File

@@ -0,0 +1,64 @@
import { Button, Flex, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { setGlobalReferenceImage } from 'features/imageActions/actions';
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
import { memo, useCallback, useMemo } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import type { ImageDTO } from 'services/api/types';
export const IPAdapterSettingsEmptyState = memo(() => {
const { t } = useTranslation();
const entityIdentifier = useEntityIdentifierContext('reference_image');
const dispatch = useAppDispatch();
const isBusy = useCanvasIsBusy();
const onUpload = useCallback(
(imageDTO: ImageDTO) => {
setGlobalReferenceImage({ imageDTO, entityIdentifier, dispatch });
},
[dispatch, entityIdentifier]
);
const uploadApi = useImageUploadButton({ onUpload, allowMultiple: false });
const onClickGalleryButton = useCallback(() => {
dispatch(activeTabCanvasRightPanelChanged('gallery'));
}, [dispatch]);
const dndTargetData = useMemo<SetGlobalReferenceImageDndTargetData>(
() => setGlobalReferenceImageDndTarget.getData({ entityIdentifier }),
[entityIdentifier]
);
const components = useMemo(
() => ({
UploadButton: (
<Button isDisabled={isBusy} size="sm" variant="link" color="base.300" {...uploadApi.getUploadButtonProps()} />
),
GalleryButton: (
<Button onClick={onClickGalleryButton} isDisabled={isBusy} size="sm" variant="link" color="base.300" />
),
}),
[isBusy, onClickGalleryButton, uploadApi]
);
return (
<Flex flexDir="column" gap={3} position="relative" w="full" p={4}>
<Text textAlign="center" color="base.300">
<Trans i18nKey="controlLayers.referenceImageEmptyState" components={components} />
</Text>
<input {...uploadApi.getUploadInputProps()} />
<DndDropTarget
dndTarget={setGlobalReferenceImageDndTarget}
dndTargetData={dndTargetData}
label={t('controlLayers.useImage')}
isDisabled={isBusy}
/>
</Flex>
);
});
IPAdapterSettingsEmptyState.displayName = 'IPAdapterSettingsEmptyState';

View File

@@ -4,6 +4,7 @@ import { CanvasEntityHeader } from 'features/controlLayers/components/common/Can
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate';
import { InpaintMaskAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
@@ -19,14 +20,16 @@ export const InpaintMask = memo(({ id }: Props) => {
return (
<EntityIdentifierContext.Provider value={entityIdentifier}>
<InpaintMaskAdapterGate>
<CanvasEntityContainer>
<CanvasEntityHeader>
<CanvasEntityPreviewImage />
<CanvasEntityEditableTitle />
<Spacer />
<CanvasEntityHeaderCommonActions />
</CanvasEntityHeader>
</CanvasEntityContainer>
<CanvasEntityStateGate entityIdentifier={entityIdentifier}>
<CanvasEntityContainer>
<CanvasEntityHeader>
<CanvasEntityPreviewImage />
<CanvasEntityEditableTitle />
<Spacer />
<CanvasEntityHeaderCommonActions />
</CanvasEntityHeader>
</CanvasEntityContainer>
</CanvasEntityStateGate>
</InpaintMaskAdapterGate>
</EntityIdentifierContext.Provider>
);

View File

@@ -4,6 +4,7 @@ import { CanvasEntityHeader } from 'features/controlLayers/components/common/Can
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate';
import { RasterLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
@@ -30,20 +31,22 @@ export const RasterLayer = memo(({ id }: Props) => {
return (
<EntityIdentifierContext.Provider value={entityIdentifier}>
<RasterLayerAdapterGate>
<CanvasEntityContainer>
<CanvasEntityHeader>
<CanvasEntityPreviewImage />
<CanvasEntityEditableTitle />
<Spacer />
<CanvasEntityHeaderCommonActions />
</CanvasEntityHeader>
<DndDropTarget
dndTarget={replaceCanvasEntityObjectsWithImageDndTarget}
dndTargetData={dndTargetData}
label={t('controlLayers.replaceLayer')}
isDisabled={isBusy}
/>
</CanvasEntityContainer>
<CanvasEntityStateGate entityIdentifier={entityIdentifier}>
<CanvasEntityContainer>
<CanvasEntityHeader>
<CanvasEntityPreviewImage />
<CanvasEntityEditableTitle />
<Spacer />
<CanvasEntityHeaderCommonActions />
</CanvasEntityHeader>
<DndDropTarget
dndTarget={replaceCanvasEntityObjectsWithImageDndTarget}
dndTargetData={dndTargetData}
label={t('controlLayers.replaceLayer')}
isDisabled={isBusy}
/>
</CanvasEntityContainer>
</CanvasEntityStateGate>
</RasterLayerAdapterGate>
</EntityIdentifierContext.Provider>
);

View File

@@ -6,6 +6,7 @@ import { CanvasEntityPreviewImage } from 'features/controlLayers/components/comm
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
import { RegionalGuidanceBadges } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges';
import { RegionalGuidanceSettings } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings';
import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate';
import { RegionalGuidanceAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
@@ -24,16 +25,18 @@ export const RegionalGuidance = memo(({ id }: Props) => {
return (
<EntityIdentifierContext.Provider value={entityIdentifier}>
<RegionalGuidanceAdapterGate>
<CanvasEntityContainer>
<CanvasEntityHeader>
<CanvasEntityPreviewImage />
<CanvasEntityEditableTitle />
<Spacer />
<RegionalGuidanceBadges />
<CanvasEntityHeaderCommonActions />
</CanvasEntityHeader>
<RegionalGuidanceSettings />
</CanvasEntityContainer>
<CanvasEntityStateGate entityIdentifier={entityIdentifier}>
<CanvasEntityContainer>
<CanvasEntityHeader>
<CanvasEntityPreviewImage />
<CanvasEntityEditableTitle />
<Spacer />
<RegionalGuidanceBadges />
<CanvasEntityHeaderCommonActions />
</CanvasEntityHeader>
<RegionalGuidanceSettings />
</CanvasEntityContainer>
</CanvasEntityStateGate>
</RegionalGuidanceAdapterGate>
</EntityIdentifierContext.Provider>
);

View File

@@ -10,7 +10,11 @@ export const RegionalGuidanceBadges = memo(() => {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const { t } = useTranslation();
const selectAutoNegative = useMemo(
() => createSelector(selectCanvasSlice, (canvas) => selectEntityOrThrow(canvas, entityIdentifier).autoNegative),
() =>
createSelector(
selectCanvasSlice,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidanceBadges').autoNegative
),
[entityIdentifier]
);
const autoNegative = useAppSelector(selectAutoNegative);

View File

@@ -6,6 +6,7 @@ import { Weight } from 'features/controlLayers/components/common/Weight';
import { IPAdapterImagePreview } from 'features/controlLayers/components/IPAdapter/IPAdapterImagePreview';
import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod';
import { IPAdapterModel } from 'features/controlLayers/components/IPAdapter/IPAdapterModel';
import { RegionalGuidanceIPAdapterSettingsEmptyState } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettingsEmptyState';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { usePullBboxIntoRegionalGuidanceReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
@@ -19,12 +20,12 @@ import {
rgIPAdapterWeightChanged,
} from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice, selectRegionalGuidanceReferenceImage } from 'features/controlLayers/store/selectors';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import type { CanvasEntityIdentifier, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import type { SetRegionalGuidanceReferenceImageDndTargetData } from 'features/dnd/dnd';
import { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiBoundingBoxBold, PiTrashSimpleFill } from 'react-icons/pi';
import { PiBoundingBoxBold, PiXBold } from 'react-icons/pi';
import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
@@ -32,7 +33,7 @@ type Props = {
referenceImageId: string;
};
export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Props) => {
const RegionalGuidanceIPAdapterSettingsContent = memo(({ referenceImageId }: Props) => {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const { t } = useTranslation();
const dispatch = useAppDispatch();
@@ -115,7 +116,7 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Pro
size="sm"
variant="link"
alignSelf="stretch"
icon={<PiTrashSimpleFill />}
icon={<PiXBold />}
tooltip={t('controlLayers.deleteReferenceImage')}
aria-label={t('controlLayers.deleteReferenceImage')}
onClick={onDeleteIPAdapter}
@@ -161,4 +162,31 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Pro
);
});
RegionalGuidanceIPAdapterSettingsContent.displayName = 'RegionalGuidanceIPAdapterSettingsContent';
const buildSelectIPAdapterHasImage = (
entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>,
referenceImageId: string
) =>
createSelector(selectCanvasSlice, (canvas) => {
const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId);
return !!referenceImage && referenceImage.ipAdapter.image !== null;
});
export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Props) => {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const selectIPAdapterHasImage = useMemo(
() => buildSelectIPAdapterHasImage(entityIdentifier, referenceImageId),
[entityIdentifier, referenceImageId]
);
const hasImage = useAppSelector(selectIPAdapterHasImage);
if (!hasImage) {
return <RegionalGuidanceIPAdapterSettingsEmptyState referenceImageId={referenceImageId} />;
}
return <RegionalGuidanceIPAdapterSettingsContent referenceImageId={referenceImageId} />;
});
RegionalGuidanceIPAdapterSettings.displayName = 'RegionalGuidanceIPAdapterSettings';

View File

@@ -0,0 +1,76 @@
import { Button, Flex, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import type { SetRegionalGuidanceReferenceImageDndTargetData } from 'features/dnd/dnd';
import { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { setRegionalGuidanceReferenceImage } from 'features/imageActions/actions';
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
import { memo, useCallback, useMemo } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import type { ImageDTO } from 'services/api/types';
type Props = {
referenceImageId: string;
};
export const RegionalGuidanceIPAdapterSettingsEmptyState = memo(({ referenceImageId }: Props) => {
const { t } = useTranslation();
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const dispatch = useAppDispatch();
const isBusy = useCanvasIsBusy();
const onUpload = useCallback(
(imageDTO: ImageDTO) => {
setRegionalGuidanceReferenceImage({ imageDTO, entityIdentifier, referenceImageId, dispatch });
},
[dispatch, entityIdentifier, referenceImageId]
);
const uploadApi = useImageUploadButton({ onUpload, allowMultiple: false });
const onClickGalleryButton = useCallback(() => {
dispatch(activeTabCanvasRightPanelChanged('gallery'));
}, [dispatch]);
const dndTargetData = useMemo<SetRegionalGuidanceReferenceImageDndTargetData>(
() =>
setRegionalGuidanceReferenceImageDndTarget.getData({
entityIdentifier,
referenceImageId,
}),
[entityIdentifier, referenceImageId]
);
return (
<Flex flexDir="column" gap={3} position="relative" w="full" p={4}>
<Text textAlign="center" color="base.300">
<Trans
i18nKey="controlLayers.referenceImageEmptyState"
components={{
UploadButton: (
<Button
isDisabled={isBusy}
size="sm"
variant="link"
color="base.300"
{...uploadApi.getUploadButtonProps()}
/>
),
GalleryButton: (
<Button onClick={onClickGalleryButton} isDisabled={isBusy} size="sm" variant="link" color="base.300" />
),
}}
/>
</Text>
<input {...uploadApi.getUploadInputProps()} />
<DndDropTarget
dndTarget={setRegionalGuidanceReferenceImageDndTarget}
dndTargetData={dndTargetData}
label={t('controlLayers.useImage')}
isDisabled={isBusy}
/>
</Flex>
);
});
RegionalGuidanceIPAdapterSettingsEmptyState.displayName = 'RegionalGuidanceIPAdapterSettingsEmptyState';

View File

@@ -13,7 +13,11 @@ export const RegionalGuidanceIPAdapters = memo(() => {
const selectIPAdapterIds = useMemo(
() =>
createMemoizedSelector(selectCanvasSlice, (canvas) => {
const ipAdapterIds = selectEntityOrThrow(canvas, entityIdentifier).referenceImages.map(({ id }) => id);
const ipAdapterIds = selectEntityOrThrow(
canvas,
entityIdentifier,
'RegionalGuidanceIPAdapters'
).referenceImages.map(({ id }) => id);
if (ipAdapterIds.length === 0) {
return EMPTY_ARRAY;
}

View File

@@ -13,7 +13,11 @@ export const RegionalGuidanceMenuItemsAutoNegative = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectAutoNegative = useMemo(
() => createSelector(selectCanvasSlice, (canvas) => selectEntityOrThrow(canvas, entityIdentifier).autoNegative),
() =>
createSelector(
selectCanvasSlice,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidanceMenuItemsAutoNegative').autoNegative
),
[entityIdentifier]
);
const autoNegative = useAppSelector(selectAutoNegative);

View File

@@ -20,7 +20,10 @@ export const RegionalGuidanceNegativePrompt = memo(() => {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const selectPrompt = useMemo(
() =>
createSelector(selectCanvasSlice, (canvas) => selectEntityOrThrow(canvas, entityIdentifier).negativePrompt ?? ''),
createSelector(
selectCanvasSlice,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidanceNegativePrompt').negativePrompt ?? ''
),
[entityIdentifier]
);
const prompt = useAppSelector(selectPrompt);

View File

@@ -20,7 +20,10 @@ export const RegionalGuidancePositivePrompt = memo(() => {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const selectPrompt = useMemo(
() =>
createSelector(selectCanvasSlice, (canvas) => selectEntityOrThrow(canvas, entityIdentifier).positivePrompt ?? ''),
createSelector(
selectCanvasSlice,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidancePositivePrompt').positivePrompt ?? ''
),
[entityIdentifier]
);
const prompt = useAppSelector(selectPrompt);

View File

@@ -5,26 +5,25 @@ import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/c
import { RegionalGuidanceAddPromptsIPAdapterButtons } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useMemo } from 'react';
import { RegionalGuidanceIPAdapters } from './RegionalGuidanceIPAdapters';
import { RegionalGuidanceNegativePrompt } from './RegionalGuidanceNegativePrompt';
import { RegionalGuidancePositivePrompt } from './RegionalGuidancePositivePrompt';
const buildSelectFlags = (entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>) =>
createMemoizedSelector(selectCanvasSlice, (canvas) => {
const entity = selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidanceSettings');
return {
hasPositivePrompt: entity.positivePrompt !== null,
hasNegativePrompt: entity.negativePrompt !== null,
hasIPAdapters: entity.referenceImages.length > 0,
};
});
export const RegionalGuidanceSettings = memo(() => {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const selectFlags = useMemo(
() =>
createMemoizedSelector(selectCanvasSlice, (canvas) => {
const entity = selectEntityOrThrow(canvas, entityIdentifier);
return {
hasPositivePrompt: entity.positivePrompt !== null,
hasNegativePrompt: entity.negativePrompt !== null,
hasIPAdapters: entity.referenceImages.length > 0,
};
}),
[entityIdentifier]
);
const selectFlags = useMemo(() => buildSelectFlags(entityIdentifier), [entityIdentifier]);
const flags = useAppSelector(selectFlags);
return (

View File

@@ -1,4 +1,6 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $authToken } from 'app/store/nanostores/authToken';
import { useAppSelector } from 'app/store/storeHooks';
import { withResultAsync } from 'common/util/result';
import { selectSelectedImage } from 'features/controlLayers/store/canvasStagingAreaSlice';
@@ -14,6 +16,7 @@ const TOAST_ID = 'SAVE_STAGING_AREA_IMAGE_TO_GALLERY';
export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
const selectedImage = useAppSelector(selectSelectedImage);
const authToken = useStore($authToken);
const { t } = useTranslation();
@@ -26,7 +29,14 @@ export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
// the gallery without borking the canvas, which may need this image to exist.
const result = await withResultAsync(async () => {
// Download the image
const res = await fetch(selectedImage.imageDTO.image_url);
const requestOpts = authToken
? {
headers: {
Authorization: `Bearer ${authToken}`,
},
}
: {};
const res = await fetch(selectedImage.imageDTO.image_url, requestOpts);
const blob = await res.blob();
// Create a new file with the same name, which we will upload
const file = new File([blob], `copy_of_${selectedImage.imageDTO.image_name}`, { type: 'image/png' });
@@ -56,7 +66,7 @@ export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
status: 'error',
});
}
}, [autoAddBoardId, selectedImage, t]);
}, [autoAddBoardId, selectedImage, t, authToken]);
return (
<IconButton

View File

@@ -4,8 +4,8 @@ import { CanvasSettingsPopover } from 'features/controlLayers/components/Setting
import { ToolColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings';
import { CanvasToolbarFitBboxToLayersButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarFitBboxToLayersButton';
import { CanvasToolbarNewSessionMenuButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarNewSessionMenuButton';
import { CanvasToolbarRedoButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarRedoButton';
import { CanvasToolbarResetCanvasButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarResetCanvasButton';
import { CanvasToolbarResetViewButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton';
import { CanvasToolbarSaveToGalleryButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarSaveToGalleryButton';
import { CanvasToolbarScale } from 'features/controlLayers/components/Toolbar/CanvasToolbarScale';
@@ -43,7 +43,7 @@ export const CanvasToolbar = memo(() => {
<CanvasToolbarSaveToGalleryButton />
<CanvasToolbarUndoButton />
<CanvasToolbarRedoButton />
<CanvasToolbarResetCanvasButton />
<CanvasToolbarNewSessionMenuButton />
<CanvasSettingsPopover />
</Flex>
</Flex>

View File

@@ -0,0 +1,25 @@
import { IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
import { SessionMenuItems } from 'common/components/SessionMenuItems';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFilePlusBold } from 'react-icons/pi';
export const CanvasToolbarNewSessionMenuButton = memo(() => {
const { t } = useTranslation();
return (
<Menu placement="bottom-end">
<MenuButton
as={IconButton}
aria-label={t('controlLayers.newSession')}
icon={<PiFilePlusBold />}
variant="link"
alignSelf="stretch"
/>
<MenuList>
<SessionMenuItems />
</MenuList>
</Menu>
);
});
CanvasToolbarNewSessionMenuButton.displayName = 'CanvasToolbarNewSessionMenuButton';

View File

@@ -1,30 +0,0 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { canvasReset } from 'features/controlLayers/store/actions';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashBold } from 'react-icons/pi';
export const CanvasToolbarResetCanvasButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const canvasManager = useCanvasManager();
const onClick = useCallback(() => {
dispatch(canvasReset());
canvasManager.stage.fitLayersToStage();
}, [canvasManager.stage, dispatch]);
return (
<IconButton
aria-label={t('controlLayers.resetCanvas')}
tooltip={t('controlLayers.resetCanvas')}
onClick={onClick}
colorScheme="error"
icon={<PiTrashBold />}
variant="link"
alignSelf="stretch"
/>
);
});
CanvasToolbarResetCanvasButton.displayName = 'CanvasToolbarResetCanvasButton';

View File

@@ -0,0 +1,20 @@
import { useAppSelector } from 'app/store/storeHooks';
import { selectEntityExists } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import type { PropsWithChildren } from 'react';
import { memo, useMemo } from 'react';
/**
* A "gate" component that renders its children only if the entity exists in redux state.
*/
export const CanvasEntityStateGate = memo((props: PropsWithChildren<{ entityIdentifier: CanvasEntityIdentifier }>) => {
const selector = useMemo(() => selectEntityExists(props.entityIdentifier), [props.entityIdentifier]);
const entityExistsInState = useAppSelector(selector);
if (!entityExistsInState) {
return null;
}
return props.children;
});
CanvasEntityStateGate.displayName = 'CanvasEntityStateGate';

View File

@@ -14,7 +14,7 @@ import {
rgPositivePromptChanged,
} from 'features/controlLayers/store/canvasSlice';
import { selectBase } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors';
import type {
CanvasEntityIdentifier,
CanvasRegionalGuidanceState,
@@ -168,7 +168,7 @@ export const buildSelectValidRegionalGuidanceActions = (
entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>
) => {
return createMemoizedSelector(selectCanvasSlice, (canvas) => {
const entity = selectEntityOrThrow(canvas, entityIdentifier);
const entity = selectEntity(canvas, entityIdentifier);
return {
canAddPositivePrompt: entity?.positivePrompt === null,
canAddNegativePrompt: entity?.negativePrompt === null,

View File

@@ -0,0 +1,31 @@
import { useStore } from '@nanostores/react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { $invocationProgressMessage } from 'services/events/stores';
export const useDeferredModelLoadingInvocationProgressMessage = () => {
const { t } = useTranslation();
const invocationProgressMessage = useStore($invocationProgressMessage);
const [delayedMessage, setDelayedMessage] = useState<string | null>(null);
useEffect(() => {
if (!invocationProgressMessage) {
setDelayedMessage(null);
return;
}
if (invocationProgressMessage && !invocationProgressMessage.startsWith('Loading model')) {
setDelayedMessage(null);
return;
}
// Set a timeout to update delayedMessage after 5 seconds
const timer = setTimeout(() => {
setDelayedMessage(`${t('common.loadingModel')}...`);
}, 5000);
return () => clearTimeout(timer); // Cleanup on effect re-run
}, [invocationProgressMessage, t]);
return delayedMessage;
};

View File

@@ -9,6 +9,7 @@ import {
getEmptyRect,
getKonvaNodeDebugAttrs,
getPrefixedId,
offsetCoord,
} from 'features/controlLayers/konva/util';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import type { Coordinate, Rect, RectWithRotation } from 'features/controlLayers/store/types';
@@ -558,6 +559,25 @@ export class CanvasEntityTransformer extends CanvasModuleBase {
this.manager.stateApi.setEntityPosition({ entityIdentifier: this.parent.entityIdentifier, position });
};
nudgeBy = (offset: Coordinate) => {
// We can immediately move both the proxy rect and layer objects so we don't have to wait for a redux round-trip,
// which can take up to 2ms in my testing. This is optional, but can make the interaction feel more responsive,
// especially on lower-end devices.
// Get the relative position of the layer's objects, according to konva
const position = this.konva.proxyRect.position();
// Offset the position by the nudge amount
const newPosition = offsetCoord(position, offset);
// Set the new position of the proxy rect - this doesn't move the layer objects - only the outline rect
this.konva.proxyRect.setAttrs(newPosition);
// Sync the layer objects with the proxy rect - moves them to the new position
this.syncObjectGroupWithProxyRect();
// Push to redux. The state change will do a round-trip, and eventually make it back to the canvas classes, at
// which point the layer will be moved to the new position.
this.manager.stateApi.moveEntityBy({ entityIdentifier: this.parent.entityIdentifier, offset });
this.log.trace({ offset }, 'Nudged');
};
syncObjectGroupWithProxyRect = () => {
this.parent.renderer.konva.objectGroup.setAttrs({
x: this.konva.proxyRect.x(),

View File

@@ -20,7 +20,8 @@ import {
controlLayerAdded,
entityBrushLineAdded,
entityEraserLineAdded,
entityMoved,
entityMovedBy,
entityMovedTo,
entityRasterized,
entityRectAdded,
entityReset,
@@ -40,7 +41,8 @@ import type {
EntityBrushLineAddedPayload,
EntityEraserLineAddedPayload,
EntityIdentifierPayload,
EntityMovedPayload,
EntityMovedByPayload,
EntityMovedToPayload,
EntityRasterizedPayload,
EntityRectAddedPayload,
Rect,
@@ -51,7 +53,7 @@ import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import { atom, computed } from 'nanostores';
import type { Logger } from 'roarr';
import { getImageDTO } from 'services/api/endpoints/images';
import { queueApi } from 'services/api/endpoints/queue';
import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue';
import type { BatchConfig, ImageDTO, S } from 'services/api/types';
import { QueueError } from 'services/events/errors';
import type { Param0 } from 'tsafe';
@@ -139,8 +141,15 @@ export class CanvasStateApiModule extends CanvasModuleBase {
/**
* Updates an entity's position, pushing state to redux.
*/
setEntityPosition = (arg: EntityMovedPayload) => {
this.store.dispatch(entityMoved(arg));
setEntityPosition = (arg: EntityMovedToPayload) => {
this.store.dispatch(entityMovedTo(arg));
};
/**
* Moves an entity by the give offset, pushing state to redux.
*/
moveEntityBy = (arg: EntityMovedByPayload) => {
this.store.dispatch(entityMovedBy(arg));
};
/**
@@ -402,7 +411,7 @@ export class CanvasStateApiModule extends CanvasModuleBase {
queueApi.endpoints.enqueueBatch.initiate(batch, {
// Use the same cache key for all enqueueBatch requests, so that all consumers of this query get the same status
// updates.
fixedCacheKey: 'enqueueBatch',
...enqueueMutationFixedCacheKeyOptions,
// We do not need RTK to track this request in the store
track: false,
})

View File

@@ -1,9 +1,24 @@
import { $focusedRegion } from 'common/hooks/focus';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import type { CanvasToolModule } from 'features/controlLayers/konva/CanvasTool/CanvasToolModule';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import type { Coordinate } from 'features/controlLayers/store/types';
import type { Logger } from 'roarr';
type CanvasMoveToolModuleConfig = {
/**
* The number of pixels to nudge the entity by when moving with the arrow keys.
*/
NUDGE_PX: number;
};
const DEFAULT_CONFIG: CanvasMoveToolModuleConfig = {
NUDGE_PX: 1,
};
type NudgeKey = 'ArrowLeft' | 'ArrowRight' | 'ArrowUp' | 'ArrowDown';
export class CanvasMoveToolModule extends CanvasModuleBase {
readonly type = 'move_tool';
readonly id: string;
@@ -12,6 +27,9 @@ export class CanvasMoveToolModule extends CanvasModuleBase {
readonly manager: CanvasManager;
readonly log: Logger;
config: CanvasMoveToolModuleConfig = DEFAULT_CONFIG;
nudgeOffsets: Record<NudgeKey, Coordinate>;
constructor(parent: CanvasToolModule) {
super();
this.id = getPrefixedId(this.type);
@@ -19,8 +37,18 @@ export class CanvasMoveToolModule extends CanvasModuleBase {
this.manager = this.parent.manager;
this.path = this.manager.buildPath(this);
this.log = this.manager.buildLogger(this);
this.log.debug('Creating module');
this.nudgeOffsets = {
ArrowLeft: { x: -this.config.NUDGE_PX, y: 0 },
ArrowRight: { x: this.config.NUDGE_PX, y: 0 },
ArrowUp: { x: 0, y: -this.config.NUDGE_PX },
ArrowDown: { x: 0, y: this.config.NUDGE_PX },
};
}
isNudgeKey(key: string): key is NudgeKey {
return this.nudgeOffsets[key as NudgeKey] !== undefined;
}
syncCursorStyle = () => {
@@ -32,4 +60,45 @@ export class CanvasMoveToolModule extends CanvasModuleBase {
selectedEntity.transformer.syncCursorStyle();
}
};
nudge = (nudgeKey: NudgeKey) => {
if ($focusedRegion.get() !== 'canvas') {
return;
}
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
if (!selectedEntity) {
return;
}
if (
selectedEntity.$isDisabled.get() ||
selectedEntity.$isEmpty.get() ||
selectedEntity.$isLocked.get() ||
selectedEntity.$isEntityTypeHidden.get()
) {
return;
}
const isBusy = this.manager.$isBusy.get();
const isMoveToolSelected = this.parent.$tool.get() === 'move';
const isThisEntityTransforming = this.manager.stateApi.$transformingAdapter.get() === selectedEntity;
if (isBusy) {
// When the canvas is busy, we shouldn't allow nudging - except when the canvas is busy transforming the selected
// entity. Nudging is allowed during transformation, regardless of the selected tool.
if (!isThisEntityTransforming) {
return;
}
} else {
// Otherwise, the canvas is not busy, and we should only allow nudging when the move tool is selected.
if (!isMoveToolSelected) {
return;
}
}
const offset = this.nudgeOffsets[nudgeKey];
selectedEntity.transformer.nudgeBy(offset);
};
}

View File

@@ -528,11 +528,16 @@ export class CanvasToolModule extends CanvasModuleBase {
};
onKeyDown = (e: KeyboardEvent) => {
if (e.repeat) {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;
}
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
// Handle nudging - must be before repeat, as we may want to catch repeating keys
if (this.tools.move.isNudgeKey(e.key)) {
this.tools.move.nudge(e.key);
}
if (e.repeat) {
return;
}

View File

@@ -18,6 +18,7 @@ import type {
CanvasEntityType,
CanvasInpaintMaskState,
CanvasMetadata,
EntityMovedByPayload,
FillStyle,
RegionalGuidanceReferenceImageState,
RgbColor,
@@ -51,7 +52,7 @@ import type {
EntityBrushLineAddedPayload,
EntityEraserLineAddedPayload,
EntityIdentifierPayload,
EntityMovedPayload,
EntityMovedToPayload,
EntityRasterizedPayload,
EntityRectAddedPayload,
IPMethodV2,
@@ -1201,7 +1202,7 @@ export const canvasSlice = createSlice({
}
entity.fill.style = style;
},
entityMoved: (state, action: PayloadAction<EntityMovedPayload>) => {
entityMovedTo: (state, action: PayloadAction<EntityMovedToPayload>) => {
const { entityIdentifier, position } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
@@ -1212,6 +1213,20 @@ export const canvasSlice = createSlice({
entity.position = position;
}
},
entityMovedBy: (state, action: PayloadAction<EntityMovedByPayload>) => {
const { entityIdentifier, offset } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
if (!isRenderableEntity(entity)) {
return;
}
entity.position.x += offset.x;
entity.position.y += offset.y;
},
entityRasterized: (state, action: PayloadAction<EntityRasterizedPayload>) => {
const { entityIdentifier, imageObject, position, replaceObjects, isSelected } = action.payload;
const entity = selectEntity(state, entityIdentifier);
@@ -1505,7 +1520,8 @@ export const {
entityIsLockedToggled,
entityFillColorChanged,
entityFillStyleChanged,
entityMoved,
entityMovedTo,
entityMovedBy,
entityDuplicated,
entityRasterized,
entityBrushLineAdded,

View File

@@ -83,7 +83,7 @@ const initialState: ParamsState = {
canvasCoherenceMode: 'Gaussian Blur',
canvasCoherenceMinDenoise: 0,
canvasCoherenceEdgeSize: 16,
infillMethod: 'patchmatch',
infillMethod: 'lama',
infillTileSize: 32,
infillPatchmatchDownscaleSize: 1,
infillColorValue: { r: 0, g: 0, b: 0, a: 1 },
@@ -273,24 +273,27 @@ export const paramsSlice = createSlice({
setCanvasCoherenceMinDenoise: (state, action: PayloadAction<number>) => {
state.canvasCoherenceMinDenoise = action.payload;
},
paramsReset: (state) => resetState(state),
},
extraReducers(builder) {
builder.addMatcher(newSessionRequested, (state) => {
// When a new session is requested, we need to keep the current model selections, plus dependent state
// like VAE precision. Everything else gets reset to default.
const newState = deepClone(initialState);
newState.model = state.model;
newState.vae = state.vae;
newState.fluxVAE = state.fluxVAE;
newState.vaePrecision = state.vaePrecision;
newState.t5EncoderModel = state.t5EncoderModel;
newState.clipEmbedModel = state.clipEmbedModel;
newState.refinerModel = state.refinerModel;
return newState;
});
builder.addMatcher(newSessionRequested, (state) => resetState(state));
},
});
const resetState = (state: ParamsState): ParamsState => {
// When a new session is requested, we need to keep the current model selections, plus dependent state
// like VAE precision. Everything else gets reset to default.
const newState = deepClone(initialState);
newState.model = state.model;
newState.vae = state.vae;
newState.fluxVAE = state.fluxVAE;
newState.vaePrecision = state.vaePrecision;
newState.t5EncoderModel = state.t5EncoderModel;
newState.clipEmbedModel = state.clipEmbedModel;
newState.refinerModel = state.refinerModel;
return newState;
};
export const {
setInfillMethod,
setInfillTileSize,
@@ -334,6 +337,7 @@ export const {
setRefinerNegativeAestheticScore,
setRefinerStart,
modelChanged,
paramsReset,
} = paramsSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */

View File

@@ -202,17 +202,25 @@ export const selectRegionalGuidanceEntities = createSelector(
/**
* Selected an entity from the canvas slice. If the entity is not found, an error is thrown.
*
* Provide a `caller` string to help identify the caller in the error message.
*
* Wrapper around {@link selectEntity}.
*/
export function selectEntityOrThrow<T extends CanvasEntityIdentifier>(
state: CanvasState,
entityIdentifier: T
entityIdentifier: T,
caller: string
): Extract<CanvasEntityState, T> {
const entity = selectEntity(state, entityIdentifier);
assert(entity, `Entity with id ${entityIdentifier.id} not found`);
assert(entity, `Entity with id ${entityIdentifier.id} not found in ${caller}`);
return entity;
}
export const selectEntityExists = <T extends CanvasEntityIdentifier>(entityIdentifier: T) => {
return createSelector(selectCanvasSlice, (canvas) => Boolean(selectEntity(canvas, entityIdentifier)));
};
/**
* Selects all entities of the given type.
*/

View File

@@ -439,7 +439,8 @@ export type EntityIdentifierPayload<
entityIdentifier: CanvasEntityIdentifier<U>;
} & T;
export type EntityMovedPayload = EntityIdentifierPayload<{ position: Coordinate }>;
export type EntityMovedToPayload = EntityIdentifierPayload<{ position: Coordinate }>;
export type EntityMovedByPayload = EntityIdentifierPayload<{ offset: Coordinate }>;
export type EntityBrushLineAddedPayload = EntityIdentifierPayload<{
brushLine: CanvasBrushLineState | CanvasBrushLineWithPressureState;
}>;

View File

@@ -22,12 +22,15 @@ const sx = {
},
} satisfies SystemStyleObject;
type Props = ImageProps & {
imageDTO: ImageDTO;
asThumbnail?: boolean;
};
/* eslint-disable-next-line @typescript-eslint/no-namespace */
export namespace DndImage {
export interface Props extends ImageProps {
imageDTO: ImageDTO;
asThumbnail?: boolean;
}
}
export const DndImage = memo(({ imageDTO, asThumbnail, ...rest }: Props) => {
export const DndImage = memo(({ imageDTO, asThumbnail, ...rest }: DndImage.Props) => {
const store = useAppStore();
const [isDragging, setIsDragging] = useState(false);
const [element, ref] = useState<HTMLImageElement | null>(null);

View File

@@ -9,6 +9,7 @@ import { selectComparisonImages } from 'features/gallery/components/ImageViewer/
import type { BoardId } from 'features/gallery/store/types';
import {
addImagesToBoard,
addImagesToNodeImageFieldCollectionAction,
createNewCanvasEntityFromImage,
removeImagesFromBoard,
replaceCanvasEntityObjectsWithImage,
@@ -220,7 +221,7 @@ const _setNodeImageFieldImage = buildTypeAndKey('set-node-image-field-image');
export type SetNodeImageFieldImageDndTargetData = DndData<
typeof _setNodeImageFieldImage.type,
typeof _setNodeImageFieldImage.key,
{ fieldIdentifer: FieldIdentifier }
{ fieldIdentifier: FieldIdentifier }
>;
export const setNodeImageFieldImageDndTarget: DndTarget<SetNodeImageFieldImageDndTargetData, SingleImageDndSourceData> =
{
@@ -235,12 +236,51 @@ export const setNodeImageFieldImageDndTarget: DndTarget<SetNodeImageFieldImageDn
},
handler: ({ sourceData, targetData, dispatch }) => {
const { imageDTO } = sourceData.payload;
const { fieldIdentifer } = targetData.payload;
setNodeImageFieldImage({ fieldIdentifer, imageDTO, dispatch });
const { fieldIdentifier } = targetData.payload;
setNodeImageFieldImage({ fieldIdentifier, imageDTO, dispatch });
},
};
//#endregion
//#region Add Images to Image Collection Node Field
const _addImagesToNodeImageFieldCollection = buildTypeAndKey('add-images-to-image-collection-node-field');
export type AddImagesToNodeImageFieldCollection = DndData<
typeof _addImagesToNodeImageFieldCollection.type,
typeof _addImagesToNodeImageFieldCollection.key,
{ fieldIdentifier: FieldIdentifier }
>;
export const addImagesToNodeImageFieldCollectionDndTarget: DndTarget<
AddImagesToNodeImageFieldCollection,
SingleImageDndSourceData | MultipleImageDndSourceData
> = {
..._addImagesToNodeImageFieldCollection,
typeGuard: buildTypeGuard(_addImagesToNodeImageFieldCollection.key),
getData: buildGetData(_addImagesToNodeImageFieldCollection.key, _addImagesToNodeImageFieldCollection.type),
isValid: ({ sourceData }) => {
if (singleImageDndSource.typeGuard(sourceData) || multipleImageDndSource.typeGuard(sourceData)) {
return true;
}
return false;
},
handler: ({ sourceData, targetData, dispatch, getState }) => {
if (!singleImageDndSource.typeGuard(sourceData) && !multipleImageDndSource.typeGuard(sourceData)) {
return;
}
const { fieldIdentifier } = targetData.payload;
const imageDTOs: ImageDTO[] = [];
if (singleImageDndSource.typeGuard(sourceData)) {
imageDTOs.push(sourceData.payload.imageDTO);
} else {
imageDTOs.push(...sourceData.payload.imageDTOs);
}
addImagesToNodeImageFieldCollectionAction({ fieldIdentifier, imageDTOs, dispatch, getState });
},
};
//#endregion
//# Set Comparison Image
const _setComparisonImage = buildTypeAndKey('set-comparison-image');
export type SetComparisonImageDndTargetData = DndData<
@@ -430,6 +470,7 @@ export const dndTargets = [
// Single or Multiple Image
addImageToBoardDndTarget,
removeImageFromBoardDndTarget,
addImagesToNodeImageFieldCollectionDndTarget,
] as const;
export type AnyDndTarget = (typeof dndTargets)[number];

View File

@@ -7,7 +7,7 @@ const zSeedBehaviour = z.enum(['PER_ITERATION', 'PER_PROMPT']);
type SeedBehaviour = z.infer<typeof zSeedBehaviour>;
export const isSeedBehaviour = (v: unknown): v is SeedBehaviour => zSeedBehaviour.safeParse(v).success;
interface DynamicPromptsState {
export interface DynamicPromptsState {
_version: 1;
maxPrompts: number;
combinatorial: boolean;

View File

@@ -0,0 +1,110 @@
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { useAppSelector } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { selectIsSD3 } from 'features/controlLayers/store/paramsSlice';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { newCanvasFromImage } from 'features/imageActions/actions';
import { toast } from 'features/toast/toast';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFileBold, PiPlusBold } from 'react-icons/pi';
export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
const { t } = useTranslation();
const subMenu = useSubMenu();
const store = useAppStore();
const imageDTO = useImageDTOContext();
const imageViewer = useImageViewer();
const isBusy = useCanvasIsBusy();
const isSD3 = useAppSelector(selectIsSD3);
const onClickNewCanvasWithRasterLayerFromImage = useCallback(() => {
const { dispatch, getState } = store;
newCanvasFromImage({ imageDTO, withResize: false, type: 'raster_layer', dispatch, getState });
dispatch(setActiveTab('canvas'));
imageViewer.close();
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
status: 'success',
});
}, [imageDTO, imageViewer, store, t]);
const onClickNewCanvasWithControlLayerFromImage = useCallback(() => {
const { dispatch, getState } = store;
newCanvasFromImage({ imageDTO, withResize: false, type: 'control_layer', dispatch, getState });
dispatch(setActiveTab('canvas'));
imageViewer.close();
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
status: 'success',
});
}, [imageDTO, imageViewer, store, t]);
const onClickNewCanvasWithRasterLayerFromImageWithResize = useCallback(() => {
const { dispatch, getState } = store;
newCanvasFromImage({ imageDTO, withResize: true, type: 'raster_layer', dispatch, getState });
dispatch(setActiveTab('canvas'));
imageViewer.close();
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
status: 'success',
});
}, [imageDTO, imageViewer, store, t]);
const onClickNewCanvasWithControlLayerFromImageWithResize = useCallback(() => {
const { dispatch, getState } = store;
newCanvasFromImage({ imageDTO, withResize: true, type: 'control_layer', dispatch, getState });
dispatch(setActiveTab('canvas'));
imageViewer.close();
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
status: 'success',
});
}, [imageDTO, imageViewer, store, t]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiPlusBold />}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.newCanvasFromImage')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem icon={<PiFileBold />} onClickCapture={onClickNewCanvasWithRasterLayerFromImage} isDisabled={isBusy}>
{t('controlLayers.asRasterLayer')}
</MenuItem>
<MenuItem
icon={<PiFileBold />}
onClickCapture={onClickNewCanvasWithRasterLayerFromImageWithResize}
isDisabled={isBusy}
>
{t('controlLayers.asRasterLayerResize')}
</MenuItem>
<MenuItem
icon={<PiFileBold />}
onClickCapture={onClickNewCanvasWithControlLayerFromImage}
isDisabled={isBusy || isSD3}
>
{t('controlLayers.asControlLayer')}
</MenuItem>
<MenuItem
icon={<PiFileBold />}
onClickCapture={onClickNewCanvasWithControlLayerFromImageWithResize}
isDisabled={isBusy || isSD3}
>
{t('controlLayers.asControlLayerResize')}
</MenuItem>
</MenuList>
</Menu>
</MenuItem>
);
});
ImageMenuItemNewCanvasFromImageSubMenu.displayName = 'ImageMenuItemNewCanvasFromImageSubMenu';

View File

@@ -8,14 +8,14 @@ import { selectIsFLUX, selectIsSD3 } from 'features/controlLayers/store/paramsSl
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { sentImageToCanvas } from 'features/gallery/store/actions';
import { createNewCanvasEntityFromImage, newCanvasFromImage } from 'features/imageActions/actions';
import { createNewCanvasEntityFromImage } from 'features/imageActions/actions';
import { toast } from 'features/toast/toast';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFileBold, PiPlusBold } from 'react-icons/pi';
import { PiPlusBold } from 'react-icons/pi';
export const ImageMenuItemNewFromImageSubMenu = memo(() => {
export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
const { t } = useTranslation();
const subMenu = useSubMenu();
const store = useAppStore();
@@ -25,30 +25,6 @@ export const ImageMenuItemNewFromImageSubMenu = memo(() => {
const isFLUX = useAppSelector(selectIsFLUX);
const isSD3 = useAppSelector(selectIsSD3);
const onClickNewCanvasWithRasterLayerFromImage = useCallback(() => {
const { dispatch, getState } = store;
newCanvasFromImage({ imageDTO, type: 'raster_layer', dispatch, getState });
dispatch(setActiveTab('canvas'));
imageViewer.close();
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
status: 'success',
});
}, [imageDTO, imageViewer, store, t]);
const onClickNewCanvasWithControlLayerFromImage = useCallback(() => {
const { dispatch, getState } = store;
newCanvasFromImage({ imageDTO, type: 'control_layer', dispatch, getState });
dispatch(setActiveTab('canvas'));
imageViewer.close();
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
status: 'success',
});
}, [imageDTO, imageViewer, store, t]);
const onClickNewRasterLayerFromImage = useCallback(() => {
const { dispatch, getState } = store;
createNewCanvasEntityFromImage({ imageDTO, type: 'raster_layer', dispatch, getState });
@@ -105,19 +81,9 @@ export const ImageMenuItemNewFromImageSubMenu = memo(() => {
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiPlusBold />}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.newFromImage')} />
<SubMenuButtonContent label={t('controlLayers.newLayerFromImage')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem icon={<PiFileBold />} onClickCapture={onClickNewCanvasWithRasterLayerFromImage} isDisabled={isBusy}>
{t('controlLayers.canvasAsRasterLayer')}
</MenuItem>
<MenuItem
icon={<PiFileBold />}
onClickCapture={onClickNewCanvasWithControlLayerFromImage}
isDisabled={isBusy || isSD3}
>
{t('controlLayers.canvasAsControlLayer')}
</MenuItem>
<MenuItem icon={<NewLayerIcon />} onClickCapture={onClickNewInpaintMaskFromImage} isDisabled={isBusy}>
{t('controlLayers.inpaintMask')}
</MenuItem>
@@ -144,4 +110,4 @@ export const ImageMenuItemNewFromImageSubMenu = memo(() => {
);
});
ImageMenuItemNewFromImageSubMenu.displayName = 'ImageMenuItemNewFromImageSubMenu';
ImageMenuItemNewLayerFromImageSubMenu.displayName = 'ImageMenuItemNewLayerFromImageSubMenu';

View File

@@ -7,7 +7,8 @@ import { ImageMenuItemDelete } from 'features/gallery/components/ImageContextMen
import { ImageMenuItemDownload } from 'features/gallery/components/ImageContextMenu/ImageMenuItemDownload';
import { ImageMenuItemLoadWorkflow } from 'features/gallery/components/ImageContextMenu/ImageMenuItemLoadWorkflow';
import { ImageMenuItemMetadataRecallActions } from 'features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActions';
import { ImageMenuItemNewFromImageSubMenu } from 'features/gallery/components/ImageContextMenu/ImageMenuItemNewFromImageSubMenu';
import { ImageMenuItemNewCanvasFromImageSubMenu } from 'features/gallery/components/ImageContextMenu/ImageMenuItemNewCanvasFromImageSubMenu';
import { ImageMenuItemNewLayerFromImageSubMenu } from 'features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu';
import { ImageMenuItemOpenInNewTab } from 'features/gallery/components/ImageContextMenu/ImageMenuItemOpenInNewTab';
import { ImageMenuItemOpenInViewer } from 'features/gallery/components/ImageContextMenu/ImageMenuItemOpenInViewer';
import { ImageMenuItemSelectForCompare } from 'features/gallery/components/ImageContextMenu/ImageMenuItemSelectForCompare';
@@ -38,7 +39,8 @@ const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) =
<MenuDivider />
<ImageMenuItemSendToUpscale />
<CanvasManagerProviderGate>
<ImageMenuItemNewFromImageSubMenu />
<ImageMenuItemNewCanvasFromImageSubMenu />
<ImageMenuItemNewLayerFromImageSubMenu />
</CanvasManagerProviderGate>
<MenuDivider />
<ImageMenuItemChangeBoard />

View File

@@ -1,3 +1,4 @@
import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { MetadataItem } from 'features/metadata/components/MetadataItem';
import { MetadataLoRAs } from 'features/metadata/components/MetadataLoRAs';
@@ -18,7 +19,7 @@ const ImageMetadataActions = (props: Props) => {
}
return (
<>
<Flex flexDir="column" pl={8}>
<MetadataItem metadata={metadata} handlers={handlers.generationMode} />
<MetadataItem metadata={metadata} handlers={handlers.positivePrompt} direction="column" />
<MetadataItem metadata={metadata} handlers={handlers.negativePrompt} direction="column" />
@@ -48,7 +49,7 @@ const ImageMetadataActions = (props: Props) => {
<MetadataItem metadata={metadata} handlers={handlers.refinerStart} />
<MetadataItem metadata={metadata} handlers={handlers.refinerSteps} />
<MetadataLoRAs metadata={metadata} />
</>
</Flex>
);
};

View File

@@ -1,3 +1,4 @@
import { logger } from 'app/logging/logger';
import type { AppDispatch, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
import { selectDefaultIPAdapter } from 'features/controlLayers/hooks/addLayerHooks';
@@ -29,15 +30,19 @@ import { imageDTOToImageObject, imageDTOToImageWithDims, initialControlNet } fro
import { calculateNewSize } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
import type { BoardId } from 'features/gallery/store/types';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import type { FieldIdentifier } from 'features/nodes/types/field';
import { fieldImageCollectionValueChanged, fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { selectFieldInputInstance, selectNodesSlice } from 'features/nodes/store/selectors';
import { type FieldIdentifier, isImageFieldCollectionInputInstance } from 'features/nodes/types/field';
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
import { uniqBy } from 'lodash-es';
import { imagesApi } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
const log = logger('system');
export const setGlobalReferenceImage = (arg: {
imageDTO: ImageDTO;
entityIdentifier: CanvasEntityIdentifier<'reference_image'>;
@@ -64,11 +69,59 @@ export const setUpscaleInitialImage = (arg: { imageDTO: ImageDTO; dispatch: AppD
export const setNodeImageFieldImage = (arg: {
imageDTO: ImageDTO;
fieldIdentifer: FieldIdentifier;
fieldIdentifier: FieldIdentifier;
dispatch: AppDispatch;
}) => {
const { imageDTO, fieldIdentifer, dispatch } = arg;
dispatch(fieldImageValueChanged({ ...fieldIdentifer, value: imageDTO }));
const { imageDTO, fieldIdentifier, dispatch } = arg;
dispatch(fieldImageValueChanged({ ...fieldIdentifier, value: imageDTO }));
};
export const addImagesToNodeImageFieldCollectionAction = (arg: {
imageDTOs: ImageDTO[];
fieldIdentifier: FieldIdentifier;
dispatch: AppDispatch;
getState: () => RootState;
}) => {
const { imageDTOs, fieldIdentifier, dispatch, getState } = arg;
const fieldInputInstance = selectFieldInputInstance(
selectNodesSlice(getState()),
fieldIdentifier.nodeId,
fieldIdentifier.fieldName
);
if (!isImageFieldCollectionInputInstance(fieldInputInstance)) {
log.warn({ fieldIdentifier }, 'Attempted to add images to a non-image field collection');
return;
}
const images = fieldInputInstance.value ? [...fieldInputInstance.value] : [];
images.push(...imageDTOs.map(({ image_name }) => ({ image_name })));
const uniqueImages = uniqBy(images, 'image_name');
dispatch(fieldImageCollectionValueChanged({ ...fieldIdentifier, value: uniqueImages }));
};
export const removeImageFromNodeImageFieldCollectionAction = (arg: {
imageName: string;
fieldIdentifier: FieldIdentifier;
dispatch: AppDispatch;
getState: () => RootState;
}) => {
const { imageName, fieldIdentifier, dispatch, getState } = arg;
const fieldInputInstance = selectFieldInputInstance(
selectNodesSlice(getState()),
fieldIdentifier.nodeId,
fieldIdentifier.fieldName
);
if (!isImageFieldCollectionInputInstance(fieldInputInstance)) {
log.warn({ fieldIdentifier }, 'Attempted to remove image from a non-image field collection');
return;
}
const images = fieldInputInstance.value ? [...fieldInputInstance.value] : [];
const imagesWithoutTheImageToRemove = images.filter((image) => image.image_name !== imageName);
const uniqueImages = uniqBy(imagesWithoutTheImageToRemove, 'image_name');
dispatch(fieldImageCollectionValueChanged({ ...fieldIdentifier, value: uniqueImages }));
};
export const setComparisonImage = (arg: { imageDTO: ImageDTO; dispatch: AppDispatch }) => {
@@ -129,21 +182,24 @@ export const createNewCanvasEntityFromImage = (arg: {
};
/**
* Creates a new canvas with the given image as the initial image, replicating the img2img flow:
* Creates a new canvas with the given image as the only layer:
* - Reset the canvas
* - Resize the bbox to the image's aspect ratio at the optimal size for the selected model
* - Add the image as a raster layer
* - Resizes the layer to fit the bbox using the 'fill' strategy
* - Add the image as a layer of the given type
* - If `withResize`: Resizes the layer to fit the bbox using the 'fill' strategy
*
* This allows the user to immediately generate a new image from the given image without any additional steps.
*
* Using 'raster_layer' for the type and enabling `withResize` replicates the common img2img flow.
*/
export const newCanvasFromImage = (arg: {
imageDTO: ImageDTO;
type: CanvasEntityType | 'regional_guidance_with_reference_image';
withResize: boolean;
dispatch: AppDispatch;
getState: () => RootState;
}) => {
const { type, imageDTO, dispatch, getState } = arg;
const { type, imageDTO, withResize, dispatch, getState } = arg;
const state = getState();
const base = selectBboxModelBase(state);
@@ -176,7 +232,9 @@ export const newCanvasFromImage = (arg: {
objects: [imageObject],
position: { x, y },
} satisfies Partial<CanvasRasterLayerState>;
addInitCallback(overrides.id);
if (withResize) {
addInitCallback(overrides.id);
}
dispatch(canvasReset());
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
@@ -190,7 +248,9 @@ export const newCanvasFromImage = (arg: {
position: { x, y },
controlAdapter: deepClone(initialControlNet),
} satisfies Partial<CanvasControlLayerState>;
addInitCallback(overrides.id);
if (withResize) {
addInitCallback(overrides.id);
}
dispatch(canvasReset());
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
@@ -203,7 +263,9 @@ export const newCanvasFromImage = (arg: {
objects: [imageObject],
position: { x, y },
} satisfies Partial<CanvasInpaintMaskState>;
addInitCallback(overrides.id);
if (withResize) {
addInitCallback(overrides.id);
}
dispatch(canvasReset());
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
@@ -216,7 +278,9 @@ export const newCanvasFromImage = (arg: {
objects: [imageObject],
position: { x, y },
} satisfies Partial<CanvasRegionalGuidanceState>;
addInitCallback(overrides.id);
if (withResize) {
addInitCallback(overrides.id);
}
dispatch(canvasReset());
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));

View File

@@ -40,7 +40,7 @@ import { computed } from 'nanostores';
import type { ChangeEvent } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCircuitryBold, PiFlaskBold, PiHammerBold } from 'react-icons/pi';
import { PiCircuitryBold, PiFlaskBold, PiHammerBold, PiLightningFill } from 'react-icons/pi';
import type { EdgeChange, NodeChange } from 'reactflow';
import type { S } from 'services/api/types';
@@ -403,7 +403,7 @@ const NodeCommandList = memo(({ searchTerm, onSelect }: { searchTerm: string; on
}
return _items;
}, [pendingConnection, currentImageFilterItem, searchTerm, notesFilterItem, templatesArray]);
}, [pendingConnection, templatesArray, searchTerm, currentImageFilterItem, notesFilterItem]);
return (
<>
@@ -414,6 +414,7 @@ const NodeCommandList = memo(({ searchTerm, onSelect }: { searchTerm: string; on
{item.classification === 'beta' && <Icon boxSize={4} color="invokeYellow.300" as={PiHammerBold} />}
{item.classification === 'prototype' && <Icon boxSize={4} color="invokeRed.300" as={PiFlaskBold} />}
{item.classification === 'internal' && <Icon boxSize={4} color="invokePurple.300" as={PiCircuitryBold} />}
{item.classification === 'special' && <Icon boxSize={4} color="invokeGreen.300" as={PiLightningFill} />}
<Text fontWeight="semibold">{item.label}</Text>
<Spacer />
<Text variant="subtext" fontWeight="semibold">

View File

@@ -3,7 +3,7 @@ import { useNodeClassification } from 'features/nodes/hooks/useNodeClassificatio
import type { Classification } from 'features/nodes/types/common';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCircuitryBold, PiFlaskBold, PiHammerBold } from 'react-icons/pi';
import { PiCircuitryBold, PiFlaskBold, PiHammerBold, PiLightningFill } from 'react-icons/pi';
interface Props {
nodeId: string;
@@ -41,7 +41,11 @@ const ClassificationTooltipContent = memo(({ classification }: { classification:
}
if (classification === 'internal') {
return t('nodes.prototypeDesc');
return t('nodes.internalDesc');
}
if (classification === 'special') {
return t('nodes.specialDesc');
}
return null;
@@ -62,5 +66,9 @@ const ClassificationIcon = ({ classification }: { classification: Classification
return <Icon as={PiCircuitryBold} display="block" boxSize={4} color="invokePurple.300" />;
}
if (classification === 'special') {
return <Icon as={PiLightningFill} display="block" boxSize={4} color="invokeGreen.300" />;
}
return null;
};

View File

@@ -1,9 +1,9 @@
import { Flex, Icon, Text, Tooltip } from '@invoke-ai/ui-library';
import { compare } from 'compare-versions';
import { useNodeData } from 'features/nodes/hooks/useNodeData';
import { useNode } from 'features/nodes/hooks/useNode';
import { useNodeNeedsUpdate } from 'features/nodes/hooks/useNodeNeedsUpdate';
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
import { isInvocationNodeData } from 'features/nodes/types/invocation';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiInfoBold } from 'react-icons/pi';
@@ -25,32 +25,32 @@ const InvocationNodeInfoIcon = ({ nodeId }: Props) => {
export default memo(InvocationNodeInfoIcon);
const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
const data = useNodeData(nodeId);
const node = useNode(nodeId);
const nodeTemplate = useNodeTemplate(nodeId);
const { t } = useTranslation();
const title = useMemo(() => {
if (data?.label && nodeTemplate?.title) {
return `${data.label} (${nodeTemplate.title})`;
if (node.data?.label && nodeTemplate?.title) {
return `${node.data.label} (${nodeTemplate.title})`;
}
if (data?.label && !nodeTemplate) {
return data.label;
if (node.data?.label && !nodeTemplate) {
return node.data.label;
}
if (!data?.label && nodeTemplate) {
if (!node.data?.label && nodeTemplate) {
return nodeTemplate.title;
}
return t('nodes.unknownNode');
}, [data, nodeTemplate, t]);
}, [node.data.label, nodeTemplate, t]);
const versionComponent = useMemo(() => {
if (!isInvocationNodeData(data) || !nodeTemplate) {
if (!isInvocationNode(node) || !nodeTemplate) {
return null;
}
if (!data.version) {
if (!node.data.version) {
return (
<Text as="span" color="error.500">
{t('nodes.versionUnknown')}
@@ -61,35 +61,35 @@ const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
if (!nodeTemplate.version) {
return (
<Text as="span" color="error.500">
{t('nodes.version')} {data.version} ({t('nodes.unknownTemplate')})
{t('nodes.version')} {node.data.version} ({t('nodes.unknownTemplate')})
</Text>
);
}
if (compare(data.version, nodeTemplate.version, '<')) {
if (compare(node.data.version, nodeTemplate.version, '<')) {
return (
<Text as="span" color="error.500">
{t('nodes.version')} {data.version} ({t('nodes.updateNode')})
{t('nodes.version')} {node.data.version} ({t('nodes.updateNode')})
</Text>
);
}
if (compare(data.version, nodeTemplate.version, '>')) {
if (compare(node.data.version, nodeTemplate.version, '>')) {
return (
<Text as="span" color="error.500">
{t('nodes.version')} {data.version} ({t('nodes.updateApp')})
{t('nodes.version')} {node.data.version} ({t('nodes.updateApp')})
</Text>
);
}
return (
<Text as="span">
{t('nodes.version')} {data.version}
{t('nodes.version')} {node.data.version}
</Text>
);
}, [data, nodeTemplate, t]);
}, [node, nodeTemplate, t]);
if (!isInvocationNodeData(data)) {
if (!isInvocationNode(node)) {
return <Text fontWeight="semibold">{t('nodes.unknownNode')}</Text>;
}
@@ -107,7 +107,7 @@ const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
{nodeTemplate?.description}
</Text>
{versionComponent}
{data?.notes && <Text>{data.notes}</Text>}
{node.data?.notes && <Text>{node.data.notes}</Text>}
</Flex>
);
});

View File

@@ -1,15 +1,15 @@
import { FormControl, FormLabel, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useNodeData } from 'features/nodes/hooks/useNodeData';
import { useNode } from 'features/nodes/hooks/useNode';
import { nodeNotesChanged } from 'features/nodes/store/nodesSlice';
import { isInvocationNodeData } from 'features/nodes/types/invocation';
import { isInvocationNode } from 'features/nodes/types/invocation';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const NotesTextarea = ({ nodeId }: { nodeId: string }) => {
const dispatch = useAppDispatch();
const data = useNodeData(nodeId);
const node = useNode(nodeId);
const { t } = useTranslation();
const handleNotesChanged = useCallback(
(e: ChangeEvent<HTMLTextAreaElement>) => {
@@ -17,13 +17,13 @@ const NotesTextarea = ({ nodeId }: { nodeId: string }) => {
},
[dispatch, nodeId]
);
if (!isInvocationNodeData(data)) {
if (!isInvocationNode(node)) {
return null;
}
return (
<FormControl orientation="vertical" h="full">
<FormLabel>{t('nodes.notes')}</FormLabel>
<Textarea value={data?.notes} onChange={handleNotesChanged} rows={10} resize="none" />
<Textarea value={node.data?.notes} onChange={handleNotesChanged} rows={10} resize="none" />
</FormControl>
);
};

View File

@@ -23,13 +23,13 @@ interface Props {
nodeId: string;
fieldName: string;
kind: 'inputs' | 'outputs';
isMissingInput?: boolean;
isInvalid?: boolean;
withTooltip?: boolean;
shouldDim?: boolean;
}
const EditableFieldTitle = forwardRef((props: Props, ref) => {
const { nodeId, fieldName, kind, isMissingInput = false, withTooltip = false, shouldDim = false } = props;
const { nodeId, fieldName, kind, isInvalid = false, withTooltip = false, shouldDim = false } = props;
const label = useFieldLabel(nodeId, fieldName);
const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind);
const { t } = useTranslation();
@@ -78,7 +78,7 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
fontWeight="semibold"
sx={editablePreviewStyles}
noOfLines={1}
color={isMissingInput ? 'error.300' : 'base.300'}
color={isInvalid ? 'error.300' : 'base.300'}
opacity={shouldDim ? 0.5 : 1}
/>
</Tooltip>

View File

@@ -0,0 +1,42 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
import { useFieldValue } from 'features/nodes/hooks/useFieldValue';
import { fieldValueReset } from 'features/nodes/store/nodesSlice';
import { isEqual } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
type Props = {
nodeId: string;
fieldName: string;
};
const FieldResetToDefaultValueButton = ({ nodeId, fieldName }: Props) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const value = useFieldValue(nodeId, fieldName);
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
const isDisabled = useMemo(() => {
return isEqual(value, fieldTemplate.default);
}, [value, fieldTemplate.default]);
const onClick = useCallback(() => {
dispatch(fieldValueReset({ nodeId, fieldName, value: fieldTemplate.default }));
}, [dispatch, fieldName, fieldTemplate.default, nodeId]);
return (
<IconButton
variant="ghost"
tooltip={t('nodes.resetToDefaultValue')}
aria-label={t('nodes.resetToDefaultValue')}
icon={<PiArrowCounterClockwiseBold />}
onClick={onClick}
isDisabled={isDisabled}
pointerEvents="auto"
size="xs"
/>
);
};
export default memo(FieldResetToDefaultValueButton);

View File

@@ -1,8 +1,9 @@
import { Flex, FormControl } from '@invoke-ai/ui-library';
import FieldResetToDefaultValueButton from 'features/nodes/components/flow/nodes/Invocation/fields/FieldResetToDefaultValueButton';
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
import { useDoesInputHaveValue } from 'features/nodes/hooks/useDoesInputHaveValue';
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
import { memo, useCallback, useMemo, useState } from 'react';
import { useFieldIsInvalid } from 'features/nodes/hooks/useFieldIsInvalid';
import { memo, useCallback, useState } from 'react';
import EditableFieldTitle from './EditableFieldTitle';
import FieldHandle from './FieldHandle';
@@ -17,32 +18,12 @@ interface Props {
const InputField = ({ nodeId, fieldName }: Props) => {
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName);
const [isHovered, setIsHovered] = useState(false);
const isInvalid = useFieldIsInvalid(nodeId, fieldName);
const { isConnected, isConnectionInProgress, isConnectionStartField, validationResult, shouldDim } =
useConnectionState({ nodeId, fieldName, kind: 'inputs' });
const isMissingInput = useMemo(() => {
if (!fieldTemplate) {
return false;
}
if (!fieldTemplate.required) {
return false;
}
if (!isConnected && fieldTemplate.input === 'connection') {
return true;
}
if (!doesFieldHaveValue && !isConnected && fieldTemplate.input !== 'connection') {
return true;
}
return false;
}, [fieldTemplate, isConnected, doesFieldHaveValue]);
const onMouseEnter = useCallback(() => {
setIsHovered(true);
}, []);
@@ -54,12 +35,12 @@ const InputField = ({ nodeId, fieldName }: Props) => {
if (fieldTemplate.input === 'connection' || isConnected) {
return (
<InputFieldWrapper shouldDim={shouldDim}>
<FormControl isInvalid={isMissingInput} isDisabled={isConnected} px={2}>
<FormControl isInvalid={isInvalid} isDisabled={isConnected} px={2}>
<EditableFieldTitle
nodeId={nodeId}
fieldName={fieldName}
kind="inputs"
isMissingInput={isMissingInput}
isInvalid={isInvalid}
withTooltip
shouldDim
/>
@@ -79,7 +60,7 @@ const InputField = ({ nodeId, fieldName }: Props) => {
return (
<InputFieldWrapper shouldDim={shouldDim}>
<FormControl
isInvalid={isMissingInput}
isInvalid={isInvalid}
isDisabled={isConnected}
// Without pointerEvents prop, disabled inputs don't trigger reactflow events. For example, when making a
// connection, the mouse up to end the connection won't fire, leaving the connection in-progress.
@@ -88,14 +69,9 @@ const InputField = ({ nodeId, fieldName }: Props) => {
px={2}
>
<Flex flexDir="column" w="full" gap={1} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<Flex>
<EditableFieldTitle
nodeId={nodeId}
fieldName={fieldName}
kind="inputs"
isMissingInput={isMissingInput}
withTooltip
/>
<Flex gap={1}>
<EditableFieldTitle nodeId={nodeId} fieldName={fieldName} kind="inputs" isInvalid={isInvalid} withTooltip />
{isHovered && <FieldResetToDefaultValueButton nodeId={nodeId} fieldName={fieldName} />}
{isHovered && <FieldLinearViewToggle nodeId={nodeId} fieldName={fieldName} />}
</Flex>
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />

View File

@@ -1,3 +1,4 @@
import { ImageFieldCollectionInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldCollectionInputComponent';
import ModelIdentifierFieldInputComponent from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelIdentifierFieldInputComponent';
import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance';
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
@@ -24,6 +25,8 @@ import {
isFluxMainModelFieldInputTemplate,
isFluxVAEModelFieldInputInstance,
isFluxVAEModelFieldInputTemplate,
isImageFieldCollectionInputInstance,
isImageFieldCollectionInputTemplate,
isImageFieldInputInstance,
isImageFieldInputTemplate,
isIntegerFieldInputInstance,
@@ -110,6 +113,10 @@ const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => {
return <EnumFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
}
if (isImageFieldCollectionInputInstance(fieldInstance) && isImageFieldCollectionInputTemplate(fieldTemplate)) {
return <ImageFieldCollectionInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
}
if (isImageFieldInputInstance(fieldInstance) && isImageFieldInputTemplate(fieldTemplate)) {
return <ImageFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
}

View File

@@ -0,0 +1,167 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Flex, Grid, GridItem } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { IAINoContentFallback, IAINoContentFallbackWithSpinner } from 'common/components/IAIImageFallback';
import { getOverlayScrollbarsParams, overlayScrollbarsStyles } from 'common/components/OverlayScrollbars/constants';
import { UploadMultipleImageButton } from 'common/hooks/useImageUploadButton';
import { TRANSPARENCY_CHECKERBOARD_PATTERN_DARK_DATAURL } from 'features/controlLayers/konva/patterns/transparency-checkerboard-pattern';
import type { AddImagesToNodeImageFieldCollection } from 'features/dnd/dnd';
import { addImagesToNodeImageFieldCollectionDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { DndImage } from 'features/dnd/DndImage';
import { DndImageIcon } from 'features/dnd/DndImageIcon';
import { removeImageFromNodeImageFieldCollectionAction } from 'features/imageActions/actions';
import { useFieldIsInvalid } from 'features/nodes/hooks/useFieldIsInvalid';
import { fieldImageCollectionValueChanged } from 'features/nodes/store/nodesSlice';
import type { ImageFieldCollectionInputInstance, ImageFieldCollectionInputTemplate } from 'features/nodes/types/field';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiExclamationMarkBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
import type { FieldComponentProps } from './types';
const overlayscrollbarsOptions = getOverlayScrollbarsParams().options;
const sx = {
borderWidth: 1,
'&[data-error=true]': {
borderColor: 'error.500',
borderStyle: 'solid',
},
} satisfies SystemStyleObject;
export const ImageFieldCollectionInputComponent = memo(
(props: FieldComponentProps<ImageFieldCollectionInputInstance, ImageFieldCollectionInputTemplate>) => {
const { t } = useTranslation();
const { nodeId, field } = props;
const store = useAppStore();
const isInvalid = useFieldIsInvalid(nodeId, field.name);
const dndTargetData = useMemo<AddImagesToNodeImageFieldCollection>(
() =>
addImagesToNodeImageFieldCollectionDndTarget.getData({ fieldIdentifier: { nodeId, fieldName: field.name } }),
[field, nodeId]
);
const onUpload = useCallback(
(imageDTOs: ImageDTO[]) => {
store.dispatch(
fieldImageCollectionValueChanged({
nodeId,
fieldName: field.name,
value: imageDTOs,
})
);
},
[store, nodeId, field.name]
);
const onRemoveImage = useCallback(
(imageName: string) => {
removeImageFromNodeImageFieldCollectionAction({
imageName,
fieldIdentifier: { nodeId, fieldName: field.name },
dispatch: store.dispatch,
getState: store.getState,
});
},
[field.name, nodeId, store.dispatch, store.getState]
);
return (
<Flex
className="nodrag"
position="relative"
w="full"
h="full"
minH={16}
maxH={64}
alignItems="stretch"
justifyContent="center"
>
{(!field.value || field.value.length === 0) && (
<UploadMultipleImageButton
w="full"
h="auto"
isError={isInvalid}
onUpload={onUpload}
fontSize={24}
variant="outline"
/>
)}
{field.value && field.value.length > 0 && (
<Box w="full" h="auto" p={1} sx={sx} data-error={isInvalid} borderRadius="base">
<OverlayScrollbarsComponent
className="nowheel"
defer
style={overlayScrollbarsStyles}
options={overlayscrollbarsOptions}
>
<Grid w="full" h="full" templateColumns="repeat(4, 1fr)" gap={1}>
{field.value.map(({ image_name }) => (
<GridItem key={image_name} position="relative" className="nodrag">
<ImageGridItemContent imageName={image_name} onRemoveImage={onRemoveImage} />
</GridItem>
))}
</Grid>
</OverlayScrollbarsComponent>
</Box>
)}
<DndDropTarget
dndTarget={addImagesToNodeImageFieldCollectionDndTarget}
dndTargetData={dndTargetData}
label={t('gallery.drop')}
/>
</Flex>
);
}
);
ImageFieldCollectionInputComponent.displayName = 'ImageFieldCollectionInputComponent';
const ImageGridItemContent = memo(
({ imageName, onRemoveImage }: { imageName: string; onRemoveImage: (imageName: string) => void }) => {
const query = useGetImageDTOQuery(imageName);
const onClickRemove = useCallback(() => {
onRemoveImage(imageName);
}, [imageName, onRemoveImage]);
if (query.isLoading) {
return <IAINoContentFallbackWithSpinner />;
}
if (!query.data) {
return <IAINoContentFallback icon={<PiExclamationMarkBold />} />;
}
return (
<>
<DndImage
imageDTO={query.data}
asThumbnail
objectFit="contain"
w="full"
h="full"
aspectRatio="1/1"
backgroundSize={8}
backgroundImage={TRANSPARENCY_CHECKERBOARD_PATTERN_DARK_DATAURL}
/>
<DndImageIcon
onClick={onClickRemove}
icon={<PiArrowCounterClockwiseBold />}
tooltip="Reset Image"
position="absolute"
flexDir="column"
top={1}
insetInlineEnd={1}
gap={1}
/>
</>
);
}
);
ImageGridItemContent.displayName = 'ImageGridItemContent';

View File

@@ -38,7 +38,7 @@ const ImageFieldInputComponent = (props: FieldComponentProps<ImageFieldInputInst
const dndTargetData = useMemo<SetNodeImageFieldImageDndTargetData>(
() =>
setNodeImageFieldImageDndTarget.getData(
{ fieldIdentifer: { nodeId, fieldName: field.name } },
{ fieldIdentifier: { nodeId, fieldName: field.name } },
field.value?.image_name
),
[field, nodeId]
@@ -85,13 +85,16 @@ const ImageFieldInputComponent = (props: FieldComponentProps<ImageFieldInputInst
{imageDTO && (
<>
<DndImage imageDTO={imageDTO} minW={8} minH={8} />
<Flex position="absolute" flexDir="column" top={1} insetInlineEnd={1} gap={1}>
<DndImageIcon
onClick={handleReset}
icon={imageDTO ? <PiArrowCounterClockwiseBold /> : undefined}
tooltip="Reset Image"
/>
</Flex>
<DndImageIcon
onClick={handleReset}
icon={imageDTO ? <PiArrowCounterClockwiseBold /> : undefined}
tooltip="Reset Image"
position="absolute"
flexDir="column"
top={1}
insetInlineEnd={1}
gap={1}
/>
</>
)}
<DndDropTarget

View File

@@ -1,22 +0,0 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodeData, selectNodesSlice } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useDoesInputHaveValue = (nodeId: string, fieldName: string): boolean => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, (nodes) => {
const data = selectNodeData(nodes, nodeId);
if (!data) {
return false;
}
return data.inputs[fieldName]?.value !== undefined;
}),
[fieldName, nodeId]
);
const doesFieldHaveValue = useAppSelector(selector);
return doesFieldHaveValue;
};

View File

@@ -0,0 +1,57 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
import { selectFieldInputInstance, selectNodesSlice } from 'features/nodes/store/selectors';
import { isImageFieldCollectionInputInstance, isImageFieldCollectionInputTemplate } from 'features/nodes/types/field';
import { useMemo } from 'react';
export const useFieldIsInvalid = (nodeId: string, fieldName: string) => {
const template = useFieldInputTemplate(nodeId, fieldName);
const connectionState = useConnectionState({ nodeId, fieldName, kind: 'inputs' });
const selectIsInvalid = useMemo(() => {
return createSelector(selectNodesSlice, (nodes) => {
const field = selectFieldInputInstance(nodes, nodeId, fieldName);
// No field instance is a problem - should not happen
if (!field) {
return true;
}
// 'connection' input fields have no data validation - only connection validation
if (template.input === 'connection') {
return template.required && !connectionState.isConnected;
}
// 'any' input fields are valid if they are connected
if (template.input === 'any' && connectionState.isConnected) {
return false;
}
// If there is no valid for the field & the field is required, it is invalid
if (field.value === undefined) {
return template.required;
}
// Else special handling for individual field types
if (isImageFieldCollectionInputInstance(field) && isImageFieldCollectionInputTemplate(template)) {
// Image collections may have min or max item counts
if (template.minItems !== undefined && field.value.length < template.minItems) {
return true;
}
if (template.maxItems !== undefined && field.value.length > template.maxItems) {
return true;
}
}
// Field looks OK
return false;
});
}, [connectionState.isConnected, fieldName, nodeId, template]);
const isInvalid = useAppSelector(selectIsInvalid);
return isInvalid;
};

View File

@@ -0,0 +1,19 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNode, selectNodesSlice } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
import type { Node } from 'reactflow';
export const useNode = (nodeId: string): Node => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, (nodes) => {
return selectNode(nodes, nodeId);
}),
[nodeId]
);
const node = useAppSelector(selector);
return node;
};

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