Compare commits

...

90 Commits

Author SHA1 Message Date
Mary Hipp
981177e934 lint 2024-01-29 10:31:39 -05:00
Mary Hipp
aad486e59e try using fns instead; 2024-01-29 10:27:47 -05:00
Ufuk Sarp Selçok
7fde19730e translationBot(ui): update translation (Turkish)
Currently translated at 22.8% (326 of 1426 strings)

Co-authored-by: Ufuk Sarp Selçok <ilkel@live.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/tr/
Translation: InvokeAI/Web UI
2024-01-29 14:15:29 +11:00
psychedelicious
13575642d8 chore: update issue template
- Improve spelling and grammar
- Add browser, GPU model, python deps fields
- Revise other fields
2024-01-29 14:11:00 +11:00
psychedelicious
3f5370b284 feat(ui): add a copy button to the about modal
This copies the dependencies as JSON.
2024-01-28 20:50:08 -06:00
psychedelicious
d048eb5b20 docs(ui): add STATE_MGMT.md
Supersedes the mini nanostores doc.
2024-01-29 07:28:20 +11:00
psychedelicious
dd7031a472 docs(ui): update README.md
Also moved it to the frontend package root
2024-01-29 07:28:20 +11:00
Millun Atluri
4160d5ef26 update contributors list to bring into sync with discord roles (#5586)
## What type of PR is this? (check all applicable)

- [ ] Refactor
- [ ] Feature
- [ ] Bug Fix
- [ ] Optimization
- [X] Documentation Update
- [ ] Community Node Submission


## Have you discussed this change with the InvokeAI team?
- [X] Yes
- [ ] No, because:

      
## Have you updated all relevant documentation?
- [X] Yes
- [ ] No


## Description

This brings `docs/other/CONTRIBUTORS.md` into sync with collaborator
roles in Discord as of January 27, 2024.

## Related Tickets & Documents

<!--
For pull requests that relate or close an issue, please include them
below. 

For example having the text: "closes #1234" would connect the current
pull
request to issue 1234.  And when we merge the pull request, Github will
automatically close the issue.
-->

## QA Instructions, Screenshots, Recordings

N/A

<!-- 
Please provide steps on how to test changes, any hardware or 
software specifications as well as any other pertinent information. 
-->

## Merge Plan

Merge when approved.

<!--
A merge plan describes how this PR should be handled after it is
approved.

Example merge plans:
- "This PR can be merged when approved"
- "This must be squash-merged when approved"
- "DO NOT MERGE - I will rebase and tidy commits before merging"
- "#dev-chat on discord needs to be advised of this change when it is
merged"

A merge plan is particularly important for large PRs or PRs that touch
the
database in any way.
-->

## Added/updated tests?

- [ ] Yes
- [ ] No : _please replace this line with details on why tests
      have not been included_

## [optional] Are there any post deployment tasks we need to perform?
2024-01-28 11:28:22 -05:00
Millun Atluri
51bdf2fd19 Merge branch 'main' into docs/update-contributors 2024-01-28 11:26:35 -05:00
Ufuk Sarp Selçok
6a44697911 translationBot(ui): update translation (Turkish)
Currently translated at 10.5% (151 of 1426 strings)

translationBot(ui): update translation (Turkish)

Currently translated at 8.1% (116 of 1426 strings)

translationBot(ui): update translation (Turkish)

Currently translated at 6.6% (95 of 1426 strings)

Co-authored-by: Ufuk Sarp Selçok <ilkel@live.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/tr/
Translation: InvokeAI/Web UI
2024-01-28 22:27:25 +11:00
Hosted Weblate
7a1d0ec228 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-01-28 22:27:25 +11:00
Riccardo Giovanetti
b5928fd411 translationBot(ui): update translation (Italian)
Currently translated at 97.2% (1387 of 1426 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-01-28 22:27:25 +11:00
psychedelicious
2f345d1976 chore(ui): lint 2024-01-28 19:57:53 +11:00
psychedelicious
f5d0721fa8 chore(ui): bump @invoke-ai/eslint-config-react 2024-01-28 19:57:53 +11:00
psychedelicious
c3b36cb61d chore(ui): remove chakra CLI
It doesn't work now that the theme is external. I'm not sure how to fix it and not sure if it really did much (I don't think I ever got autocomplete...). Maybe it can be implemented in `@invoke-ai/ui-library`.
2024-01-28 19:57:53 +11:00
psychedelicious
189c430e46 chore(ui): format
Lots of changed bc the line length is now 120. May as well do it now.
2024-01-28 19:57:53 +11:00
psychedelicious
b922ee566a chore(ui): use new prettier config 2024-01-28 19:57:53 +11:00
psychedelicious
89da69f647 fix(ui): correct import in ReduxInit 2024-01-28 19:57:53 +11:00
psychedelicious
138caa34de chore(ui): lint 2024-01-28 19:57:53 +11:00
psychedelicious
26c3378ede chore(ui): use new eslint config, add some overrides 2024-01-28 19:57:53 +11:00
psychedelicious
aa134a2db8 chore(ui): remove postinstall script 2024-01-28 19:57:53 +11:00
Mary Hipp
d0391cb430 chore(ui): bump @invoke-ai/ui-library, add @invoke-ai/eslint-config-react & @invoke-ai/prettier-config-react 2024-01-28 19:57:53 +11:00
Millun Atluri
c955ea9de0 Update CONTRIBUTORS.md 2024-01-27 17:04:32 -05:00
Lincoln Stein
fc29a5d439 update contributors list to bring into sync with discord roles 2024-01-27 16:59:56 -05:00
Millun Atluri
7e9942dbab {fix} install docs house keeping (#5583)
## What type of PR is this? (check all applicable)

- [ ] Refactor
- [ ] Feature
- [ ] Bug Fix
- [ ] Optimization
- [X] Documentation Update
- [ ] Community Node Submission


## Have you discussed this change with the InvokeAI team?
- [X] Yes
- [ ] No, because:

      
## Have you updated all relevant documentation?
- [X] Yes
- [ ] No


## Description
- Update docs to make link to automated installer easier to find
- Fixed issue in SDXL + refiner example workflow 

## Related Tickets & Documents

<!--
For pull requests that relate or close an issue, please include them
below. 

For example having the text: "closes #1234" would connect the current
pull
request to issue 1234.  And when we merge the pull request, Github will
automatically close the issue.
-->

- Related Issue #
- Closes #

## QA Instructions, Screenshots, Recordings
Read over docs changes
<!-- 
Please provide steps on how to test changes, any hardware or 
software specifications as well as any other pertinent information. 
-->

## Merge Plan
Merge when approved
<!--
A merge plan describes how this PR should be handled after it is
approved.

Example merge plans:
- "This PR can be merged when approved"
- "This must be squash-merged when approved"
- "DO NOT MERGE - I will rebase and tidy commits before merging"
- "#dev-chat on discord needs to be advised of this change when it is
merged"

A merge plan is particularly important for large PRs or PRs that touch
the
database in any way.
-->

## [optional] Are there any post deployment tasks we need to perform?
Deploy new docs
2024-01-27 12:10:47 -05:00
Millun Atluri
c003967eaa Merge branch 'main' into feat/install_docs_update 2024-01-27 11:55:19 -05:00
Mary Hipp
b28fcc6be5 lint 2024-01-27 21:36:42 +11:00
Mary Hipp
418cdbabb7 add option for workflowCategories 2024-01-27 21:36:42 +11:00
Millun Atluri
18e61e92d9 {fix} install docs house keeping 2024-01-26 21:19:48 -06:00
Mary Hipp
de20711637 add nanostore for open API schema 2024-01-27 12:43:47 +11:00
Mary Hipp
55e91b97be dep 2024-01-27 12:43:47 +11:00
Mary Hipp
f79bbd2d6e account for baseUrl 2024-01-27 12:43:47 +11:00
Brandon
e1c2c3905d Github action for ensuring PRs are labeled in a way that makes it eas… (#5543)
…y to distinguish what's being changed

## What type of PR is this? (check all applicable)

- [ ] Refactor
- [ ] Feature
- [ ] Bug Fix
- [x] Optimization
- [ ] Documentation Update
- [ ] Community Node Submission


## Have you discussed this change with the InvokeAI team?
- [x] Yes
- [ ] No, because:

      
## Have you updated all relevant documentation?
- [x] Yes
- [ ] No


## Description


## Related Tickets & Documents

<!--
For pull requests that relate or close an issue, please include them
below. 

For example having the text: "closes #1234" would connect the current
pull
request to issue 1234.  And when we merge the pull request, Github will
automatically close the issue.
-->

- Related Issue #
- Closes #

## QA Instructions, Screenshots, Recordings

<!-- 
Please provide steps on how to test changes, any hardware or 
software specifications as well as any other pertinent information. 
-->

## Merge Plan

<!--
A merge plan describes how this PR should be handled after it is
approved.

Example merge plans:
- "This PR can be merged when approved"
- "This must be squash-merged when approved"
- "DO NOT MERGE - I will rebase and tidy commits before merging"
- "#dev-chat on discord needs to be advised of this change when it is
merged"

A merge plan is particularly important for large PRs or PRs that touch
the
database in any way.
-->

## Added/updated tests?

- [ ] Yes
- [x] No : _please replace this line with details on why tests
      have not been included_

## [optional] Are there any post deployment tasks we need to perform?
2024-01-25 20:37:39 -05:00
Brandon
03ac93bfc7 Merge branch 'main' into pr-labeler 2024-01-25 20:36:12 -05:00
Mary Hipp Rogers
89da976949 workflow library updates (#5568)
* dont show duplicate toasts if workflow actions fail due to auth

* dynamic order by options based on projectId

* add endpointName to authtoast to makeit unique per endpoint

* lint

* update toast logic to check based on endpoint name w type safety

* fix save as endpoit name

* lint

* fix type

---------

Co-authored-by: Mary Hipp <maryhipp@Marys-MacBook-Air.local>
2024-01-25 11:43:47 -05:00
Millun Atluri
57dafd294d {release} v3.6.2
## What type of PR is this? (check all applicable)

Invoke v3.6.2 release


## Have you discussed this change with the InvokeAI team?
- [X] Yes
- [ ] No, because:

      
## Have you updated all relevant documentation?
- [X] Yes
- [ ] No


## Description
Invoke v3.6.2

## Related Tickets & Documents

<!--
For pull requests that relate or close an issue, please include them
below. 

For example having the text: "closes #1234" would connect the current
pull
request to issue 1234.  And when we merge the pull request, Github will
automatically close the issue.
-->

- Related Issue #
- Closes #

## QA Instructions, Screenshots, Recordings

[InvokeAI-installer-v3.6.2.zip](https://github.com/invoke-ai/InvokeAI/files/14046191/InvokeAI-installer-v3.6.2.zip)
2024-01-24 22:05:10 -05:00
Millun Atluri
e611baa4b4 {release} v3.6.2 2024-01-24 21:40:03 -05:00
psychedelicious
fc448d5b6d feat(ui): handle proxy configs rewriting paths
We can't assume that the base URL is `host:port/` - it could be `host:port/some/path/`. Make the path handling dynamic to account for this.
2024-01-25 13:29:56 +11:00
Mary Hipp Rogers
e59954f956 fix workflow updating (#5567)
* retain id through workflow state so that we correctly update or save new

* lint

---------

Co-authored-by: Mary Hipp <maryhipp@Marys-MacBook-Air.local>
2024-01-24 16:10:19 -05:00
Brandon
e160cbb1e9 Merge branch 'main' into pr-labeler 2024-01-24 15:44:35 -05:00
Millun Atluri
86c857b9c2 {release} v3.6.1 (#5564)
## What type of PR is this? (check all applicable)

Invoke 3.6.1 release

## QA Instructions, Screenshots, Recordings

[InvokeAI-installer-v3.6.1.zip](https://github.com/invoke-ai/InvokeAI/files/14041411/InvokeAI-installer-v3.6.1.zip)

<!-- 
Please provide steps on how to test changes, any hardware or 
software specifications as well as any other pertinent information. 
-->

## Merge Plan

This PR can be merged when approved

## [optional] Are there any post deployment tasks we need to perform?
PyPI Release & GitHub Release
2024-01-24 12:31:10 -05:00
Millun Atluri
0a13d7d2c7 {release} v3.6.1 2024-01-24 11:27:36 -05:00
blessedcoolant
68da5c6d22 feat: Add Depth Anything PreProcessor (#5548)
## What type of PR is this? (check all applicable)

- [x] Feature

## Have you discussed this change with the InvokeAI team?
- [x] Yes

## Have you updated all relevant documentation?
- [x] No


## Description

- This adds the newly released Depth Anything to InvokeAI. A new node
`Depth Anything Processor` has been added to generate depth maps using
this new technique. https://depth-anything.github.io

- All related checkpoints will be downloaded automatically on first
boot. The `DinoV2` models will be loaded to your torch cache dir and the
checkpoints pertaining to Depth Anything will be downloaded to
`any/annotators/depth_anything`.

- Alternatively you can find the checkpoints here and download them to
that folder:
https://huggingface.co/spaces/LiheYoung/Depth-Anything/tree/main/checkpoints

- This depth map can be used with any depth ControlNet model out there
but the folks at DepthAnything have also released a custom fine tuned
ControlNet model. From my limited testing, I still prefer the original
depth model because this one seems to be producing weird artifacts. Not
sure if that is a specific problem to Invoke or just the model itself.
I'll test more later. Place these in your controlnet folder like your
other ControlNets. You can get that here:
https://huggingface.co/spaces/LiheYoung/Depth-Anything/tree/main/checkpoints_controlnet

- Also available in the LinearUI

- DepthAnything has three models `large`, `base` and `small` -- I've
defaulted the processor to small but a user can change to the large
model if they wish to do so. Small is way faster but obviously somewhat
of a lesser quality.

- DepthAnything is now the default processor for depth controlnet
models.

## Screenshots


![opera_o3jHnWxVRi](https://github.com/invoke-ai/InvokeAI/assets/54517381/573c66f3-1492-45b0-b6df-25756f5e1d1a)

## Merge Plan

DO NOT MERGE YET. Test it first and I'm sure the model caching can be
done better. Coz I don't think I've done that at all. I would appreciate
if @brandonrising or @lstein or anyone can take a look at that part of
it.
2024-01-24 19:14:34 +05:30
blessedcoolant
f82744b95e fix: linting issues 2024-01-24 18:45:54 +05:30
Kent Keirsey
5a67bc68a1 Merge branch 'main' into depth-anything 2024-01-23 22:31:19 -06:00
Josh Corbett
61cf4d4c70 feat: "Remix Image" option on images (#5553)
* feat:  "Remix Image" option on images

Adds a new "remix image" option where applicable, recalls all metadata except the seed

* refactor: 🚨 lint code

* feat:  "Remix Image" option on images

Adds a new "remix image" option where applicable, recalls all metadata except the seed

* refactor: 🚨 lint code

* feat:  add new remix hotkey to hotkeys modal

---------

Co-authored-by: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
2024-01-24 00:45:30 +00:00
Wubbbi
9d20a2d5a3 Update huggingface deps to their lastest versions 2024-01-24 11:14:21 +11:00
Riccardo Giovanetti
8b0ac451e3 translationBot(ui): update translation (Italian)
Currently translated at 97.3% (1378 of 1415 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-01-24 11:05:17 +11:00
Alexander Eichhorn
470dbe75a2 translationBot(ui): update translation (German)
Currently translated at 60.0% (850 of 1415 strings)

Co-authored-by: Alexander Eichhorn <pfannkuchensack@einfach-doof.de>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/
Translation: InvokeAI/Web UI
2024-01-24 11:05:17 +11:00
maryhipp
b7d19b8130 add project as category to back-end 2024-01-24 10:59:04 +11:00
Mary Hipp
3dc13221d8 add project as a workflow category in the front-end 2024-01-24 10:59:04 +11:00
blessedcoolant
35184dbd86 fix: incorrect local file path 2024-01-24 03:37:16 +05:30
blessedcoolant
0868fc2558 Merge branch 'main' into depth-anything 2024-01-24 03:36:25 +05:30
blessedcoolant
92fb09c4df fix: Move the models to any folder to avoid boot warnings 2024-01-24 03:35:37 +05:30
psychedelicious
b4cf5496b6 fix(ui): handle model names with spaces
Remove `trim()` from model identifier schema, which prevented parsed model identifiers from matching.

The root issue here is that model names are identifiers. This will be resolved in the model manager refactor.

Closes #5556
2024-01-23 15:48:10 -06:00
psychedelicious
a0e68705dd feat(ui): improved dynamic prompts behaviour
- Bump `@invoke-ai/ui` for updated styles
- Update regex to parse prompts with newlines
- Update styling of overlay button when prompt has an error
- Fix bug where loading and error state sometimes weren't cleared
2024-01-23 15:26:12 -06:00
blessedcoolant
7cb49e65bd feat: Add Resolution to DepthAnything 2024-01-23 14:13:50 -06:00
blessedcoolant
39fedb090b feat: Make depth anything the default processor for depth controlnet 2024-01-23 14:13:50 -06:00
blessedcoolant
f36a691219 feat: Make the depth anything small model the default 2024-01-23 14:13:50 -06:00
blessedcoolant
6a2eb1d2e4 fix: Change the path of the annotator folder to annotators
Just making this change in case there are other models added to the folder in the future
2024-01-23 14:13:50 -06:00
blessedcoolant
13123daa3f feat: Add DepthAnything to Linear UI 2024-01-23 14:13:50 -06:00
blessedcoolant
c859eb865e fix: lint & other minor issues 2024-01-23 14:13:50 -06:00
blessedcoolant
8f5e2cbcc7 feat: Add Depth Anything PreProcessor 2024-01-23 14:13:50 -06:00
psychedelicious
2aed6e2dba fix(ui): duplicate "base model" in merge UI
closes #5505
2024-01-23 14:13:18 -06:00
psychedelicious
52b51a6088 fix(ui): recall/use size sets aspect ratio correctly
Added a new action that resets the aspect ratio when dispatched.

Closes #5456
2024-01-23 14:13:18 -06:00
psychedelicious
52b24e01e2 feat(ui): remove chakra as direct dependency
Moved a number of things to `@invoke-ai/ui` to support this.

Unfortunately, the bundle size has increased a bit. I will work on that later.
2024-01-23 14:13:18 -06:00
psychedelicious
1178fd8bd3 fix(ui): fix styling of some form elements 2024-01-23 14:13:18 -06:00
psychedelicious
a0187cc9df fix(ui): remove unused import in storybook 2024-01-23 14:13:18 -06:00
psychedelicious
2f656cc357 fix(ui): fix field context menu jank
Closes #5551
2024-01-23 14:13:18 -06:00
psychedelicious
71f9ac9985 feat(ui): scollable areas support x-axis scrolling
Closes #5490
2024-01-23 14:13:18 -06:00
psychedelicious
8bbdfc45fa fix(ui): increase size of control adapters advanced toggle button 2024-01-23 14:13:18 -06:00
psychedelicious
3cbb1a7671 chore(ui): bump @invoke-ai/ui
This includes a minor enhancement, increasing the contrast on tabs.
2024-01-23 14:13:18 -06:00
psychedelicious
b74e0de74a tidy(ui): remove unused state from uiSlice 2024-01-23 14:13:18 -06:00
psychedelicious
e7e7793896 feat(ui): remember open/closed state of accordions/expanders 2024-01-23 14:13:18 -06:00
psychedelicious
504bdac14a tidy(ui): remove unused state from uiSlice 2024-01-23 14:13:18 -06:00
psychedelicious
b76d2cd716 fix(ui): handle base model compat when recalling parameters
We had a one-behind issue with recalling metadata items that had a model.

For example, when recalling LoRAs, we check against the current main model to decide whether or not the requested LoRA is compatible and may be recalled.

When recalling all params, we are often also recalling the main model, but the compat logic didn't compare against this new main model.

The logic is updated to check against the new main model, if one is being set.

Closes #5512
2024-01-23 14:13:18 -06:00
psychedelicious
022b32c724 fix(ui): reset clip skip to 0 if new model is sdxl
Clip skip wasn't actually used in SDXL graphs so enabling it didn't do anything, just a UI quirk.

Closes #5508
2024-01-23 14:13:18 -06:00
psychedelicious
653b820da1 tidy(ui): clearer variable names in modelSelected listener 2024-01-23 14:13:18 -06:00
Brandon
68232e642f Merge branch 'main' into pr-labeler 2024-01-23 09:20:43 -05:00
blessedcoolant
4ba0bf4dcf docs(ui): update README (#5473)
## What type of PR is this? (check all applicable)

- [ ] Refactor
- [ ] Feature
- [ ] Bug Fix
- [ ] Optimization
- [x] Documentation Update
- [ ] Community Node Submission

## Description

Update UI README


## Merge Plan

This PR can be merged when approved

<!--
A merge plan describes how this PR should be handled after it is
approved.

Example merge plans:
- "This PR can be merged when approved"
- "This must be squash-merged when approved"
- "DO NOT MERGE - I will rebase and tidy commits before merging"
- "#dev-chat on discord needs to be advised of this change when it is
merged"

A merge plan is particularly important for large PRs or PRs that touch
the
database in any way.
-->
2024-01-23 18:47:24 +05:30
psychedelicious
5e4daf4bc6 docs: remove separate frontend docs and link to repo
The frontend docs should just be in the frontend. This is a standard practice for monorepos with developer information for specific packages within the monorepo.
2024-01-23 18:04:41 +11:00
psychedelicious
7e0713c869 docs(ui): fix typo 2024-01-23 18:04:41 +11:00
psychedelicious
099d516ac0 docs(ui): update README 2024-01-23 18:04:41 +11:00
Brandon Rising
b94f6a4a29 Fix python label, add test label 2024-01-22 15:14:02 -05:00
Brandon Rising
4caf63d53d Added a few more labels 2024-01-22 15:08:11 -05:00
Brandon Rising
6057229ceb Github action for ensuring PRs are labeled in a way that makes it easy to distinguish what's being changed 2024-01-22 11:22:33 -05:00
JPPhoto
6a2856e46f Updated field descriptions 2024-01-23 02:26:30 +11:00
Jonathan
4dedd63b74 Update defaultNodes.md
Added ideal size node
2024-01-23 02:26:30 +11:00
Jonathan
db74837eb1 Update communityNodes.md
Removed ideal size node
2024-01-23 02:26:30 +11:00
Jonathan
892fe62264 Add Ideal Size node to core nodes
The Ideal Size node is useful for High-Res Optimization as it gives the optimum size for creating an initial generation with minimal artifacts (duplication and other strangeness) from today's models.

After inclusion, front end graph generation can be simplified by offloading calculations for HRO initial generation to this node.
2024-01-23 02:26:30 +11:00
669 changed files with 8645 additions and 12877 deletions

View File

@@ -6,10 +6,6 @@ title: '[bug]: '
labels: ['bug']
# assignees:
# - moderator_bot
# - lstein
body:
- type: markdown
attributes:
@@ -18,10 +14,9 @@ body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
label: Is there an existing issue for this problem?
description: |
Please use the [search function](https://github.com/invoke-ai/InvokeAI/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
irst to see if an issue already exists for the bug you encountered.
Please [search](https://github.com/invoke-ai/InvokeAI/issues) first to see if an issue already exists for the problem.
options:
- label: I have searched the existing issues
required: true
@@ -33,80 +28,119 @@ body:
- type: dropdown
id: os_dropdown
attributes:
label: OS
description: Which operating System did you use when the bug occured
label: Operating system
description: Your computer's operating system.
multiple: false
options:
- 'Linux'
- 'Windows'
- 'macOS'
- 'other'
validations:
required: true
- type: dropdown
id: gpu_dropdown
attributes:
label: GPU
description: Which kind of Graphic-Adapter is your System using
label: GPU vendor
description: Your GPU's vendor.
multiple: false
options:
- 'cuda'
- 'amd'
- 'mps'
- 'cpu'
- 'Nvidia (CUDA)'
- 'AMD (ROCm)'
- 'Apple Silicon (MPS)'
- 'None (CPU)'
validations:
required: true
- type: input
id: gpu_model
attributes:
label: GPU model
description: Your GPU's model. If on Apple Silicon, this is your Mac's chip. Leave blank if on CPU.
placeholder: ex. RTX 2080 Ti, Mac M1 Pro
validations:
required: false
- type: input
id: vram
attributes:
label: VRAM
description: Size of the VRAM if known
label: GPU VRAM
description: Your GPU's VRAM. If on Apple Silicon, this is your Mac's unified memory. Leave blank if on CPU.
placeholder: 8GB
validations:
required: false
- type: input
id: version-number
attributes:
label: What version did you experience this issue on?
label: Version number
description: |
Please share the version of Invoke AI that you experienced the issue on. If this is not the latest version, please update first to confirm the issue still exists. If you are testing main, please include the commit hash instead.
placeholder: X.X.X
The version of Invoke you have installed. If it is not the latest version, please update and try again to confirm the issue still exists. If you are testing main, please include the commit hash instead.
placeholder: ex. 3.6.1
validations:
required: true
- type: input
id: browser-version
attributes:
label: Browser
description: Your web browser and version.
placeholder: ex. Firefox 123.0b3
validations:
required: true
- type: textarea
id: python-deps
attributes:
label: Python dependencies
description: |
If the problem occurred during image generation, click the gear icon at the bottom left corner, click "About", click the copy button and then paste here.
validations:
required: false
- type: textarea
id: what-happened
attributes:
label: What happened?
label: What happened
description: |
Briefly describe what happened, what you expected to happen and how to reproduce this bug.
placeholder: When using the webinterface and right-clicking on button X instead of the popup-menu there error Y appears
Describe what happened. Include any relevant error messages, stack traces and screenshots here.
placeholder: I clicked button X and then Y happened.
validations:
required: true
- type: textarea
id: what-you-expected
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem
placeholder: this is what the result looked like <screenshot>
label: What you expected to happen
description: Describe what you expected to happen.
placeholder: I expected Z to happen.
validations:
required: true
- type: textarea
id: how-to-repro
attributes:
label: How to reproduce the problem
description: List steps to reproduce the problem.
placeholder: Start the app, generate an image with these settings, then click button X.
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context about the problem here
description: Any other context that might help us to understand the problem.
placeholder: Only happens when there is full moon and Friday the 13th on Christmas Eve 🎅🏻
validations:
required: false
- type: input
id: contact
id: discord-username
attributes:
label: Contact Details
description: __OPTIONAL__ How can we get in touch with you if we need more info (besides this issue)?
placeholder: ex. email@example.com, discordname, twitter, ...
label: Discord username
description: If you are on the Invoke discord and would prefer to be contacted there, please provide your username.
placeholder: supercoolusername123
validations:
required: false

59
.github/pr_labels.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
Root:
- changed-files:
- any-glob-to-any-file: '*'
PythonDeps:
- changed-files:
- any-glob-to-any-file: 'pyproject.toml'
Python:
- changed-files:
- all-globs-to-any-file:
- 'invokeai/**'
- '!invokeai/frontend/web/**'
PythonTests:
- changed-files:
- any-glob-to-any-file: 'tests/**'
CICD:
- changed-files:
- any-glob-to-any-file: .github/**
Docker:
- changed-files:
- any-glob-to-any-file: docker/**
Installer:
- changed-files:
- any-glob-to-any-file: installer/**
Documentation:
- changed-files:
- any-glob-to-any-file: docs/**
Invocations:
- changed-files:
- any-glob-to-any-file: 'invokeai/app/invocations/**'
Backend:
- changed-files:
- any-glob-to-any-file: 'invokeai/backend/**'
Api:
- changed-files:
- any-glob-to-any-file: 'invokeai/app/api/**'
Services:
- changed-files:
- any-glob-to-any-file: 'invokeai/app/services/**'
FrontendDeps:
- changed-files:
- any-glob-to-any-file:
- '**/*/package.json'
- '**/*/pnpm-lock.yaml'
Frontend:
- changed-files:
- any-glob-to-any-file: 'invokeai/frontend/web/**'

16
.github/workflows/label-pr.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/labeler@v5
with:
configuration-path: .github/pr_labels.yml

View File

@@ -1,76 +0,0 @@
# Contributing to the Frontend
# InvokeAI Web UI
- [InvokeAI Web UI](https://github.com/invoke-ai/InvokeAI/tree/main/invokeai/frontend/web/docs#invokeai-web-ui)
- [Stack](https://github.com/invoke-ai/InvokeAI/tree/main/invokeai/frontend/web/docs#stack)
- [Contributing](https://github.com/invoke-ai/InvokeAI/tree/main/invokeai/frontend/web/docs#contributing)
- [Dev Environment](https://github.com/invoke-ai/InvokeAI/tree/main/invokeai/frontend/web/docs#dev-environment)
- [Production builds](https://github.com/invoke-ai/InvokeAI/tree/main/invokeai/frontend/web/docs#production-builds)
The UI is a fairly straightforward Typescript React app, with the Unified Canvas being more complex.
Code is located in `invokeai/frontend/web/` for review.
## Stack
State management is Redux via [Redux Toolkit](https://github.com/reduxjs/redux-toolkit). We lean heavily on RTK:
- `createAsyncThunk` for HTTP requests
- `createEntityAdapter` for fetching images and models
- `createListenerMiddleware` for workflows
The API client and associated types are generated from the OpenAPI schema. See API_CLIENT.md.
Communication with server is a mix of HTTP and [socket.io](https://github.com/socketio/socket.io-client) (with a simple socket.io redux middleware to help).
[Chakra-UI](https://github.com/chakra-ui/chakra-ui) & [Mantine](https://github.com/mantinedev/mantine) for components and styling.
[Konva](https://github.com/konvajs/react-konva) for the canvas, but we are pushing the limits of what is feasible with it (and HTML canvas in general). We plan to rebuild it with [PixiJS](https://github.com/pixijs/pixijs) to take advantage of WebGL's improved raster handling.
[Vite](https://vitejs.dev/) for bundling.
Localisation is via [i18next](https://github.com/i18next/react-i18next), but translation happens on our [Weblate](https://hosted.weblate.org/engage/invokeai/) project. Only the English source strings should be changed on this repo.
## Contributing
Thanks for your interest in contributing to the InvokeAI Web UI!
We encourage you to ping @psychedelicious and @blessedcoolant on [Discord](https://discord.gg/ZmtBAhwWhy) if you want to contribute, just to touch base and ensure your work doesn't conflict with anything else going on. The project is very active.
### Dev Environment
**Setup**
1. Install [node](https://nodejs.org/en/download/). You can confirm node is installed with:
```bash
node --version
```
2. Install [pnpm](https://pnpm.io/) and confirm it is installed by running this:
```bash
npm install --global pnpm
pnpm --version
```
From `invokeai/frontend/web/` run `pnpm install` to get everything set up.
Start everything in dev mode:
1. Ensure your virtual environment is running
2. Start the dev server: `pnpm dev`
3. Start the InvokeAI Nodes backend: `python scripts/invokeai-web.py # run from the repo root`
4. Point your browser to the dev server address e.g. [http://localhost:5173/](http://localhost:5173/)
### VSCode Remote Dev
We've noticed an intermittent issue with the VSCode Remote Dev port forwarding. If you use this feature of VSCode, you may intermittently click the Invoke button and then get nothing until the request times out. Suggest disabling the IDE's port forwarding feature and doing it manually via SSH:
`ssh -L 9090:localhost:9090 -L 5173:localhost:5173 user@host`
### Production builds
For a number of technical and logistical reasons, we need to commit UI build artefacts to the repo.
If you submit a PR, there is a good chance we will ask you to include a separate commit with a build of the app.
To build for production, run `pnpm build`.

View File

@@ -12,7 +12,7 @@ To get started, take a look at our [new contributors checklist](newContributorCh
Once you're setup, for more information, you can review the documentation specific to your area of interest:
* #### [InvokeAI Architecure](../ARCHITECTURE.md)
* #### [Frontend Documentation](./contributingToFrontend.md)
* #### [Frontend Documentation](https://github.com/invoke-ai/InvokeAI/tree/main/invokeai/frontend/web)
* #### [Node Documentation](../INVOCATIONS.md)
* #### [Local Development](../LOCAL_DEVELOPMENT.md)

BIN
docs/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -117,6 +117,11 @@ Mac and Linux machines, and runs on GPU cards with as little as 4 GB of RAM.
## :octicons-gift-24: InvokeAI Features
### Installation
- [Automated Installer](installation/010_INSTALL_AUTOMATED.md)
- [Manual Installation](installation/020_INSTALL_MANUAL.md)
- [Docker Installation](installation/040_INSTALL_DOCKER.md)
### The InvokeAI Web Interface
- [WebUI overview](features/WEB.md)
- [WebUI hotkey reference guide](features/WEBUIHOTKEYS.md)

View File

@@ -18,13 +18,18 @@ either an Nvidia-based card (with CUDA support) or an AMD card (using the ROCm
driver).
## **[Automated Installer](010_INSTALL_AUTOMATED.md)**
✅ This is the recommended installation method for first-time users.
## **[Automated Installer (Recommended)](010_INSTALL_AUTOMATED.md)**
✅ This is the recommended installation method for first-time users.
This is a script that will install all of InvokeAI's essential
third party libraries and InvokeAI itself. It includes access to a
"developer console" which will help us debug problems with you and
give you to access experimental features.
third party libraries and InvokeAI itself.
🖥️ **Download the latest installer .zip file here** : https://github.com/invoke-ai/InvokeAI/releases/latest
- *Look for the file labelled "InvokeAI-installer-v3.X.X.zip" at the bottom of the page*
- If you experience issues, read through the full [installation instructions](010_INSTALL_AUTOMATED.md) to make sure you have met all of the installation requirements. If you need more help, join the [Discord](discord.gg/invoke-ai) or create an issue on [Github](https://github.com/invoke-ai/InvokeAI).
## **[Manual Installation](020_INSTALL_MANUAL.md)**
This method is recommended for experienced users and developers.

View File

@@ -25,7 +25,6 @@ To use a community workflow, download the the `.json` node graph file and load i
+ [GPT2RandomPromptMaker](#gpt2randompromptmaker)
+ [Grid to Gif](#grid-to-gif)
+ [Halftone](#halftone)
+ [Ideal Size](#ideal-size)
+ [Image and Mask Composition Pack](#image-and-mask-composition-pack)
+ [Image Dominant Color](#image-dominant-color)
+ [Image to Character Art Image Nodes](#image-to-character-art-image-nodes)
@@ -196,13 +195,6 @@ CMYK Halftone Output:
<img src="https://github.com/invoke-ai/InvokeAI/assets/34005131/c59c578f-db8e-4d66-8c66-2851752d75ea" width="300" />
--------------------------------
### Ideal Size
**Description:** This node calculates an ideal image size for a first pass of a multi-pass upscaling. The aim is to avoid duplication that results from choosing a size larger than the model is capable of.
**Node Link:** https://github.com/JPPhoto/ideal-size-node
--------------------------------
### Image and Mask Composition Pack

View File

@@ -36,6 +36,7 @@ their descriptions.
| Integer Math | Perform basic math operations on two integers |
| Convert Image Mode | Converts an image to a different mode. |
| Crop Image | Crops an image to a specified box. The box can be outside of the image. |
| Ideal Size | Calculates an ideal image size for latents for a first pass of a multi-pass upscaling to avoid duplication and other artifacts |
| Image Hue Adjustment | Adjusts the Hue of an image. |
| Inverse Lerp Image | Inverse linear interpolation of all pixels of an image |
| Image Primitive | An image primitive value |

View File

@@ -13,46 +13,69 @@ We thank them for all of their time and hard work.
- [Lincoln D. Stein](mailto:lincoln.stein@gmail.com)
## **Current core team**
## **Current Core Team**
* @lstein (Lincoln Stein) - Co-maintainer
* @blessedcoolant - Co-maintainer
* @hipsterusername (Kent Keirsey) - Co-maintainer, CEO, Positive Vibes
* @psychedelicious (Spencer Mabrito) - Web Team Leader
* @Kyle0654 (Kyle Schouviller) - Node Architect and General Backend Wizard
* @damian0815 - Attention Systems and Compel Maintainer
* @ebr (Eugene Brodsky) - Cloud/DevOps/Sofware engineer; your friendly neighbourhood cluster-autoscaler
* @genomancer (Gregg Helt) - Controlnet support
* @StAlKeR7779 (Sergey Borisov) - Torch stack, ONNX, model management, optimization
* @chainchompa (Jennifer Player) - Web Development & Chain-Chomping
* @josh is toast (Josh Corbett) - Web Development
* @cheerio (Mary Rogers) - Lead Engineer & Web App Development
* @ebr (Eugene Brodsky) - Cloud/DevOps/Sofware engineer; your friendly neighbourhood cluster-autoscaler
* @sunija - Standalone version
* @genomancer (Gregg Helt) - Controlnet support
* @brandon (Brandon Rising) - Platform, Infrastructure, Backend Systems
* @ryanjdick (Ryan Dick) - Machine Learning & Training
* @millu (Millun Atluri) - Community Manager, Documentation, Node-wrangler
* @chainchompa (Jennifer Player) - Web Development & Chain-Chomping
* @JPPhoto - Core image generation nodes
* @dunkeroni - Image generation backend
* @SkunkWorxDark - Image generation backend
* @keturn (Kevin Turner) - Diffusers
* @millu (Millun Atluri) - Community Wizard, Documentation, Node-wrangler,
* @glimmerleaf (Devon Hopkins) - Community Wizard
* @gogurt enjoyer - Discord moderator and end user support
* @whosawhatsis - Discord moderator and end user support
* @dwinrger - Discord moderator and end user support
* @526christian - Discord moderator and end user support
* @harvester62 - Discord moderator and end user support
## **Honored Team Alumni**
* @StAlKeR7779 (Sergey Borisov) - Torch stack, ONNX, model management, optimization
* @damian0815 - Attention Systems and Compel Maintainer
* @netsvetaev (Artur) - Localization support
* @Kyle0654 (Kyle Schouviller) - Node Architect and General Backend Wizard
* @tildebyte - Installation and configuration
* @mauwii (Matthias Wilde) - Installation, release, continuous integration
## **Full List of Contributors by Commit Name**
- 이승석
- AbdBarho
- ablattmann
- AdamOStark
- Adam Rice
- Airton Silva
- Aldo Hoeben
- Alexander Eichhorn
- Alexandre D. Roberge
- Alexandre Macabies
- Alfie John
- Andreas Rozek
- Andre LaBranche
- Andy Bearman
- Andy Luhrs
- Andy Pilate
- Anonymous
- Anthony Monthe
- Any-Winter-4079
- apolinario
- Ar7ific1al
- ArDiouscuros
- Armando C. Santisbon
- Arnold Cordewiner
- Arthur Holstvoogd
- artmen1516
- Artur
@@ -64,13 +87,16 @@ We thank them for all of their time and hard work.
- blhook
- BlueAmulet
- Bouncyknighter
- Brandon
- Brandon Rising
- Brent Ozar
- Brian Racer
- bsilvereagle
- c67e708d
- camenduru
- CapableWeb
- Carson Katri
- chainchompa
- Chloe
- Chris Dawson
- Chris Hayes
@@ -86,30 +112,45 @@ We thank them for all of their time and hard work.
- cpacker
- Cragin Godley
- creachec
- CrypticWit
- d8ahazard
- damian
- damian0815
- Damian at mba
- Damian Stewart
- Daniel Manzke
- Danny Beer
- Dan Sully
- Darren Ringer
- David Burnett
- David Ford
- David Regla
- David Sisco
- David Wager
- Daya Adianto
- db3000
- DekitaRPG
- Denis Olshin
- Dennis
- dependabot[bot]
- Dmitry Parnas
- Dobrynia100
- Dominic Letz
- DrGunnarMallon
- Drun555
- dunkeroni
- Edward Johan
- elliotsayes
- Elrik
- ElrikUnderlake
- Eric Khun
- Eric Wolf
- Eugene
- Eugene Brodsky
- ExperimentalCyborg
- Fabian Bahl
- Fabio 'MrWHO' Torchetti
- Fattire
- fattire
- Felipe Nogueira
- Félix Sanz
@@ -118,8 +159,12 @@ We thank them for all of their time and hard work.
- gabrielrotbart
- gallegonovato
- Gérald LONLAS
- Gille
- GitHub Actions Bot
- glibesyck
- gogurtenjoyer
- Gohsuke Shimada
- greatwolf
- greentext2
- Gregg Helt
- H4rk
@@ -131,6 +176,7 @@ We thank them for all of their time and hard work.
- Hosted Weblate
- Iman Karim
- ismail ihsan bülbül
- ItzAttila
- Ivan Efimov
- jakehl
- Jakub Kolčář
@@ -141,6 +187,7 @@ We thank them for all of their time and hard work.
- Jason Toffaletti
- Jaulustus
- Jeff Mahoney
- Jennifer Player
- jeremy
- Jeremy Clark
- JigenD
@@ -148,19 +195,26 @@ We thank them for all of their time and hard work.
- Johan Roxendal
- Johnathon Selstad
- Jonathan
- Jordan Hewitt
- Joseph Dries III
- Josh Corbett
- JPPhoto
- jspraul
- junzi
- Justin Wong
- Juuso V
- Kaspar Emanuel
- Katsuyuki-Karasawa
- Keerigan45
- Kent Keirsey
- Kevin Brack
- Kevin Coakley
- Kevin Gibbons
- Kevin Schaul
- Kevin Turner
- Kieran Klaassen
- krummrey
- Kyle
- Kyle Lacy
- Kyle Schouviller
- Lawrence Norton
@@ -171,10 +225,15 @@ We thank them for all of their time and hard work.
- Lynne Whitehorn
- majick
- Marco Labarile
- Marta Nahorniuk
- Martin Kristiansen
- Mary Hipp
- maryhipp
- Mary Hipp Rogers
- mastercaster
- mastercaster9000
- Matthias Wild
- mauwii
- michaelk71
- mickr777
- Mihai
@@ -182,11 +241,15 @@ We thank them for all of their time and hard work.
- Mikhail Tishin
- Millun Atluri
- Minjune Song
- Mitchell Allain
- mitien
- mofuzz
- Muhammad Usama
- Name
- _nderscore
- Neil Wang
- nekowaiz
- nemuruibai
- Netzer R
- Nicholas Koh
- Nicholas Körfer
@@ -197,9 +260,11 @@ We thank them for all of their time and hard work.
- ofirkris
- Olivier Louvignes
- owenvincent
- pand4z31
- Patrick Esser
- Patrick Tien
- Patrick von Platen
- Paul Curry
- Paul Sajna
- pejotr
- Peter Baylies
@@ -207,6 +272,7 @@ We thank them for all of their time and hard work.
- plucked
- prixt
- psychedelicious
- psychedelicious@windows
- Rainer Bernhardt
- Riccardo Giovanetti
- Rich Jones
@@ -215,17 +281,22 @@ We thank them for all of their time and hard work.
- Robert Bolender
- Robin Rombach
- Rohan Barar
- rohinish404
- Rohinish
- rpagliuca
- rromb
- Rupesh Sreeraman
- Ryan
- Ryan Cao
- Ryan Dick
- Saifeddine
- Saifeddine ALOUI
- Sam
- SammCheese
- Sam McLeod
- Sammy
- sammyf
- Samuel Husso
- Saurav Maheshkar
- Scott Lahteine
- Sean McLellan
- Sebastian Aigner
@@ -233,16 +304,21 @@ We thank them for all of their time and hard work.
- Sergey Krashevich
- Shapor Naghibzadeh
- Shawn Zhong
- Simona Liliac
- Simon Vans-Colina
- skunkworxdark
- slashtechno
- SoheilRezaei
- Song, Pengcheng
- spezialspezial
- ssantos
- StAlKeR7779
- Stefan Tobler
- Stephan Koglin-Fischer
- SteveCaruso
- Steve Martinelli
- Steven Frank
- Surisen
- System X - Files
- Taylor Kems
- techicode
@@ -261,26 +337,34 @@ We thank them for all of their time and hard work.
- tyler
- unknown
- user1
- vedant-3010
- Vedant Madane
- veprogames
- wa.code
- wfng92
- whjms
- whosawhatsis
- Will
- William Becher
- William Chong
- Wilson E. Alvarez
- woweenie
- Wubbbi
- xra
- Yeung Yiu Hung
- ymgenesis
- Yorzaren
- Yosuke Shinya
- yun saki
- ZachNagengast
- Zadagu
- zeptofine
- Zerdoumi
- Васянатор
- 冯不游
- 唐澤 克幸
## **Original CompVis Authors**
## **Original CompVis (Stable Diffusion) Authors**
- [Robin Rombach](https://github.com/rromb)
- [Patrick von Platen](https://github.com/patrickvonplaten)

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,7 @@ from invokeai.app.invocations.primitives import ImageField, ImageOutput
from invokeai.app.invocations.util import validate_begin_end_step, validate_weights
from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
from invokeai.app.shared.fields import FieldDescriptions
from invokeai.backend.image_util.depth_anything import DepthAnythingDetector
from ...backend.model_management import BaseModelType
from .baseinvocation import (
@@ -602,3 +603,33 @@ class ColorMapImageProcessorInvocation(ImageProcessorInvocation):
color_map = cv2.resize(color_map, (width, height), interpolation=cv2.INTER_NEAREST)
color_map = Image.fromarray(color_map)
return color_map
DEPTH_ANYTHING_MODEL_SIZES = Literal["large", "base", "small"]
@invocation(
"depth_anything_image_processor",
title="Depth Anything Processor",
tags=["controlnet", "depth", "depth anything"],
category="controlnet",
version="1.0.0",
)
class DepthAnythingImageProcessorInvocation(ImageProcessorInvocation):
"""Generates a depth map based on the Depth Anything algorithm"""
model_size: DEPTH_ANYTHING_MODEL_SIZES = InputField(
default="small", description="The size of the depth model to use"
)
resolution: int = InputField(default=512, ge=64, multiple_of=64, description=FieldDescriptions.image_res)
offload: bool = InputField(default=False)
def run_processor(self, image):
depth_anything_detector = DepthAnythingDetector()
depth_anything_detector.load_model(model_size=self.model_size)
if image.mode == "RGBA":
image = image.convert("RGB")
processed_image = depth_anything_detector(image=image, resolution=self.resolution, offload=self.offload)
return processed_image

View File

@@ -1,5 +1,6 @@
# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654)
import math
from contextlib import ExitStack
from functools import singledispatchmethod
from typing import List, Literal, Optional, Union
@@ -1228,3 +1229,57 @@ class CropLatentsCoreInvocation(BaseInvocation):
context.services.latents.save(name, cropped_latents)
return build_latents_output(latents_name=name, latents=cropped_latents)
@invocation_output("ideal_size_output")
class IdealSizeOutput(BaseInvocationOutput):
"""Base class for invocations that output an image"""
width: int = OutputField(description="The ideal width of the image (in pixels)")
height: int = OutputField(description="The ideal height of the image (in pixels)")
@invocation(
"ideal_size",
title="Ideal Size",
tags=["latents", "math", "ideal_size"],
version="1.0.2",
)
class IdealSizeInvocation(BaseInvocation):
"""Calculates the ideal size for generation to avoid duplication"""
width: int = InputField(default=1024, description="Final image width")
height: int = InputField(default=576, description="Final image height")
unet: UNetField = InputField(default=None, description=FieldDescriptions.unet)
multiplier: float = InputField(
default=1.0,
description="Amount to multiply the model's dimensions by when calculating the ideal size (may result in initial generation artifacts if too large)",
)
def trim_to_multiple_of(self, *args, multiple_of=LATENT_SCALE_FACTOR):
return tuple((x - x % multiple_of) for x in args)
def invoke(self, context: InvocationContext) -> IdealSizeOutput:
aspect = self.width / self.height
dimension = 512
if self.unet.unet.base_model == BaseModelType.StableDiffusion2:
dimension = 768
elif self.unet.unet.base_model == BaseModelType.StableDiffusionXL:
dimension = 1024
dimension = dimension * self.multiplier
min_dimension = math.floor(dimension * 0.5)
model_area = dimension * dimension # hardcoded for now since all models are trained on square images
if aspect > 1.0:
init_height = max(min_dimension, math.sqrt(model_area / aspect))
init_width = init_height * aspect
else:
init_width = max(min_dimension, math.sqrt(model_area * aspect))
init_height = init_width / aspect
scaled_width, scaled_height = self.trim_to_multiple_of(
math.floor(init_width),
math.floor(init_height),
)
return IdealSizeOutput(width=scaled_width, height=scaled_height)

View File

@@ -31,6 +31,7 @@ class WorkflowRecordOrderBy(str, Enum, metaclass=MetaEnum):
class WorkflowCategory(str, Enum, metaclass=MetaEnum):
User = "user"
Default = "default"
Project = "project"
class WorkflowMeta(BaseModel):

View File

@@ -0,0 +1,109 @@
import pathlib
from typing import Literal, Union
import cv2
import numpy as np
import torch
import torch.nn.functional as F
from einops import repeat
from PIL import Image
from torchvision.transforms import Compose
from invokeai.app.services.config.config_default import InvokeAIAppConfig
from invokeai.backend.image_util.depth_anything.model.dpt import DPT_DINOv2
from invokeai.backend.image_util.depth_anything.utilities.util import NormalizeImage, PrepareForNet, Resize
from invokeai.backend.util.devices import choose_torch_device
from invokeai.backend.util.util import download_with_progress_bar
config = InvokeAIAppConfig.get_config()
DEPTH_ANYTHING_MODELS = {
"large": {
"url": "https://huggingface.co/spaces/LiheYoung/Depth-Anything/resolve/main/checkpoints/depth_anything_vitl14.pth?download=true",
"local": "any/annotators/depth_anything/depth_anything_vitl14.pth",
},
"base": {
"url": "https://huggingface.co/spaces/LiheYoung/Depth-Anything/resolve/main/checkpoints/depth_anything_vitb14.pth?download=true",
"local": "any/annotators/depth_anything/depth_anything_vitb14.pth",
},
"small": {
"url": "https://huggingface.co/spaces/LiheYoung/Depth-Anything/resolve/main/checkpoints/depth_anything_vits14.pth?download=true",
"local": "any/annotators/depth_anything/depth_anything_vits14.pth",
},
}
transform = Compose(
[
Resize(
width=518,
height=518,
resize_target=False,
keep_aspect_ratio=True,
ensure_multiple_of=14,
resize_method="lower_bound",
image_interpolation_method=cv2.INTER_CUBIC,
),
NormalizeImage(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
PrepareForNet(),
]
)
class DepthAnythingDetector:
def __init__(self) -> None:
self.model = None
self.model_size: Union[Literal["large", "base", "small"], None] = None
def load_model(self, model_size=Literal["large", "base", "small"]):
DEPTH_ANYTHING_MODEL_PATH = pathlib.Path(config.models_path / DEPTH_ANYTHING_MODELS[model_size]["local"])
if not DEPTH_ANYTHING_MODEL_PATH.exists():
download_with_progress_bar(DEPTH_ANYTHING_MODELS[model_size]["url"], DEPTH_ANYTHING_MODEL_PATH)
if not self.model or model_size != self.model_size:
del self.model
self.model_size = model_size
match self.model_size:
case "small":
self.model = DPT_DINOv2(encoder="vits", features=64, out_channels=[48, 96, 192, 384])
case "base":
self.model = DPT_DINOv2(encoder="vitb", features=128, out_channels=[96, 192, 384, 768])
case "large":
self.model = DPT_DINOv2(encoder="vitl", features=256, out_channels=[256, 512, 1024, 1024])
case _:
raise TypeError("Not a supported model")
self.model.load_state_dict(torch.load(DEPTH_ANYTHING_MODEL_PATH.as_posix(), map_location="cpu"))
self.model.eval()
self.model.to(choose_torch_device())
return self.model
def to(self, device):
self.model.to(device)
return self
def __call__(self, image, resolution=512, offload=False):
image = np.array(image, dtype=np.uint8)
image = image[:, :, ::-1] / 255.0
image_height, image_width = image.shape[:2]
image = transform({"image": image})["image"]
image = torch.from_numpy(image).unsqueeze(0).to(choose_torch_device())
with torch.no_grad():
depth = self.model(image)
depth = F.interpolate(depth[None], (image_height, image_width), mode="bilinear", align_corners=False)[0, 0]
depth = (depth - depth.min()) / (depth.max() - depth.min()) * 255.0
depth_map = repeat(depth, "h w -> h w 3").cpu().numpy().astype(np.uint8)
depth_map = Image.fromarray(depth_map)
new_height = int(image_height * (resolution / image_width))
depth_map = depth_map.resize((resolution, new_height))
if offload:
del self.model
return depth_map

View File

@@ -0,0 +1,145 @@
import torch.nn as nn
def _make_scratch(in_shape, out_shape, groups=1, expand=False):
scratch = nn.Module()
out_shape1 = out_shape
out_shape2 = out_shape
out_shape3 = out_shape
if len(in_shape) >= 4:
out_shape4 = out_shape
if expand:
out_shape1 = out_shape
out_shape2 = out_shape * 2
out_shape3 = out_shape * 4
if len(in_shape) >= 4:
out_shape4 = out_shape * 8
scratch.layer1_rn = nn.Conv2d(
in_shape[0], out_shape1, kernel_size=3, stride=1, padding=1, bias=False, groups=groups
)
scratch.layer2_rn = nn.Conv2d(
in_shape[1], out_shape2, kernel_size=3, stride=1, padding=1, bias=False, groups=groups
)
scratch.layer3_rn = nn.Conv2d(
in_shape[2], out_shape3, kernel_size=3, stride=1, padding=1, bias=False, groups=groups
)
if len(in_shape) >= 4:
scratch.layer4_rn = nn.Conv2d(
in_shape[3], out_shape4, kernel_size=3, stride=1, padding=1, bias=False, groups=groups
)
return scratch
class ResidualConvUnit(nn.Module):
"""Residual convolution module."""
def __init__(self, features, activation, bn):
"""Init.
Args:
features (int): number of features
"""
super().__init__()
self.bn = bn
self.groups = 1
self.conv1 = nn.Conv2d(features, features, kernel_size=3, stride=1, padding=1, bias=True, groups=self.groups)
self.conv2 = nn.Conv2d(features, features, kernel_size=3, stride=1, padding=1, bias=True, groups=self.groups)
if self.bn:
self.bn1 = nn.BatchNorm2d(features)
self.bn2 = nn.BatchNorm2d(features)
self.activation = activation
self.skip_add = nn.quantized.FloatFunctional()
def forward(self, x):
"""Forward pass.
Args:
x (tensor): input
Returns:
tensor: output
"""
out = self.activation(x)
out = self.conv1(out)
if self.bn:
out = self.bn1(out)
out = self.activation(out)
out = self.conv2(out)
if self.bn:
out = self.bn2(out)
if self.groups > 1:
out = self.conv_merge(out)
return self.skip_add.add(out, x)
class FeatureFusionBlock(nn.Module):
"""Feature fusion block."""
def __init__(self, features, activation, deconv=False, bn=False, expand=False, align_corners=True, size=None):
"""Init.
Args:
features (int): number of features
"""
super(FeatureFusionBlock, self).__init__()
self.deconv = deconv
self.align_corners = align_corners
self.groups = 1
self.expand = expand
out_features = features
if self.expand:
out_features = features // 2
self.out_conv = nn.Conv2d(features, out_features, kernel_size=1, stride=1, padding=0, bias=True, groups=1)
self.resConfUnit1 = ResidualConvUnit(features, activation, bn)
self.resConfUnit2 = ResidualConvUnit(features, activation, bn)
self.skip_add = nn.quantized.FloatFunctional()
self.size = size
def forward(self, *xs, size=None):
"""Forward pass.
Returns:
tensor: output
"""
output = xs[0]
if len(xs) == 2:
res = self.resConfUnit1(xs[1])
output = self.skip_add.add(output, res)
output = self.resConfUnit2(output)
if (size is None) and (self.size is None):
modifier = {"scale_factor": 2}
elif size is None:
modifier = {"size": self.size}
else:
modifier = {"size": size}
output = nn.functional.interpolate(output, **modifier, mode="bilinear", align_corners=self.align_corners)
output = self.out_conv(output)
return output

View File

@@ -0,0 +1,183 @@
from pathlib import Path
import torch
import torch.nn as nn
import torch.nn.functional as F
from .blocks import FeatureFusionBlock, _make_scratch
torchhub_path = Path(__file__).parent.parent / "torchhub"
def _make_fusion_block(features, use_bn, size=None):
return FeatureFusionBlock(
features,
nn.ReLU(False),
deconv=False,
bn=use_bn,
expand=False,
align_corners=True,
size=size,
)
class DPTHead(nn.Module):
def __init__(self, nclass, in_channels, features, out_channels, use_bn=False, use_clstoken=False):
super(DPTHead, self).__init__()
self.nclass = nclass
self.use_clstoken = use_clstoken
self.projects = nn.ModuleList(
[
nn.Conv2d(
in_channels=in_channels,
out_channels=out_channel,
kernel_size=1,
stride=1,
padding=0,
)
for out_channel in out_channels
]
)
self.resize_layers = nn.ModuleList(
[
nn.ConvTranspose2d(
in_channels=out_channels[0], out_channels=out_channels[0], kernel_size=4, stride=4, padding=0
),
nn.ConvTranspose2d(
in_channels=out_channels[1], out_channels=out_channels[1], kernel_size=2, stride=2, padding=0
),
nn.Identity(),
nn.Conv2d(
in_channels=out_channels[3], out_channels=out_channels[3], kernel_size=3, stride=2, padding=1
),
]
)
if use_clstoken:
self.readout_projects = nn.ModuleList()
for _ in range(len(self.projects)):
self.readout_projects.append(nn.Sequential(nn.Linear(2 * in_channels, in_channels), nn.GELU()))
self.scratch = _make_scratch(
out_channels,
features,
groups=1,
expand=False,
)
self.scratch.stem_transpose = None
self.scratch.refinenet1 = _make_fusion_block(features, use_bn)
self.scratch.refinenet2 = _make_fusion_block(features, use_bn)
self.scratch.refinenet3 = _make_fusion_block(features, use_bn)
self.scratch.refinenet4 = _make_fusion_block(features, use_bn)
head_features_1 = features
head_features_2 = 32
if nclass > 1:
self.scratch.output_conv = nn.Sequential(
nn.Conv2d(head_features_1, head_features_1, kernel_size=3, stride=1, padding=1),
nn.ReLU(True),
nn.Conv2d(head_features_1, nclass, kernel_size=1, stride=1, padding=0),
)
else:
self.scratch.output_conv1 = nn.Conv2d(
head_features_1, head_features_1 // 2, kernel_size=3, stride=1, padding=1
)
self.scratch.output_conv2 = nn.Sequential(
nn.Conv2d(head_features_1 // 2, head_features_2, kernel_size=3, stride=1, padding=1),
nn.ReLU(True),
nn.Conv2d(head_features_2, 1, kernel_size=1, stride=1, padding=0),
nn.ReLU(True),
nn.Identity(),
)
def forward(self, out_features, patch_h, patch_w):
out = []
for i, x in enumerate(out_features):
if self.use_clstoken:
x, cls_token = x[0], x[1]
readout = cls_token.unsqueeze(1).expand_as(x)
x = self.readout_projects[i](torch.cat((x, readout), -1))
else:
x = x[0]
x = x.permute(0, 2, 1).reshape((x.shape[0], x.shape[-1], patch_h, patch_w))
x = self.projects[i](x)
x = self.resize_layers[i](x)
out.append(x)
layer_1, layer_2, layer_3, layer_4 = out
layer_1_rn = self.scratch.layer1_rn(layer_1)
layer_2_rn = self.scratch.layer2_rn(layer_2)
layer_3_rn = self.scratch.layer3_rn(layer_3)
layer_4_rn = self.scratch.layer4_rn(layer_4)
path_4 = self.scratch.refinenet4(layer_4_rn, size=layer_3_rn.shape[2:])
path_3 = self.scratch.refinenet3(path_4, layer_3_rn, size=layer_2_rn.shape[2:])
path_2 = self.scratch.refinenet2(path_3, layer_2_rn, size=layer_1_rn.shape[2:])
path_1 = self.scratch.refinenet1(path_2, layer_1_rn)
out = self.scratch.output_conv1(path_1)
out = F.interpolate(out, (int(patch_h * 14), int(patch_w * 14)), mode="bilinear", align_corners=True)
out = self.scratch.output_conv2(out)
return out
class DPT_DINOv2(nn.Module):
def __init__(
self,
features,
out_channels,
encoder="vitl",
use_bn=False,
use_clstoken=False,
):
super(DPT_DINOv2, self).__init__()
assert encoder in ["vits", "vitb", "vitl"]
# # in case the Internet connection is not stable, please load the DINOv2 locally
# if use_local:
# self.pretrained = torch.hub.load(
# torchhub_path / "facebookresearch_dinov2_main",
# "dinov2_{:}14".format(encoder),
# source="local",
# pretrained=False,
# )
# else:
# self.pretrained = torch.hub.load(
# "facebookresearch/dinov2",
# "dinov2_{:}14".format(encoder),
# )
self.pretrained = torch.hub.load(
"facebookresearch/dinov2",
"dinov2_{:}14".format(encoder),
)
dim = self.pretrained.blocks[0].attn.qkv.in_features
self.depth_head = DPTHead(1, dim, features, out_channels=out_channels, use_bn=use_bn, use_clstoken=use_clstoken)
def forward(self, x):
h, w = x.shape[-2:]
features = self.pretrained.get_intermediate_layers(x, 4, return_class_token=True)
patch_h, patch_w = h // 14, w // 14
depth = self.depth_head(features, patch_h, patch_w)
depth = F.interpolate(depth, size=(h, w), mode="bilinear", align_corners=True)
depth = F.relu(depth)
return depth.squeeze(1)

View File

@@ -0,0 +1,227 @@
import math
import cv2
import numpy as np
import torch
import torch.nn.functional as F
def apply_min_size(sample, size, image_interpolation_method=cv2.INTER_AREA):
"""Rezise the sample to ensure the given size. Keeps aspect ratio.
Args:
sample (dict): sample
size (tuple): image size
Returns:
tuple: new size
"""
shape = list(sample["disparity"].shape)
if shape[0] >= size[0] and shape[1] >= size[1]:
return sample
scale = [0, 0]
scale[0] = size[0] / shape[0]
scale[1] = size[1] / shape[1]
scale = max(scale)
shape[0] = math.ceil(scale * shape[0])
shape[1] = math.ceil(scale * shape[1])
# resize
sample["image"] = cv2.resize(sample["image"], tuple(shape[::-1]), interpolation=image_interpolation_method)
sample["disparity"] = cv2.resize(sample["disparity"], tuple(shape[::-1]), interpolation=cv2.INTER_NEAREST)
sample["mask"] = cv2.resize(
sample["mask"].astype(np.float32),
tuple(shape[::-1]),
interpolation=cv2.INTER_NEAREST,
)
sample["mask"] = sample["mask"].astype(bool)
return tuple(shape)
class Resize(object):
"""Resize sample to given size (width, height)."""
def __init__(
self,
width,
height,
resize_target=True,
keep_aspect_ratio=False,
ensure_multiple_of=1,
resize_method="lower_bound",
image_interpolation_method=cv2.INTER_AREA,
):
"""Init.
Args:
width (int): desired output width
height (int): desired output height
resize_target (bool, optional):
True: Resize the full sample (image, mask, target).
False: Resize image only.
Defaults to True.
keep_aspect_ratio (bool, optional):
True: Keep the aspect ratio of the input sample.
Output sample might not have the given width and height, and
resize behaviour depends on the parameter 'resize_method'.
Defaults to False.
ensure_multiple_of (int, optional):
Output width and height is constrained to be multiple of this parameter.
Defaults to 1.
resize_method (str, optional):
"lower_bound": Output will be at least as large as the given size.
"upper_bound": Output will be at max as large as the given size. (Output size might be smaller
than given size.)
"minimal": Scale as least as possible. (Output size might be smaller than given size.)
Defaults to "lower_bound".
"""
self.__width = width
self.__height = height
self.__resize_target = resize_target
self.__keep_aspect_ratio = keep_aspect_ratio
self.__multiple_of = ensure_multiple_of
self.__resize_method = resize_method
self.__image_interpolation_method = image_interpolation_method
def constrain_to_multiple_of(self, x, min_val=0, max_val=None):
y = (np.round(x / self.__multiple_of) * self.__multiple_of).astype(int)
if max_val is not None and y > max_val:
y = (np.floor(x / self.__multiple_of) * self.__multiple_of).astype(int)
if y < min_val:
y = (np.ceil(x / self.__multiple_of) * self.__multiple_of).astype(int)
return y
def get_size(self, width, height):
# determine new height and width
scale_height = self.__height / height
scale_width = self.__width / width
if self.__keep_aspect_ratio:
if self.__resize_method == "lower_bound":
# scale such that output size is lower bound
if scale_width > scale_height:
# fit width
scale_height = scale_width
else:
# fit height
scale_width = scale_height
elif self.__resize_method == "upper_bound":
# scale such that output size is upper bound
if scale_width < scale_height:
# fit width
scale_height = scale_width
else:
# fit height
scale_width = scale_height
elif self.__resize_method == "minimal":
# scale as least as possbile
if abs(1 - scale_width) < abs(1 - scale_height):
# fit width
scale_height = scale_width
else:
# fit height
scale_width = scale_height
else:
raise ValueError(f"resize_method {self.__resize_method} not implemented")
if self.__resize_method == "lower_bound":
new_height = self.constrain_to_multiple_of(scale_height * height, min_val=self.__height)
new_width = self.constrain_to_multiple_of(scale_width * width, min_val=self.__width)
elif self.__resize_method == "upper_bound":
new_height = self.constrain_to_multiple_of(scale_height * height, max_val=self.__height)
new_width = self.constrain_to_multiple_of(scale_width * width, max_val=self.__width)
elif self.__resize_method == "minimal":
new_height = self.constrain_to_multiple_of(scale_height * height)
new_width = self.constrain_to_multiple_of(scale_width * width)
else:
raise ValueError(f"resize_method {self.__resize_method} not implemented")
return (new_width, new_height)
def __call__(self, sample):
width, height = self.get_size(sample["image"].shape[1], sample["image"].shape[0])
# resize sample
sample["image"] = cv2.resize(
sample["image"],
(width, height),
interpolation=self.__image_interpolation_method,
)
if self.__resize_target:
if "disparity" in sample:
sample["disparity"] = cv2.resize(
sample["disparity"],
(width, height),
interpolation=cv2.INTER_NEAREST,
)
if "depth" in sample:
sample["depth"] = cv2.resize(sample["depth"], (width, height), interpolation=cv2.INTER_NEAREST)
if "semseg_mask" in sample:
# sample["semseg_mask"] = cv2.resize(
# sample["semseg_mask"], (width, height), interpolation=cv2.INTER_NEAREST
# )
sample["semseg_mask"] = F.interpolate(
torch.from_numpy(sample["semseg_mask"]).float()[None, None, ...], (height, width), mode="nearest"
).numpy()[0, 0]
if "mask" in sample:
sample["mask"] = cv2.resize(
sample["mask"].astype(np.float32),
(width, height),
interpolation=cv2.INTER_NEAREST,
)
# sample["mask"] = sample["mask"].astype(bool)
# print(sample['image'].shape, sample['depth'].shape)
return sample
class NormalizeImage(object):
"""Normlize image by given mean and std."""
def __init__(self, mean, std):
self.__mean = mean
self.__std = std
def __call__(self, sample):
sample["image"] = (sample["image"] - self.__mean) / self.__std
return sample
class PrepareForNet(object):
"""Prepare sample for usage as network input."""
def __init__(self):
pass
def __call__(self, sample):
image = np.transpose(sample["image"], (2, 0, 1))
sample["image"] = np.ascontiguousarray(image).astype(np.float32)
if "mask" in sample:
sample["mask"] = sample["mask"].astype(np.float32)
sample["mask"] = np.ascontiguousarray(sample["mask"])
if "depth" in sample:
depth = sample["depth"].astype(np.float32)
sample["depth"] = np.ascontiguousarray(depth)
if "semseg_mask" in sample:
sample["semseg_mask"] = sample["semseg_mask"].astype(np.float32)
sample["semseg_mask"] = np.ascontiguousarray(sample["semseg_mask"])
return sample

View File

@@ -1,131 +1,9 @@
module.exports = {
env: {
browser: true,
es6: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:react/jsx-runtime',
'prettier',
'plugin:storybook/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: [
'react',
'@typescript-eslint',
'eslint-plugin-react-hooks',
'i18next',
'path',
'unused-imports',
'simple-import-sort',
'eslint-plugin-import',
// These rules are too strict for normal usage, but are useful for optimizing rerenders
// '@arthurgeron/react-usememo',
],
root: true,
extends: ['@invoke-ai/eslint-config-react'],
rules: {
'path/no-relative-imports': ['error', { maxDepth: 0 }],
curly: 'error',
'i18next/no-literal-string': 'warn',
'react/jsx-no-bind': ['error', { allowBind: true }],
'react/jsx-curly-brace-presence': [
'error',
{ props: 'never', children: 'never' },
],
'react-hooks/exhaustive-deps': 'error',
'no-var': 'error',
'brace-style': 'error',
'prefer-template': 'error',
'import/no-duplicates': 'error',
radix: 'error',
'space-before-blocks': 'error',
'import/prefer-default-export': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
'warn',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
},
],
// These rules are too strict for normal usage, but are useful for optimizing rerenders
// '@arthurgeron/react-usememo/require-usememo': [
// 'warn',
// {
// strict: false,
// checkHookReturnObject: false,
// fix: { addImports: true },
// checkHookCalls: false,
// },
// ],
// '@arthurgeron/react-usememo/require-memo': 'warn',
'@typescript-eslint/ban-ts-comment': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-empty-interface': [
'error',
{
allowSingleExtends: true,
},
],
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
fixStyle: 'separate-type-imports',
disallowTypeAnnotations: true,
},
],
'@typescript-eslint/no-import-type-side-effects': 'error',
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
// Prefer @invoke-ai/ui components over chakra
'no-restricted-imports': 'off',
'@typescript-eslint/no-restricted-imports': [
'warn',
{
paths: [
{
name: '@chakra-ui/react',
message: "Please import from '@invoke-ai/ui' instead.",
},
{
name: '@chakra-ui/layout',
message: "Please import from '@invoke-ai/ui' instead.",
},
{
name: '@chakra-ui/portal',
message: "Please import from '@invoke-ai/ui' instead.",
},
],
},
],
},
overrides: [
{
files: ['*.stories.tsx'],
rules: {
'i18next/no-literal-string': 'off',
},
},
],
settings: {
react: {
version: 'detect',
},
// TODO(psyche): Enable this rule. Requires no default exports in components - many changes.
'react-refresh/only-export-components': 'off',
// TODO(psyche): Enable this rule. Requires a lot of eslint-disable-next-line comments.
'@typescript-eslint/consistent-type-assertions': 'off',
},
};

View File

@@ -1,9 +1,5 @@
module.exports = {
trailingComma: 'es5',
tabWidth: 2,
semi: true,
singleQuote: true,
endOfLine: 'auto',
...require('@invoke-ai/prettier-config-react'),
overrides: [
{
files: ['public/locales/*.json'],

View File

@@ -1,7 +1,7 @@
import { PropsWithChildren, memo, useEffect } from 'react';
import { modelChanged } from '../src/features/parameters/store/generationSlice';
import { useAppDispatch } from '../src/app/store/storeHooks';
import { useGlobalModifiersInit } from '@invoke-ai/ui';
import { useGlobalModifiersInit } from '@invoke-ai/ui-library';
/**
* Initializes some state for storybook. Must be in a different component
* so that it is run inside the redux context.

View File

@@ -6,7 +6,6 @@ import { Provider } from 'react-redux';
import ThemeLocaleProvider from '../src/app/components/ThemeLocaleProvider';
import { $baseUrl } from '../src/app/store/nanostores/baseUrl';
import { createStore } from '../src/app/store/store';
import { Container } from '@chakra-ui/react';
// TODO: Disabled for IDE performance issues with our translation JSON
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore

View File

@@ -1,13 +1,7 @@
{
"entry": ["src/main.tsx"],
"extensions": [".ts", ".tsx"],
"ignorePatterns": [
"**/node_modules/**",
"dist/**",
"public/**",
"**/*.stories.tsx",
"config/**"
],
"ignorePatterns": ["**/node_modules/**", "dist/**", "public/**", "**/*.stories.tsx", "config/**"],
"ignoreUnresolved": [],
"ignoreUnimported": ["src/i18.d.ts", "vite.config.ts", "src/vite-env.d.ts"],
"respectGitignore": true,

View File

@@ -0,0 +1,150 @@
# Invoke UI
<!-- @import "[TOC]" {cmd="toc" depthFrom=2 depthTo=3 orderedList=false} -->
<!-- code_chunk_output -->
- [Dev environment](#dev-environment)
- [Setup](#setup)
- [Package scripts](#package-scripts)
- [Type generation](#type-generation)
- [Localization](#localization)
- [VSCode](#vscode)
- [Contributing](#contributing)
- [Check in before investing your time](#check-in-before-investing-your-time)
- [Commit format](#commit-format)
- [Submitting a PR](#submitting-a-pr)
- [Other docs](#other-docs)
<!-- /code_chunk_output -->
Invoke's UI is made possible by many contributors and open-source libraries. Thank you!
## Dev environment
### Setup
1. Install [node] and [pnpm].
1. Run `pnpm i` to install all packages.
#### Run in dev mode
1. From `invokeai/frontend/web/`, run `pnpm dev`.
1. From repo root, run `python scripts/invokeai-web.py`.
1. Point your browser to the dev server address, e.g. <http://localhost:5173/>
### Package scripts
- `dev`: run the frontend in dev mode, enabling hot reloading
- `build`: run all checks (madge, eslint, prettier, tsc) and then build the frontend
- `typegen`: generate types from the OpenAPI schema (see [Type generation])
- `lint:madge`: check frontend for circular dependencies
- `lint:eslint`: check frontend for code quality
- `lint:prettier`: check frontend for code formatting
- `lint:tsc`: check frontend for type issues
- `lint`: run all checks concurrently
- `fix`: run `eslint` and `prettier`, fixing fixable issues
### Type generation
We use [openapi-typescript] to generate types from the app's OpenAPI schema.
The generated types are committed to the repo in [schema.ts].
```sh
# from the repo root, start the server
python scripts/invokeai-web.py
# from invokeai/frontend/web/, run the script
pnpm typegen
```
### Localization
We use [i18next] for localization, but translation to languages other than English happens on our [Weblate] project.
Only the English source strings should be changed on this repo.
### VSCode
#### Example debugger config
```jsonc
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Invoke UI",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/invokeai/frontend/web",
},
],
}
```
#### Remote dev
We've noticed an intermittent timeout issue with the VSCode remote dev port forwarding.
We suggest disabling the editor's port forwarding feature and doing it manually via SSH:
```sh
ssh -L 9090:localhost:9090 -L 5173:localhost:5173 user@host
```
## Contributing Guidelines
Thanks for your interest in contributing to the Invoke Web UI!
Please follow these guidelines when contributing.
### Check in before investing your time
Please check in before you invest your time on anything besides a trivial fix, in case it conflicts with ongoing work or isn't aligned with the vision for the app.
If a feature request or issue doesn't already exist for the thing you want to work on, please create one.
Ping `@psychedelicious` on [discord] in the `#frontend-dev` channel or in the feature request / issue you want to work on - we're happy chat.
### Code conventions
- This is a fairly complex app with a deep component tree. Please use memoization (`useCallback`, `useMemo`, `memo`) with enthusiasm.
- If you need to add some global, ephemeral state, please use [nanostores] if possible.
- Be careful with your redux selectors. If they need to be parameterized, consider creating them inside a `useMemo`.
- Feel free to use `lodash` (via `lodash-es`) to make the intent of your code clear.
- Please add comments describing the "why", not the "how" (unless it is really arcane).
### Commit format
Please use the [conventional commits] spec for the web UI, with a scope of "ui":
- `chore(ui): bump deps`
- `chore(ui): lint`
- `feat(ui): add some cool new feature`
- `fix(ui): fix some bug`
### Submitting a PR
- Ensure your branch is tidy. Use an interactive rebase to clean up the commit history and reword the commit messages if they are not descriptive.
- Run `pnpm lint`. Some issues are auto-fixable with `pnpm fix`.
- Fill out the PR form when creating the PR.
- It doesn't need to be super detailed, but a screenshot or video is nice if you changed something visually.
- If a section isn't relevant, delete it. There are no UI tests at this time.
## Other docs
- [Workflows - Design and Implementation]
- [State Management]
[node]: https://nodejs.org/en/download/
[pnpm]: https://github.com/pnpm/pnpm
[discord]: https://discord.gg/ZmtBAhwWhy
[i18next]: https://github.com/i18next/react-i18next
[Weblate]: https://hosted.weblate.org/engage/invokeai/
[openapi-typescript]: https://github.com/drwpow/openapi-typescript
[Type generation]: #type-generation
[schema.ts]: ../src/services/api/schema.ts
[conventional commits]: https://www.conventionalcommits.org/en/v1.0.0/
[Workflows - Design and Implementation]: ./docs/WORKFLOWS_DESIGN_IMPLEMENTATION.md
[State Management]: ./docs/STATE_MGMT.md

View File

@@ -1,154 +0,0 @@
# InvokeAI Web UI
<!-- @import "[TOC]" {cmd="toc" depthFrom=1 depthTo=6 orderedList=false} -->
<!-- code_chunk_output -->
- [InvokeAI Web UI](#invokeai-web-ui)
- [Core Libraries](#core-libraries)
- [Redux Toolkit](#redux-toolkit)
- [Socket\.IO](#socketio)
- [Chakra UI](#chakra-ui)
- [KonvaJS](#konvajs)
- [Vite](#vite)
- [i18next & Weblate](#i18next--weblate)
- [openapi-typescript](#openapi-typescript)
- [reactflow](#reactflow)
- [zod](#zod)
- [Client Types Generation](#client-types-generation)
- [Package Scripts](#package-scripts)
- [Contributing](#contributing)
- [Dev Environment](#dev-environment)
- [VSCode Remote Dev](#vscode-remote-dev)
- [Production builds](#production-builds)
<!-- /code_chunk_output -->
The UI is a fairly straightforward Typescript React app.
## Core Libraries
InvokeAI's UI is made possible by a number of excellent open-source libraries. The most heavily-used are listed below, but there are many others.
### Redux Toolkit
[Redux Toolkit] is used for state management and fetching/caching:
- `RTK-Query` for data fetching and caching
- `createAsyncThunk` for a couple other HTTP requests
- `createEntityAdapter` to normalize things like images and models
- `createListenerMiddleware` for async workflows
We use [redux-remember] for persistence.
### Socket\.IO
[Socket.IO] is used for server-to-client events, like generation process and queue state changes.
### Chakra UI
[Chakra UI] is our primary UI library, but we also use a few components from [Mantine v6].
### KonvaJS
[KonvaJS] powers the canvas. In the future, we'd like to explore [PixiJS] or WebGPU.
### Vite
[Vite] is our bundler.
### i18next & Weblate
We use [i18next] for localization, but translation to languages other than English happens on our [Weblate] project. **Only the English source strings should be changed on this repo.**
### openapi-typescript
[openapi-typescript] is used to generate types from the server's OpenAPI schema. See TYPES_CODEGEN.md.
### reactflow
[reactflow] powers the Workflow Editor.
### zod
[zod] schemas are used to model data structures and provide runtime validation.
## Client Types Generation
We use [openapi-typescript] to generate types from the app's OpenAPI schema.
The generated types are written to `invokeai/frontend/web/src/services/api/schema.d.ts`. This file is committed to the repo.
The server must be started and available at <http://127.0.0.1:9090>.
```sh
# from the repo root, start the server
python scripts/invokeai-web.py
# from invokeai/frontend/web/, run the script
pnpm typegen
```
## Package Scripts
See `package.json` for all scripts.
Run with `pnpm <script name>`.
- `dev`: run the frontend in dev mode, enabling hot reloading
- `build`: run all checks (madge, eslint, prettier, tsc) and then build the frontend
- `typegen`: generate types from the OpenAPI schema (see [Client Types Generation](#client-types-generation))
- `lint:madge`: check frontend for circular dependencies
- `lint:eslint`: check frontend for code quality
- `lint:prettier`: check frontend for code formatting
- `lint:tsc`: check frontend for type issues
- `lint`: run all checks concurrently
- `fix`: run `eslint` and `prettier`, fixing fixable issues
## Contributing
Thanks for your interest in contributing to the InvokeAI Web UI!
We encourage you to ping @psychedelicious and @blessedcoolant on [discord] if you want to contribute, just to touch base and ensure your work doesn't conflict with anything else going on. The project is very active.
### Dev Environment
Install [node] and [pnpm].
From `invokeai/frontend/web/` run `pnpm i` to get everything set up.
Start everything in dev mode:
1. Start the dev server: `pnpm dev`
2. Start the InvokeAI Nodes backend: `python scripts/invokeai-web.py # run from the repo root`
3. Point your browser to the dev server address e.g. <http://localhost:5173/>
#### VSCode Remote Dev
We've noticed an intermittent issue with the VSCode Remote Dev port forwarding. If you use this feature of VSCode, you may intermittently click the Invoke button and then get nothing until the request times out. Suggest disabling the IDE's port forwarding feature and doing it manually via SSH:
`ssh -L 9090:localhost:9090 -L 5173:localhost:5173 user@host`
### Production builds
For a number of technical and logistical reasons, we need to commit UI build artefacts to the repo.
If you submit a PR, there is a good chance we will ask you to include a separate commit with a build of the app.
To build for production, run `pnpm build`.
[node]: https://nodejs.org/en/download/
[pnpm]: https://github.com/pnpm/pnpm
[discord]: https://discord.gg/ZmtBAhwWhy
[Redux Toolkit]: https://github.com/reduxjs/redux-toolkit
[redux-remember]: https://github.com/zewish/redux-remember
[Socket.IO]: https://github.com/socketio/socket.io
[Chakra UI]: https://github.com/chakra-ui/chakra-ui
[Mantine v6]: https://v6.mantine.dev/
[KonvaJS]: https://github.com/konvajs/react-konva
[PixiJS]: https://github.com/pixijs/pixijs
[Vite]: https://github.com/vitejs/vite
[i18next]: https://github.com/i18next/react-i18next
[Weblate]: https://hosted.weblate.org/engage/invokeai/
[openapi-typescript]: https://github.com/drwpow/openapi-typescript
[reactflow]: https://github.com/xyflow/xyflow
[zod]: https://github.com/colinhacks/zod

View File

@@ -0,0 +1,38 @@
# State Management
The app makes heavy use of Redux Toolkit, its Query library, and `nanostores`.
## Redux
TODO
## `nanostores`
[nanostores] is a tiny state management library. It provides both imperative and declarative APIs.
### Example
```ts
export const $myStringOption = atom<string | null>(null);
// Outside a component, or within a callback for performance-critical logic
$myStringOption.get();
$myStringOption.set('new value');
// Inside a component
const myStringOption = useStore($myStringOption);
```
### Where to put nanostores
- For global application state, export your stores from `invokeai/frontend/web/src/app/store/nanostores/`.
- For feature state, create a file for the stores next to the redux slice definition (e.g. `invokeai/frontend/web/src/features/myFeature/myFeatureNanostores.ts`).
- For hooks with global state, export the store from the same file the hook is in, or put it next to the hook.
### When to use nanostores
- For non-serializable data that needs to be available throughout the app, use `nanostores` instead of a global.
- For ephemeral global state (i.e. state that does not need to be persisted), use `nanostores` instead of redux.
- For performance-critical code and in callbacks, redux selectors can be problematic due to the declarative reactivity system. Consider refactoring to use `nanostores` if there's a **measurable** performance issue.
[nanostores]: https://github.com/nanostores/nanostores/

View File

@@ -23,7 +23,7 @@
- [Primitive Types](#primitive-types)
- [Complex Types](#complex-types)
- [Collection Types](#collection-types)
- [Polymorphic Types](#polymorphic-types)
- [Collection or Scalar Types](#collection-or-scalar-types)
- [Optional Fields](#optional-fields)
- [Building Field Input Templates](#building-field-input-templates)
- [Building Field Output Templates](#building-field-output-templates)

View File

@@ -19,8 +19,8 @@
"dist"
],
"scripts": {
"dev": "concurrently \"vite dev\" \"pnpm run theme:watch\"",
"dev:host": "concurrently \"vite dev --host\" \"pnpm run theme:watch\"",
"dev": "vite dev",
"dev:host": "vite dev --host",
"build": "pnpm run lint && vite build",
"typegen": "node scripts/typegen.js",
"preview": "vite preview",
@@ -31,9 +31,6 @@
"lint": "concurrently -g -n eslint,prettier,tsc,madge -c cyan,green,magenta,yellow \"pnpm run lint:eslint\" \"pnpm run lint:prettier\" \"pnpm run lint:tsc\" \"pnpm run lint:madge\"",
"fix": "eslint --fix . && prettier --log-level warn --write .",
"preinstall": "npx only-allow pnpm",
"postinstall": "pnpm run theme",
"theme": "chakra-cli tokens node_modules/@invoke-ai/ui",
"theme:watch": "chakra-cli tokens node_modules/@invoke-ai/ui --watch",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"unimported": "npx unimported"
@@ -52,21 +49,12 @@
}
},
"dependencies": {
"@chakra-ui/anatomy": "^2.2.2",
"@chakra-ui/icons": "^2.1.1",
"@chakra-ui/layout": "^2.3.1",
"@chakra-ui/portal": "^2.1.0",
"@chakra-ui/react": "^2.8.2",
"@chakra-ui/react-use-size": "^2.1.0",
"@chakra-ui/styled-system": "^2.9.2",
"@chakra-ui/theme-tools": "^2.1.2",
"@dagrejs/graphlib": "^2.1.13",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@fontsource-variable/inter": "^5.0.16",
"@invoke-ai/ui": "0.0.10",
"@invoke-ai/ui-library": "^0.0.18",
"@mantine/form": "6.0.21",
"@nanostores/react": "^0.7.1",
"@reduxjs/toolkit": "2.0.1",
@@ -116,7 +104,6 @@
"zod-validation-error": "^3.0.0"
},
"peerDependencies": {
"@chakra-ui/cli": "^2.4.1",
"@chakra-ui/react": "^2.8.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -124,7 +111,8 @@
},
"devDependencies": {
"@arthurgeron/eslint-plugin-react-usememo": "^2.2.3",
"@chakra-ui/cli": "^2.4.1",
"@invoke-ai/eslint-config-react": "^0.0.12",
"@invoke-ai/prettier-config-react": "^0.0.6",
"@storybook/addon-docs": "^7.6.10",
"@storybook/addon-essentials": "^7.6.10",
"@storybook/addon-interactions": "^7.6.10",

File diff suppressed because it is too large Load Diff

View File

@@ -110,7 +110,28 @@
"somethingWentWrong": "Etwas ist schief gelaufen",
"copyError": "$t(gallery.copy) Fehler",
"input": "Eingabe",
"notInstalled": "Nicht $t(common.installed)"
"notInstalled": "Nicht $t(common.installed)",
"advancedOptions": "Erweiterte Einstellungen",
"alpha": "Alpha",
"red": "Rot",
"green": "Grün",
"blue": "Blau",
"delete": "Löschen",
"or": "oder",
"direction": "Richtung",
"free": "Frei",
"save": "Speichern",
"preferencesLabel": "Präferenzen",
"created": "Erstellt",
"prevPage": "Vorherige Seite",
"nextPage": "Nächste Seite",
"unknownError": "Unbekannter Fehler",
"unsaved": "Nicht gespeichert",
"aboutDesc": "Verwenden Sie Invoke für die Arbeit? Dann siehe hier:",
"localSystem": "Lokales System",
"orderBy": "Ordnen nach",
"saveAs": "Speicher als",
"updated": "Aktualisiert"
},
"gallery": {
"generations": "Erzeugungen",
@@ -701,7 +722,8 @@
"invokeProgressBar": "Invoke Fortschrittsanzeige",
"mode": "Modus",
"resetUI": "$t(accessibility.reset) von UI",
"createIssue": "Ticket erstellen"
"createIssue": "Ticket erstellen",
"about": "Über"
},
"boards": {
"autoAddBoard": "Automatisches Hinzufügen zum Ordner",
@@ -809,7 +831,8 @@
"canny": "Canny",
"hedDescription": "Ganzheitlich verschachtelte Kantenerkennung",
"scribble": "Scribble",
"maxFaces": "Maximal Anzahl Gesichter"
"maxFaces": "Maximal Anzahl Gesichter",
"resizeSimple": "Größe ändern (einfach)"
},
"queue": {
"status": "Status",
@@ -999,5 +1022,27 @@
"selectLoRA": "Wählen ein LoRA aus",
"esrganModel": "ESRGAN Modell",
"addLora": "LoRA hinzufügen"
},
"accordions": {
"generation": {
"title": "Erstellung",
"modelTab": "Modell",
"conceptsTab": "Konzepte"
},
"image": {
"title": "Bild"
},
"advanced": {
"title": "Erweitert"
},
"control": {
"title": "Kontrolle",
"controlAdaptersTab": "Kontroll Adapter",
"ipTab": "Bild Beschreibung"
},
"compositing": {
"coherenceTab": "Kohärenzpass",
"infillTab": "Füllung"
}
}
}

View File

@@ -86,6 +86,7 @@
"back": "Back",
"batch": "Batch Manager",
"cancel": "Cancel",
"copy": "Copy",
"copyError": "$t(gallery.copy) Error",
"close": "Close",
"on": "On",
@@ -224,6 +225,7 @@
"amult": "a_mult",
"autoConfigure": "Auto configure processor",
"balanced": "Balanced",
"base": "Base",
"beginEndStepPercent": "Begin / End Step Percentage",
"bgth": "bg_th",
"canny": "Canny",
@@ -237,6 +239,8 @@
"controlMode": "Control Mode",
"crop": "Crop",
"delete": "Delete",
"depthAnything": "Depth Anything",
"depthAnythingDescription": "Depth map generation using the Depth Anything technique",
"depthMidas": "Depth (Midas)",
"depthMidasDescription": "Depth map generation using Midas",
"depthZoe": "Depth (Zoe)",
@@ -256,6 +260,7 @@
"colorMapTileSize": "Tile Size",
"importImageFromCanvas": "Import Image From Canvas",
"importMaskFromCanvas": "Import Mask From Canvas",
"large": "Large",
"lineart": "Lineart",
"lineartAnime": "Lineart Anime",
"lineartAnimeDescription": "Anime-style lineart processing",
@@ -268,6 +273,7 @@
"minConfidence": "Min Confidence",
"mlsd": "M-LSD",
"mlsdDescription": "Minimalist Line Segment Detector",
"modelSize": "Model Size",
"none": "None",
"noneDescription": "No processing applied",
"normalBae": "Normal BAE",
@@ -288,6 +294,7 @@
"selectModel": "Select a model",
"setControlImageDimensions": "Set Control Image Dimensions To W/H",
"showAdvanced": "Show Advanced",
"small": "Small",
"toggleControlNet": "Toggle this ControlNet",
"w": "W",
"weight": "Weight",
@@ -600,6 +607,10 @@
"desc": "Send current image to Image to Image",
"title": "Send To Image To Image"
},
"remixImage": {
"desc": "Use all parameters except seed from the current image",
"title": "Remix image"
},
"setParameters": {
"desc": "Use all parameters of the current image",
"title": "Set Parameters"
@@ -1216,6 +1227,7 @@
"useCpuNoise": "Use CPU Noise",
"cpuNoise": "CPU Noise",
"gpuNoise": "GPU Noise",
"remixImage": "Remix Image",
"useInitImg": "Use Initial Image",
"usePrompt": "Use Prompt",
"useSeed": "Use Seed",
@@ -1452,9 +1464,7 @@
},
"compositingCoherencePass": {
"heading": "Coherence Pass",
"paragraphs": [
"A second round of denoising helps to composite the Inpainted/Outpainted image."
]
"paragraphs": ["A second round of denoising helps to composite the Inpainted/Outpainted image."]
},
"compositingCoherenceMode": {
"heading": "Mode",
@@ -1462,10 +1472,7 @@
},
"compositingCoherenceSteps": {
"heading": "Steps",
"paragraphs": [
"Number of denoising steps used in the Coherence Pass.",
"Same as the main Steps parameter."
]
"paragraphs": ["Number of denoising steps used in the Coherence Pass.", "Same as the main Steps parameter."]
},
"compositingStrength": {
"heading": "Strength",
@@ -1487,15 +1494,11 @@
},
"controlNetControlMode": {
"heading": "Control Mode",
"paragraphs": [
"Lends more weight to either the prompt or ControlNet."
]
"paragraphs": ["Lends more weight to either the prompt or ControlNet."]
},
"controlNetResizeMode": {
"heading": "Resize Mode",
"paragraphs": [
"How the ControlNet image will be fit to the image output size."
]
"paragraphs": ["How the ControlNet image will be fit to the image output size."]
},
"controlNet": {
"heading": "ControlNet",
@@ -1505,9 +1508,7 @@
},
"controlNetWeight": {
"heading": "Weight",
"paragraphs": [
"How strongly the ControlNet will impact the generated image."
]
"paragraphs": ["How strongly the ControlNet will impact the generated image."]
},
"dynamicPrompts": {
"heading": "Dynamic Prompts",
@@ -1519,9 +1520,7 @@
},
"dynamicPromptsMaxPrompts": {
"heading": "Max Prompts",
"paragraphs": [
"Limits the number of prompts that can be generated by Dynamic Prompts."
]
"paragraphs": ["Limits the number of prompts that can be generated by Dynamic Prompts."]
},
"dynamicPromptsSeedBehaviour": {
"heading": "Seed Behaviour",
@@ -1538,9 +1537,7 @@
},
"lora": {
"heading": "LoRA Weight",
"paragraphs": [
"Higher LoRA weight will lead to larger impacts on the final image."
]
"paragraphs": ["Higher LoRA weight will lead to larger impacts on the final image."]
},
"noiseUseCPU": {
"heading": "Use CPU Noise",
@@ -1552,9 +1549,7 @@
},
"paramCFGScale": {
"heading": "CFG Scale",
"paragraphs": [
"Controls how much your prompt influences the generation process."
]
"paragraphs": ["Controls how much your prompt influences the generation process."]
},
"paramCFGRescaleMultiplier": {
"heading": "CFG Rescale Multiplier",
@@ -1606,9 +1601,7 @@
},
"paramVAE": {
"heading": "VAE",
"paragraphs": [
"Model used for translating AI output into the final image."
]
"paragraphs": ["Model used for translating AI output into the final image."]
},
"paramVAEPrecision": {
"heading": "VAE Precision",
@@ -1697,6 +1690,7 @@
"workflowLibrary": "Library",
"userWorkflows": "My Workflows",
"defaultWorkflows": "Default Workflows",
"projectWorkflows": "Project Workflows",
"openWorkflow": "Open Workflow",
"uploadWorkflow": "Load from File",
"deleteWorkflow": "Delete Workflow",
@@ -1709,6 +1703,7 @@
"workflowSaved": "Workflow Saved",
"noRecentWorkflows": "No Recent Workflows",
"noUserWorkflows": "No User Workflows",
"noWorkflows": "No Workflows",
"noSystemWorkflows": "No System Workflows",
"problemLoading": "Problem Loading Workflows",
"loading": "Loading Workflows",

View File

@@ -118,7 +118,14 @@
"advancedOptions": "Opzioni avanzate",
"free": "Libero",
"or": "o",
"preferencesLabel": "Preferenze"
"preferencesLabel": "Preferenze",
"red": "Rosso",
"aboutHeading": "Possiedi il tuo potere creativo",
"aboutDesc": "Utilizzi Invoke per lavoro? Guarda qui:",
"localSystem": "Sistema locale",
"green": "Verde",
"blue": "Blu",
"alpha": "Alfa"
},
"gallery": {
"generations": "Generazioni",
@@ -377,7 +384,11 @@
"desc": "Apre e chiude le opzioni e i pannelli della galleria",
"title": "Attiva/disattiva le Opzioni e la Galleria"
},
"clearSearch": "Cancella ricerca"
"clearSearch": "Cancella ricerca",
"remixImage": {
"desc": "Utilizza tutti i parametri tranne il seme dell'immagine corrente",
"title": "Remixa l'immagine"
}
},
"modelManager": {
"modelManager": "Gestione Modelli",
@@ -521,7 +532,8 @@
"customConfigFileLocation": "Posizione del file di configurazione personalizzato",
"vaePrecision": "Precisione VAE",
"noModelSelected": "Nessun modello selezionato",
"conversionNotSupported": "Conversione non supportata"
"conversionNotSupported": "Conversione non supportata",
"configFile": "File di configurazione"
},
"parameters": {
"images": "Immagini",
@@ -660,7 +672,10 @@
"lockAspectRatio": "Blocca proporzioni",
"swapDimensions": "Scambia dimensioni",
"aspect": "Aspetto",
"setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (potrebbe essere troppo grande)"
"setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (potrebbe essere troppo grande)",
"boxBlur": "Box",
"gaussianBlur": "Gaussian",
"remixImage": "Remixa l'immagine"
},
"settings": {
"models": "Modelli",
@@ -794,7 +809,9 @@
"invalidUpload": "Caricamento non valido",
"problemDeletingWorkflow": "Problema durante l'eliminazione del flusso di lavoro",
"workflowDeleted": "Flusso di lavoro eliminato",
"problemRetrievingWorkflow": "Problema nel recupero del flusso di lavoro"
"problemRetrievingWorkflow": "Problema nel recupero del flusso di lavoro",
"resetInitialImage": "Reimposta l'immagine iniziale",
"uploadInitialImage": "Carica l'immagine iniziale"
},
"tooltip": {
"feature": {
@@ -899,7 +916,8 @@
"loadMore": "Carica altro",
"mode": "Modalità",
"resetUI": "$t(accessibility.reset) l'Interfaccia Utente",
"createIssue": "Segnala un problema"
"createIssue": "Segnala un problema",
"about": "Informazioni"
},
"ui": {
"hideProgressImages": "Nascondi avanzamento immagini",
@@ -1232,7 +1250,11 @@
"scribble": "Scarabocchio",
"amult": "Angolo di illuminazione",
"coarse": "Approssimativo",
"resizeSimple": "Ridimensiona (semplice)"
"resizeSimple": "Ridimensiona (semplice)",
"large": "Grande",
"small": "Piccolo",
"depthAnythingDescription": "Generazione di mappe di profondità utilizzando la tecnica Depth Anything",
"modelSize": "Dimensioni del modello"
},
"queue": {
"queueFront": "Aggiungi all'inizio della coda",
@@ -1664,7 +1686,9 @@
"userWorkflows": "I miei flussi di lavoro",
"newWorkflowCreated": "Nuovo flusso di lavoro creato",
"downloadWorkflow": "Salva su file",
"uploadWorkflow": "Carica da file"
"uploadWorkflow": "Carica da file",
"projectWorkflows": "Flussi di lavoro del progetto",
"noWorkflows": "Nessun flusso di lavoro"
},
"app": {
"storeNotInitialized": "Il negozio non è inizializzato"

View File

@@ -1,30 +1,36 @@
{
"accessibility": {
"invokeProgressBar": "Invoke ilerleme durumu",
"nextImage": "Sonraki Resim",
"useThisParameter": "Kullanıcı parametreleri",
"nextImage": "Sonraki İmaj",
"useThisParameter": "Bu ayarları kullan",
"copyMetadataJson": "Metadata verilerini kopyala (JSON)",
"exitViewer": "Görüntüleme Modundan Çık",
"zoomIn": "Yakınlaştır",
"zoomOut": "Uzaklaştır",
"rotateCounterClockwise": "Döndür (Saat yönünün tersine)",
"rotateClockwise": "Döndür (Saat yönünde)",
"rotateCounterClockwise": "Saat yönünün tersine döndür",
"rotateClockwise": "Saat yönüne döndür",
"flipHorizontally": "Yatay Çevir",
"flipVertically": "Dikey Çevir",
"modifyConfig": "Ayarları Değiştir",
"toggleAutoscroll": "Otomatik kaydırmayı aç/kapat",
"toggleLogViewer": "Günlük Görüntüleyici/Kapa",
"showOptionsPanel": "Ayarlar Panelini Göster",
"modelSelect": "Model Seçin",
"reset": "Sıfırla",
"uploadImage": "Resim Yükle",
"previousImage": "Önceki Resim",
"menu": "Menü"
"toggleAutoscroll": "Otomatik kaydırmayı Aç-Kapat",
"toggleLogViewer": "Günlüğü-Kapat",
"showOptionsPanel": "Yan Paneli Göster",
"modelSelect": "Model Seçimi",
"reset": "Resetle",
"uploadImage": "İmaj Yükle",
"previousImage": "Önceki İmaj",
"menu": "Menü",
"about": "Hakkında",
"mode": "Kip",
"resetUI": "$t(accessibility.reset)Arayüz",
"showGalleryPanel": "Galeri Panelini Göster",
"loadMore": "Daha Getir",
"createIssue": "Sorun Bildir"
},
"common": {
"hotkeysLabel": "Kısayol Tuşları",
"languagePickerLabel": "Dil Seçimi",
"reportBugLabel": "Hata Bildir",
"languagePickerLabel": "Dil",
"reportBugLabel": "Sorun Bildir",
"githubLabel": "Github",
"discordLabel": "Discord",
"settingsLabel": "Ayarlar",
@@ -37,22 +43,324 @@
"langJapanese": "Japonca",
"langPolish": "Lehçe",
"langPortuguese": "Portekizce",
"langBrPortuguese": "Portekizcr (Brezilya)",
"langBrPortuguese": "Portekizce (Brezilya)",
"langRussian": "Rusça",
"langSimplifiedChinese": "Çince (Basit)",
"langUkranian": "Ukraynaca",
"langSpanish": "İspanyolca",
"txt2img": "Metinden Resime",
"img2img": "Resimden Metine",
"linear": "Çizgisel",
"nodes": "Düğümler",
"postprocessing": "İşlem Sonrası",
"postProcessing": "İşlem Sonrası",
"postProcessDesc2": "Daha gelişmiş özellikler için ve iş akışını kolaylaştırmak için özel bir kullanıcı arayüzü çok yakında yayınlanacaktır.",
"postProcessDesc3": "Invoke AI komut satırı arayüzü, bir çok yeni özellik sunmaktadır.",
"txt2img": "Yazıdan İmaj",
"img2img": "İmajdan İmaj",
"linear": "Doğrusal",
"nodes": "İş Akış Düzenleyici",
"postprocessing": "Rötuş",
"postProcessing": "Rötuş",
"postProcessDesc2": "Daha gelişmiş iş akışlarına olanak sağlayacak özel bir arayüz yakında yayınlanacaktır.",
"postProcessDesc3": "Invoke AI Komut Satırı Arayüzü, Embiggen dahil birçok yeni özellik sunmaktadır.",
"langKorean": "Korece",
"unifiedCanvas": "Akıllı Tuval",
"nodesDesc": "Görüntülerin oluşturulmasında hazırladığımız yeni bir sistem geliştirme aşamasındadır. Bu harika özellikler ve çok daha fazlası için bizi takip etmeye devam edin.",
"postProcessDesc1": "Invoke AI son kullanıcıya yönelik bir çok özellik sunar. Görüntü kalitesi yükseltme, yüz restorasyonu WebUI üzerinden kullanılabilir. Metinden resime ve resimden metne araçlarına gelişmiş seçenekler menüsünden ulaşabilirsiniz. İsterseniz mevcut görüntü ekranının üzerindeki veya görüntüleyicideki görüntüyü doğrudan düzenleyebilirsiniz."
"nodesDesc": "İmaj oluşturmak için hazırladığımız çizge tabanlı sistem şu an geliştirme aşamasındadır. Bu harika özellik hakkındaki gelişmeler için bizi takip etmeye devam edin.",
"postProcessDesc1": "Invoke AI birçok rötuş (post-process) aracı sağlar. İmaj büyütme ve yüz iyileştirme halihazırda WebUI üzerinden kullanılabilir. Bunlara Yazıdan İmaj ve İmajdan İmaj sekmelerindeki Gelişmiş Ayarlar menüsünden ulaşabilirsiniz. İsterseniz mevcut görüntü ekranının üzerindeki veya görüntüleyicideki imajı doğrudan üstteki tuşlar yardımıyla düzenleyebilirsiniz.",
"batch": "Toplu İş Yöneticisi",
"accept": "Onayla",
"cancel": "Vazgeç",
"advanced": "Gelişmiş",
"copyError": "$t(gallery.copy) Hata",
"on": "Açık",
"or": "ya da",
"aboutDesc": "Invoke'u iş için mi kullanıyorsunuz? Şuna bir göz atın:",
"advancedOptions": "Gelişmiş Ayarlar",
"ai": "yapay zeka",
"close": "Kapat",
"auto": "Otomatik",
"communityLabel": "Topluluk",
"back": "Geri",
"areYouSure": "Emin misiniz?",
"notInstalled": "$t(common.installed) Değil",
"openInNewTab": "Yeni Sekmede Aç",
"aboutHeading": "Yaratıcı Gücünüzün Sahibi Olun",
"lightMode": "Açık Tema",
"load": "Yükle",
"loading": "Yükleniyor",
"loadingInvokeAI": "Invoke AI Yükleniyor",
"localSystem": "Yerel Sistem",
"inpaint": "içboyama",
"modelManager": "Model Yöneticisi",
"orderBy": "Sırala",
"outpaint": "dışboyama",
"outputs": ıktılar",
"langHebrew": "İbranice",
"learnMore": "Bilgi Edin",
"nodeEditor": "Çizge Düzenleyici",
"save": "Kaydet",
"statusMergingModels": "Modeller Birleştiriliyor",
"statusGenerating": "Oluşturuluyor",
"statusGenerationComplete": "Oluşturma Tamamlandı",
"statusGeneratingOutpainting": "Dışboyama Oluşturuluyor",
"statusLoadingModel": "Model Yükleniyor",
"random": "Rastgele",
"simple": "Basit",
"preferencesLabel": "Seçenekler",
"statusConnected": "Bağlandı",
"statusMergedModels": "Modeller Birleştirildi",
"statusModelChanged": "Model Değişti",
"statusModelConverted": "Model Dönüştürüldü",
"statusPreparing": "Hazırlanıyor",
"statusProcessing": "İşleniyor",
"statusProcessingCanceled": "İşlemden Vazgeçildi",
"statusRestoringFacesCodeFormer": "Yüzler İyileştiriliyor (CodeFormer)",
"statusRestoringFacesGFPGAN": "Yüzler İyileştiriliyor (GFPGAN)",
"template": "Şablon",
"saveAs": "Farklı Kaydet",
"statusProcessingComplete": "İşlem Tamamlandı",
"statusSavingImage": "İmaj Kaydediliyor",
"somethingWentWrong": "Bir sorun oluştu",
"statusConvertingModel": "Model Dönüştürülüyor",
"statusDisconnected": "Bağlantı Kesildi",
"statusError": "Hata",
"statusGeneratingImageToImage": "İmajdan İmaj Oluşturuluyor",
"statusGeneratingInpainting": "İçboyama Oluşturuluyor",
"statusRestoringFaces": "Yüzler İyileştiriliyor",
"statusUpscaling": "Büyütme",
"statusUpscalingESRGAN": "Büyütme (ESRGAN)",
"training": "Eğitim",
"statusGeneratingTextToImage": "Yazıdan İmaj Oluşturuluyor",
"imagePrompt": "Resim İstemi",
"unknown": "Bilinmeyen",
"green": "Yeşil",
"red": "Kırmızı",
"blue": "Mavi",
"alpha": "Alfa",
"file": "Dosya",
"folder": "Klasör",
"format": "biçim",
"details": "Ayrıntılar",
"error": "Hata",
"generate": "Oluştur",
"free": "Serbest",
"imageFailedToLoad": "İmaj Yüklenemedi",
"safetensors": "Safetensor",
"upload": "Yükle",
"nextPage": "Sonraki Sayfa",
"prevPage": "Önceki Sayfa",
"dontAskMeAgain": "Bir daha sorma",
"delete": "Kaldır",
"direction": "Yön",
"darkMode": "Koyu Tema",
"unsaved": "Kaydedilmemiş",
"unknownError": "Bilinmeyen Hata"
},
"accordions": {
"generation": {
"title": "Oluşturma",
"modelTab": "Model",
"conceptsTab": "Konseptler"
},
"image": {
"title": "İmaj"
},
"advanced": {
"title": "Gelişmiş"
},
"compositing": {
"title": "Birleştirme",
"coherenceTab": "Uyum Geçişi",
"infillTab": "Doldurma"
},
"control": {
"ipTab": "Resim İstemleri"
}
},
"boards": {
"autoAddBoard": "Panoya Otomatik Ekleme",
"cancel": "Vazgeç",
"clearSearch": "Aramayı Sil",
"deleteBoard": "Panoyu Sil",
"loading": "Yükleniyor...",
"myBoard": "Panom",
"selectBoard": "Bir Pano Seç",
"addBoard": "Pano Ekle",
"deleteBoardAndImages": "Panoyu ve İmajları Sil",
"deleteBoardOnly": "Sadece Panoyu Sil",
"deletedBoardsCannotbeRestored": "Silinen panolar geri getirilemez",
"menuItemAutoAdd": "Bu panoya otomatik olarak ekle",
"move": "Taşı",
"movingImagesToBoard_one": "{{count}} imajı şu panoya taşı:",
"movingImagesToBoard_other": "{{count}} imajı şu panoya taşı:",
"noMatching": "Eşleşen pano yok",
"searchBoard": "Pano Ara...",
"topMessage": "Bu pano, şu özelliklerde kullanılan imajlar içeriyor:",
"downloadBoard": "Panoyu İndir",
"uncategorized": "Kategorisiz",
"changeBoard": "Panoyu Değiştir",
"bottomMessage": "Bu panoyu ve imajları silmek, bunları kullanan özelliklerin resetlemesine neden olacaktır."
},
"controlnet": {
"balanced": "Dengeli",
"contentShuffle": "İçerik Karma",
"contentShuffleDescription": "İmajın içeriğini karıştırır",
"depthZoe": "Derinlik (Zoe)",
"depthZoeDescription": "Zoe kullanarak derinlik haritası oluşturma",
"resizeMode": "Boyutlandırma Kipi",
"addControlNet": "$t(common.controlNet) Ekle",
"addIPAdapter": "$t(common.ipAdapter) Ekle",
"addT2IAdapter": "$t(common.t2iAdapter) Ekle",
"controlNetEnabledT2IDisabled": "$t(common.controlNet) etkin, $t(common.t2iAdapter)s etkin değil",
"t2iEnabledControlNetDisabled": "$t(common.t2iAdapter) etkin, $t(common.controlNet)s etkin değil",
"colorMap": "Renk",
"crop": "Kırpma",
"delete": "Kaldır",
"depthMidas": "Derinlik (Midas)",
"depthMidasDescription": "Midas kullanarak derinlik haritası oluşturma",
"detectResolution": "Çözünürlüğü Bul",
"none": "Hiçbiri",
"noneDescription": "Hiçbir işlem uygulanmamış",
"selectModel": "Model seçin",
"showAdvanced": "Gelişmiş Ayarları Göster",
"controlNetT2IMutexDesc": "$t(common.controlNet) ve $t(common.t2iAdapter)'nün beraber kullanımı henüz desteklenmiyor.",
"canny": "Canny",
"colorMapDescription": "İmajdan bir renk haritası oluşturur",
"handAndFace": "El ve Yüz",
"processor": "İşlemci",
"prompt": "İstem",
"duplicate": "Kopyala",
"large": "Büyük",
"modelSize": "Model Boyutu",
"resize": "Boyutlandır",
"resizeSimple": "Boyutlandır (Basit)",
"safe": "Güvenli",
"small": "Küçük",
"weight": "Etki",
"cannyDescription": "Canny kenar algılama",
"fill": "Doldur",
"highThreshold": "Üst Eşik",
"imageResolution": "İmaj Çözünürlüğü",
"colorMapTileSize": "Karo Boyutu",
"importImageFromCanvas": "Tuvalden İmajı içe Aktar",
"importMaskFromCanvas": "Tuvalden Maskeyi İçe Aktar",
"lowThreshold": "Alt Eşik",
"base": "Taban",
"depthAnythingDescription": "Depth Anything tekniği ile derinlik haritası oluşturma"
},
"queue": {
"queuedCount": "{{pending}} Sırada",
"resumeSucceeded": "İşlem Sürdürüldü",
"openQueue": "Sırayı Göster",
"cancelSucceeded": "Öğeden Vazgeçildi",
"cancelFailed": "Öğeden Vazgeçmede Sorun",
"prune": "Arındır",
"pruneTooltip": "{{item_count}} Bitmiş İşi Sil",
"resumeFailed": "İşlemi Sürdürmede Sorun",
"pauseFailed": "İşlemi Duraklatmada Sorun",
"cancelBatchSucceeded": "Toplu İşten Vazgeçildi",
"pruneSucceeded": "{{item_count}} Bitmiş İş Sıradan Kaldırıldı",
"in_progress": "İşleniyor",
"completed": "Bitti",
"canceled": "Vazgeçildi",
"back": "arka",
"queueFront": "Sıranın Başına Ekle",
"queueBack": "Sıraya Ekle",
"resumeTooltip": "İşlemi Sürdür",
"clearQueueAlertDialog2": "Sırayı boşaltmak istediğinizden emin misiniz?",
"batchQueuedDesc_one": "{{count}} iş sıranın {{direction}} eklendi",
"batchQueuedDesc_other": "{{count}} iş sıranın {{direction}} eklendi",
"batchFailedToQueue": "Toplu İş Sıraya Alınamadı",
"front": "ön",
"queue": "Sıra",
"resume": "Sürdür",
"queueTotal": "Toplam {{total}}",
"queueEmpty": "Sıra Boş",
"clearQueueAlertDialog": "Sırayı boşaltma tuşu halihazırdaki işlemi durdurur ve sırayı tamamen boşaltır.",
"current": "Şu Anki",
"time": "Süre",
"pause": "Duraklat",
"pauseTooltip": "İşlemi Duraklat",
"pruneFailed": "Sırayı Arındırmada Sorun",
"clearTooltip": "Vazgeç ve Tüm Öğeleri Sil",
"clear": "Boşalt",
"cancelBatchFailed": "Toplu İşten Vazgeçmede Sorun",
"next": "Sonraki",
"status": "Durum",
"failed": "Başarısız",
"item": "Öğe",
"enqueueing": "Toplu İş Sıraya Alınıyor",
"pauseSucceeded": "İşlem Duraklatıldı",
"cancel": "Vazgeç",
"cancelTooltip": "Şu Anki Öğeden Vazgeç",
"clearSucceeded": "Sıra Boşaltıldı",
"clearFailed": "Sırayı Boşaltmada Sorun",
"cancelBatch": "Toplu İşten Vazgeç",
"cancelItem": "Öğeden Vazgeç",
"total": "Toplam",
"pending": "Sırada",
"completedIn": "'de bitirildi",
"batch": "Toplu İş",
"session": "Oturum",
"batchQueued": "Toplu İş Sıraya Alındı",
"notReady": "Sıraya Alınamadı"
},
"invocationCache": {
"cacheSize": "Önbellek Boyutu",
"disable": "Kapat",
"clear": "Boşalt",
"maxCacheSize": "Maksimum Önbellek Boyutu",
"useCache": "Önbellek Kullan",
"enable": "Aç"
},
"gallery": {
"deleteImageBin": "Silinen imajlar işletim sisteminin çöp kutusuna gönderilir.",
"deleteImagePermanent": "Silinen imajlar geri getirilemez.",
"assets": "Özkaynaklar",
"autoAssignBoardOnClick": "Tıklanan Panoya Otomatik Atama",
"loading": "Yükleniyor",
"starImage": "Yıldız Koy",
"download": "İndir",
"deleteSelection": "Seçileni Sil",
"preparingDownloadFailed": "İndirme Hazırlanırken Sorun",
"problemDeletingImages": "İmaj Silmede Sorun",
"featuresWillReset": "Bu imajı silerseniz, o özellikler resetlenecektir.",
"galleryImageResetSize": "Boyutu Resetle",
"noImageSelected": "İmaj Seçili Değil",
"unstarImage": "Yıldızı Kaldır",
"uploads": "Yüklemeler",
"problemDeletingImagesDesc": "Bir ya da daha çok imaj silinemedi",
"gallerySettings": "Galeri Ayarları",
"image": "imaj",
"galleryImageSize": "İmaj Boyutu",
"allImagesLoaded": "Bütün İmajlar Yüklendi",
"copy": "Kopyala",
"noImagesInGallery": "Gösterilecek İmaj Yok",
"autoSwitchNewImages": "Yeni İmajı Biter Bitmez Gör",
"maintainAspectRatio": "En-Boy Oranını Koru",
"currentlyInUse": "Bu imaj şu an bu bölümlerde kullanımda:",
"deleteImage": "İmajı Sil",
"loadMore": "Daha Getir",
"setCurrentImage": "Çalışma İmajı Yap",
"unableToLoad": "Galeri Yüklenemedi",
"downloadSelection": "Seçileni İndir",
"preparingDownload": "İndirmeye Hazırlanıyor"
},
"hrf": {
"hrf": "Yüksek Çözünürlük Kürü",
"enableHrf": "Yüksek Çözünürlük Kürünü Aç",
"hrfStrength": "Yüksek Çözünürlük Kürü Etkisi",
"strengthTooltip": "Düşük değerler daha az detaya neden olsa da olası bozuklukları önleyebilir.",
"metadata": {
"enabled": "Yüksek Çözünürlük Kürü Açık",
"strength": "Yüksek Çözünürlük Kürü Etkisi",
"method": "Yüksek Çözünürlük Kürü Yöntemi"
},
"upscaleMethod": "Büyütme Yöntemi",
"enableHrfTooltip": "Daha düşük bir başlangıç çözünürlüğüyle oluşturup ana çözünürlüğe büyütür ve İmajdan İmaj yapar."
},
"hotkeys": {
"noHotkeysFound": "Kısayol Tuşu Bulanamadı",
"searchHotkeys": "Kısayol Tuşlarında Ara",
"clearSearch": "Aramayı Sil"
},
"embedding": {
"incompatibleModel": "Uyumsuz ana model:"
},
"unifiedCanvas": {
"accept": "Onayla"
}
}

View File

@@ -6,9 +6,7 @@ const OPENAPI_URL = 'http://127.0.0.1:9090/openapi.json';
const OUTPUT_FILE = 'src/services/api/schema.ts';
async function main() {
process.stdout.write(
`Generating types "${OPENAPI_URL}" --> "${OUTPUT_FILE}"...`
);
process.stdout.write(`Generating types "${OPENAPI_URL}" --> "${OUTPUT_FILE}"...`);
const types = await openapiTS(OPENAPI_URL, {
exportType: true,
transform: (schemaObject) => {

View File

@@ -1,4 +1,4 @@
import { Box, useGlobalModifiersInit } from '@invoke-ai/ui';
import { Box, useGlobalModifiersInit } from '@invoke-ai/ui-library';
import { useSocketIO } from 'app/hooks/useSocketIO';
import { useLogger } from 'app/logging/useLogger';
import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
@@ -45,8 +45,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => {
useGlobalModifiersInit();
useGlobalHotkeys();
const { dropzone, isHandlingUpload, setIsHandlingUpload } =
useFullscreenDropzone();
const { dropzone, isHandlingUpload, setIsHandlingUpload } = useFullscreenDropzone();
const handleReset = useCallback(() => {
clearStorage();
@@ -70,10 +69,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => {
}, [dispatch]);
return (
<ErrorBoundary
onReset={handleReset}
FallbackComponent={AppErrorBoundaryFallback}
>
<ErrorBoundary onReset={handleReset} FallbackComponent={AppErrorBoundaryFallback}>
<Box
id="invoke-app-wrapper"
w="100vw"
@@ -86,10 +82,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => {
<InvokeTabs />
<AnimatePresence>
{dropzone.isDragActive && isHandlingUpload && (
<ImageUploadOverlay
dropzone={dropzone}
setIsHandlingUpload={setIsHandlingUpload}
/>
<ImageUploadOverlay dropzone={dropzone} setIsHandlingUpload={setIsHandlingUpload} />
)}
</AnimatePresence>
</Box>

View File

@@ -1,12 +1,8 @@
import { Button, Flex, Heading, Link, Text, useToast } from '@invoke-ai/ui';
import { Button, Flex, Heading, Link, Text, useToast } from '@invoke-ai/ui-library';
import newGithubIssueUrl from 'new-github-issue-url';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiArrowCounterClockwiseBold,
PiArrowSquareOutBold,
PiCopyBold,
} from 'react-icons/pi';
import { PiArrowCounterClockwiseBold, PiArrowSquareOutBold, PiCopyBold } from 'react-icons/pi';
import { serializeError } from 'serialize-error';
type Props = {
@@ -37,22 +33,8 @@ const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => {
[error.message, error.name]
);
return (
<Flex
layerStyle="body"
w="100vw"
h="100vh"
alignItems="center"
justifyContent="center"
p={4}
>
<Flex
layerStyle="first"
flexDir="column"
borderRadius="base"
justifyContent="center"
gap={8}
p={16}
>
<Flex layerStyle="body" w="100vw" h="100vh" alignItems="center" justifyContent="center" p={4}>
<Flex layerStyle="first" flexDir="column" borderRadius="base" justifyContent="center" gap={8} p={16}>
<Heading>{t('common.somethingWentWrong')}</Heading>
<Flex
layerStyle="second"
@@ -68,19 +50,14 @@ const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => {
</Text>
</Flex>
<Flex gap={4}>
<Button
leftIcon={<PiArrowCounterClockwiseBold />}
onClick={resetErrorBoundary}
>
<Button leftIcon={<PiArrowCounterClockwiseBold />} onClick={resetErrorBoundary}>
{t('accessibility.resetUI')}
</Button>
<Button leftIcon={<PiCopyBold />} onClick={handleCopy}>
{t('common.copyError')}
</Button>
<Link href={url} isExternal>
<Button leftIcon={<PiArrowSquareOutBold />}>
{t('accessibility.createIssue')}
</Button>
<Button leftIcon={<PiArrowSquareOutBold />}>{t('accessibility.createIssue')}</Button>
</Link>
</Flex>
</Flex>

View File

@@ -10,14 +10,17 @@ import { $customStarUI } from 'app/store/nanostores/customStarUI';
import { $galleryHeader } from 'app/store/nanostores/galleryHeader';
import { $isDebugging } from 'app/store/nanostores/isDebugging';
import { $logo } from 'app/store/nanostores/logo';
import { $openAPISchemaUrl } from 'app/store/nanostores/openAPISchemaUrl';
import { $projectId } from 'app/store/nanostores/projectId';
import { $queueId, DEFAULT_QUEUE_ID } from 'app/store/nanostores/queueId';
import { $store } from 'app/store/nanostores/store';
import { $workflowCategories } from 'app/store/nanostores/workflowCategories';
import { createStore } from 'app/store/store';
import type { PartialAppConfig } from 'app/types/invokeai';
import Loading from 'common/components/Loading/Loading';
import AppDndContext from 'features/dnd/components/AppDndContext';
import type { PropsWithChildren, ReactNode } from 'react';
import type { WorkflowCategory } from 'features/nodes/types/workflow';
import type { PropsWithChildren } from 'react';
import React, { lazy, memo, useEffect, useMemo } from 'react';
import { Provider } from 'react-redux';
import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares';
@@ -28,12 +31,13 @@ const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
interface Props extends PropsWithChildren {
apiUrl?: string;
openAPISchemaUrl?: string;
token?: string;
config?: PartialAppConfig;
customNavComponent?: ReactNode;
customNavComponent?: () => JSX.Element;
middleware?: Middleware[];
projectId?: string;
galleryHeader?: ReactNode;
galleryHeader?: () => JSX.Element;
queueId?: string;
selectedImage?: {
imageName: string;
@@ -42,11 +46,13 @@ interface Props extends PropsWithChildren {
customStarUi?: CustomStarUi;
socketOptions?: Partial<ManagerOptions & SocketOptions>;
isDebugging?: boolean;
logo?: ReactNode;
logo?: () => JSX.Element;
workflowCategories?: WorkflowCategory[];
}
const InvokeAIUI = ({
apiUrl,
openAPISchemaUrl,
token,
config,
customNavComponent,
@@ -59,6 +65,7 @@ const InvokeAIUI = ({
socketOptions,
isDebugging = false,
logo,
workflowCategories,
}: Props) => {
useEffect(() => {
// configure API client token
@@ -123,6 +130,16 @@ const InvokeAIUI = ({
};
}, [customNavComponent]);
useEffect(() => {
if (openAPISchemaUrl) {
$openAPISchemaUrl.set(openAPISchemaUrl);
}
return () => {
$openAPISchemaUrl.set(undefined);
};
}, [openAPISchemaUrl]);
useEffect(() => {
if (galleryHeader) {
$galleryHeader.set(galleryHeader);
@@ -143,6 +160,16 @@ const InvokeAIUI = ({
};
}, [logo]);
useEffect(() => {
if (workflowCategories) {
$workflowCategories.set(workflowCategories);
}
return () => {
$workflowCategories.set([]);
};
}, [workflowCategories]);
useEffect(() => {
if (socketOptions) {
$socketOptions.set(socketOptions);

View File

@@ -1,13 +1,7 @@
import '@fontsource-variable/inter';
import 'overlayscrollbars/overlayscrollbars.css';
import {
ChakraProvider,
DarkMode,
extendTheme,
theme as _theme,
TOAST_OPTIONS,
} from '@invoke-ai/ui';
import { ChakraProvider, DarkMode, extendTheme, theme as _theme, TOAST_OPTIONS } from '@invoke-ai/ui-library';
import type { ReactNode } from 'react';
import { memo, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -1,4 +1,4 @@
import { useToast } from '@invoke-ai/ui';
import { useToast } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { addToast, clearToastQueue } from 'features/system/store/systemSlice';
import type { MakeToastArg } from 'features/system/util/makeToast';
@@ -36,10 +36,7 @@ const Toaster = () => {
*/
export const useAppToaster = () => {
const dispatch = useAppDispatch();
const toaster = useCallback(
(arg: MakeToastArg) => dispatch(addToast(makeToast(arg))),
[dispatch]
);
const toaster = useCallback((arg: MakeToastArg) => dispatch(addToast(makeToast(arg))), [dispatch]);
return toaster;
};

View File

@@ -6,10 +6,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
import type { MapStore } from 'nanostores';
import { atom, map } from 'nanostores';
import { useEffect, useMemo } from 'react';
import type {
ClientToServerEvents,
ServerToClientEvents,
} from 'services/events/types';
import type { ClientToServerEvents, ServerToClientEvents } from 'services/events/types';
import { setEventListeners } from 'services/events/util/setEventListeners';
import type { ManagerOptions, Socket, SocketOptions } from 'socket.io-client';
import { io } from 'socket.io-client';
@@ -45,7 +42,7 @@ export const useSocketIO = () => {
const socketOptions = useMemo(() => {
const options: Partial<ManagerOptions & SocketOptions> = {
timeout: 60000,
path: '/ws/socket.io',
path: baseUrl ? '/ws/socket.io' : `${window.location.pathname}ws/socket.io`,
autoConnect: false, // achtung! removing this breaks the dynamic middleware
forceNew: true,
};
@@ -56,7 +53,7 @@ export const useSocketIO = () => {
}
return { ...options, ...addlSocketOptions };
}, [authToken, addlSocketOptions]);
}, [authToken, addlSocketOptions, baseUrl]);
useEffect(() => {
if ($isSocketInitialized.get()) {
@@ -64,10 +61,7 @@ export const useSocketIO = () => {
return;
}
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io(
socketUrl,
socketOptions
);
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io(socketUrl, socketOptions);
setEventListeners({ dispatch, socket });
socket.connect();

View File

@@ -30,20 +30,11 @@ export type LoggerNamespace =
| 'queue'
| 'dnd';
export const logger = (namespace: LoggerNamespace) =>
$logger.get().child({ namespace });
export const logger = (namespace: LoggerNamespace) => $logger.get().child({ namespace });
export const zLogLevel = z.enum([
'trace',
'debug',
'info',
'warn',
'error',
'fatal',
]);
export const zLogLevel = z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']);
export type LogLevel = z.infer<typeof zLogLevel>;
export const isLogLevel = (v: unknown): v is LogLevel =>
zLogLevel.safeParse(v).success;
export const isLogLevel = (v: unknown): v is LogLevel => zLogLevel.safeParse(v).success;
// Translate human-readable log levels to numbers, used for log filtering
export const LOG_LEVEL_MAP: Record<LogLevel, number> = {

View File

@@ -17,10 +17,7 @@ export const useLogger = (namespace: LoggerNamespace) => {
localStorage.setItem('ROARR_LOG', 'true');
// Use a filter to show only logs of the given level
localStorage.setItem(
'ROARR_FILTER',
`context.logLevel:>=${LOG_LEVEL_MAP[consoleLogLevel]}`
);
localStorage.setItem('ROARR_FILTER', `context.logLevel:>=${LOG_LEVEL_MAP[consoleLogLevel]}`);
} else {
// Disable console log output
localStorage.setItem('ROARR_LOG', 'false');

View File

@@ -1,8 +1,4 @@
import {
createDraftSafeSelectorCreator,
createSelectorCreator,
lruMemoize,
} from '@reduxjs/toolkit';
import { createDraftSafeSelectorCreator, createSelectorCreator, lruMemoize } from '@reduxjs/toolkit';
import type { GetSelectorsOptions } from '@reduxjs/toolkit/dist/entities/state_selectors';
import { isEqual } from 'lodash-es';

View File

@@ -1,19 +1,12 @@
import { StorageError } from 'app/store/enhancers/reduxRemember/errors';
import { $projectId } from 'app/store/nanostores/projectId';
import type { UseStore } from 'idb-keyval';
import {
clear,
createStore as createIDBKeyValStore,
get,
set,
} from 'idb-keyval';
import { clear, createStore as createIDBKeyValStore, get, set } from 'idb-keyval';
import { action, atom } from 'nanostores';
import type { Driver } from 'redux-remember';
// Create a custom idb-keyval store (just needed to customize the name)
export const $idbKeyValStore = atom<UseStore>(
createIDBKeyValStore('invoke', 'invoke-store')
);
export const $idbKeyValStore = atom<UseStore>(createIDBKeyValStore('invoke', 'invoke-store'));
export const clearIdbKeyValStore = action($idbKeyValStore, 'clear', (store) => {
clear(store.get());

View File

@@ -4,13 +4,12 @@ import { diff } from 'jsondiffpatch';
/**
* Super simple logger middleware. Useful for debugging when the redux devtools are awkward.
*/
export const debugLoggerMiddleware: Middleware =
(api: MiddlewareAPI) => (next) => (action) => {
const originalState = api.getState();
console.log('REDUX: dispatching', action);
const result = next(action);
const nextState = api.getState();
console.log('REDUX: next state', nextState);
console.log('REDUX: diff', diff(originalState, nextState));
return result;
};
export const debugLoggerMiddleware: Middleware = (api: MiddlewareAPI) => (next) => (action) => {
const originalState = api.getState();
console.log('REDUX: dispatching', action);
const result = next(action);
const nextState = api.getState();
console.log('REDUX: next state', nextState);
console.log('REDUX: diff', diff(originalState, nextState));
return result;
};

View File

@@ -35,8 +35,7 @@ export const actionSanitizer = <A extends UnknownAction>(action: A): A => {
if (socketGeneratorProgress.match(action)) {
const sanitized = cloneDeep(action);
if (sanitized.payload.data.progress_image) {
sanitized.payload.data.progress_image.dataURL =
'<Progress image omitted>';
sanitized.payload.data.progress_image.dataURL = '<Progress image omitted>';
}
return sanitized;
}

View File

@@ -1,9 +1,4 @@
import type {
ListenerEffect,
TypedAddListener,
TypedStartListening,
UnknownAction,
} from '@reduxjs/toolkit';
import type { ListenerEffect, TypedAddListener, TypedStartListening, UnknownAction } from '@reduxjs/toolkit';
import { addListener, createListenerMiddleware } from '@reduxjs/toolkit';
import { addGalleryImageClickedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked';
import type { AppDispatch, RootState } from 'app/store/store';
@@ -47,10 +42,7 @@ import {
import { addImagesStarredListener } from './listeners/imagesStarred';
import { addImagesUnstarredListener } from './listeners/imagesUnstarred';
import { addImageToDeleteSelectedListener } from './listeners/imageToDeleteSelected';
import {
addImageUploadedFulfilledListener,
addImageUploadedRejectedListener,
} from './listeners/imageUploaded';
import { addImageUploadedFulfilledListener, addImageUploadedRejectedListener } from './listeners/imageUploaded';
import { addInitialImageSelectedListener } from './listeners/initialImageSelected';
import { addModelSelectedListener } from './listeners/modelSelected';
import { addModelsLoadedListener } from './listeners/modelsLoaded';
@@ -78,19 +70,11 @@ export const listenerMiddleware = createListenerMiddleware();
export type AppStartListening = TypedStartListening<RootState, AppDispatch>;
export const startAppListening =
listenerMiddleware.startListening as AppStartListening;
export const startAppListening = listenerMiddleware.startListening as AppStartListening;
export const addAppListener = addListener as TypedAddListener<
RootState,
AppDispatch
>;
export const addAppListener = addListener as TypedAddListener<RootState, AppDispatch>;
export type AppListenerEffect = ListenerEffect<
UnknownAction,
RootState,
AppDispatch
>;
export type AppListenerEffect = ListenerEffect<UnknownAction, RootState, AppDispatch>;
/**
* The RTK listener middleware is a lightweight alternative sagas/observables.

View File

@@ -1,10 +1,6 @@
import { isAnyOf } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import {
canvasBatchIdsReset,
commitStagingAreaImage,
discardStagedImages,
} from 'features/canvas/store/canvasSlice';
import { canvasBatchIdsReset, commitStagingAreaImage, discardStagedImages } from 'features/canvas/store/canvasSlice';
import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next';
import { queueApi } from 'services/api/endpoints/queue';
@@ -23,10 +19,7 @@ export const addCommitStagingAreaImageListener = () => {
try {
const req = dispatch(
queueApi.endpoints.cancelByBatchIds.initiate(
{ batch_ids: batchIds },
{ fixedCacheKey: 'cancelByBatchIds' }
)
queueApi.endpoints.cancelByBatchIds.initiate({ batch_ids: batchIds }, { fixedCacheKey: 'cancelByBatchIds' })
);
const { canceled } = await req.unwrap();
req.reset();

View File

@@ -12,15 +12,9 @@ export const appStarted = createAction('app/appStarted');
export const addFirstListImagesListener = () => {
startAppListening({
matcher: imagesApi.endpoints.listImages.matchFulfilled,
effect: async (
action,
{ dispatch, unsubscribe, cancelActiveListeners }
) => {
effect: async (action, { dispatch, unsubscribe, cancelActiveListeners }) => {
// Only run this listener on the first listImages request for no-board images
if (
action.meta.arg.queryCacheKey !==
getListImagesUrl({ board_id: 'none', categories: IMAGE_CATEGORIES })
) {
if (action.meta.arg.queryCacheKey !== getListImagesUrl({ board_id: 'none', categories: IMAGE_CATEGORIES })) {
return;
}

View File

@@ -1,8 +1,5 @@
import { setInfillMethod } from 'features/parameters/store/generationSlice';
import {
shouldUseNSFWCheckerChanged,
shouldUseWatermarkerChanged,
} from 'features/system/store/systemSlice';
import { shouldUseNSFWCheckerChanged, shouldUseWatermarkerChanged } from 'features/system/store/systemSlice';
import { appInfoApi } from 'services/api/endpoints/appInfo';
import { startAppListening } from '..';
@@ -11,11 +8,7 @@ export const addAppConfigReceivedListener = () => {
startAppListening({
matcher: appInfoApi.endpoints.getAppConfig.matchFulfilled,
effect: async (action, { getState, dispatch }) => {
const {
infill_methods = [],
nsfw_methods = [],
watermarking_methods = [],
} = action.payload;
const { infill_methods = [], nsfw_methods = [], watermarking_methods = [] } = action.payload;
const infillMethod = getState().generation.infillMethod;
if (!infill_methods.includes(infillMethod)) {

View File

@@ -1,4 +1,4 @@
import { createStandaloneToast, theme, TOAST_OPTIONS } from '@invoke-ai/ui';
import { createStandaloneToast, theme, TOAST_OPTIONS } from '@invoke-ai/ui-library';
import { logger } from 'app/logging/logger';
import { parseify } from 'common/util/serialize';
import { zPydanticValidationError } from 'features/system/store/zodSchemas';
@@ -20,10 +20,7 @@ export const addBatchEnqueuedListener = () => {
effect: async (action) => {
const response = action.payload;
const arg = action.meta.arg.originalArgs;
logger('queue').debug(
{ enqueueResult: parseify(response) },
'Batch enqueued'
);
logger('queue').debug({ enqueueResult: parseify(response) }, 'Batch enqueued');
if (!toast.isActive('batch-queued')) {
toast({
@@ -53,10 +50,7 @@ export const addBatchEnqueuedListener = () => {
status: 'error',
description: 'Unknown Error',
});
logger('queue').error(
{ batchConfig: parseify(arg), error: parseify(response) },
t('queue.batchFailedToQueue')
);
logger('queue').error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue'));
return;
}
@@ -81,10 +75,7 @@ export const addBatchEnqueuedListener = () => {
status: 'error',
});
}
logger('queue').error(
{ batchConfig: parseify(arg), error: parseify(response) },
t('queue.batchFailedToQueue')
);
logger('queue').error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue'));
},
});
};

View File

@@ -22,13 +22,7 @@ export const addDeleteBoardAndImagesFulfilledListener = () => {
const { generation, canvas, nodes, controlAdapters } = getState();
deleted_images.forEach((image_name) => {
const imageUsage = getImageUsage(
generation,
canvas,
nodes,
controlAdapters,
image_name
);
const imageUsage = getImageUsage(generation, canvas, nodes, controlAdapters, image_name);
if (imageUsage.isInitialImage && !wasInitialImageReset) {
dispatch(clearInitialImage());

View File

@@ -1,13 +1,6 @@
import { isAnyOf } from '@reduxjs/toolkit';
import {
boardIdSelected,
galleryViewChanged,
imageSelected,
} from 'features/gallery/store/gallerySlice';
import {
ASSETS_CATEGORIES,
IMAGE_CATEGORIES,
} from 'features/gallery/store/types';
import { boardIdSelected, galleryViewChanged, imageSelected } from 'features/gallery/store/gallerySlice';
import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
import { imagesApi } from 'services/api/endpoints/images';
import { imagesSelectors } from 'services/api/util';
@@ -16,53 +9,35 @@ import { startAppListening } from '..';
export const addBoardIdSelectedListener = () => {
startAppListening({
matcher: isAnyOf(boardIdSelected, galleryViewChanged),
effect: async (
action,
{ getState, dispatch, condition, cancelActiveListeners }
) => {
effect: async (action, { getState, dispatch, condition, cancelActiveListeners }) => {
// Cancel any in-progress instances of this listener, we don't want to select an image from a previous board
cancelActiveListeners();
const state = getState();
const board_id = boardIdSelected.match(action)
? action.payload.boardId
: state.gallery.selectedBoardId;
const board_id = boardIdSelected.match(action) ? action.payload.boardId : state.gallery.selectedBoardId;
const galleryView = galleryViewChanged.match(action)
? action.payload
: state.gallery.galleryView;
const galleryView = galleryViewChanged.match(action) ? action.payload : state.gallery.galleryView;
// when a board is selected, we need to wait until the board has loaded *some* images, then select the first one
const categories =
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES;
const categories = galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES;
const queryArgs = { board_id: board_id ?? 'none', categories };
// wait until the board has some images - maybe it already has some from a previous fetch
// must use getState() to ensure we do not have stale state
const isSuccess = await condition(
() =>
imagesApi.endpoints.listImages.select(queryArgs)(getState())
.isSuccess,
() => imagesApi.endpoints.listImages.select(queryArgs)(getState()).isSuccess,
5000
);
if (isSuccess) {
// the board was just changed - we can select the first image
const { data: boardImagesData } =
imagesApi.endpoints.listImages.select(queryArgs)(getState());
const { data: boardImagesData } = imagesApi.endpoints.listImages.select(queryArgs)(getState());
if (
boardImagesData &&
boardIdSelected.match(action) &&
action.payload.selectedImageName
) {
if (boardImagesData && boardIdSelected.match(action) && action.payload.selectedImageName) {
const firstImage = imagesSelectors.selectAll(boardImagesData)[0];
const selectedImage = imagesSelectors.selectById(
boardImagesData,
action.payload.selectedImageName
);
const selectedImage = imagesSelectors.selectById(boardImagesData, action.payload.selectedImageName);
dispatch(imageSelected(selectedImage || firstImage || null));
} else {

View File

@@ -11,9 +11,7 @@ export const addCanvasCopiedToClipboardListener = () => {
startAppListening({
actionCreator: canvasCopiedToClipboard,
effect: async (action, { dispatch, getState }) => {
const moduleLog = $logger
.get()
.child({ namespace: 'canvasCopiedToClipboardListener' });
const moduleLog = $logger.get().child({ namespace: 'canvasCopiedToClipboardListener' });
const state = getState();
try {

View File

@@ -11,9 +11,7 @@ export const addCanvasDownloadedAsImageListener = () => {
startAppListening({
actionCreator: canvasDownloadedAsImage,
effect: async (action, { dispatch, getState }) => {
const moduleLog = $logger
.get()
.child({ namespace: 'canvasSavedToGalleryListener' });
const moduleLog = $logger.get().child({ namespace: 'canvasSavedToGalleryListener' });
const state = getState();
let blob;
@@ -32,9 +30,7 @@ export const addCanvasDownloadedAsImageListener = () => {
}
downloadBlob(blob, 'canvas.png');
dispatch(
addToast({ title: t('toast.canvasDownloaded'), status: 'success' })
);
dispatch(addToast({ title: t('toast.canvasDownloaded'), status: 'success' }));
},
});
};

View File

@@ -13,9 +13,7 @@ export const addCanvasMergedListener = () => {
startAppListening({
actionCreator: canvasMerged,
effect: async (action, { dispatch }) => {
const moduleLog = $logger
.get()
.child({ namespace: 'canvasCopiedToClipboardListener' });
const moduleLog = $logger.get().child({ namespace: 'canvasCopiedToClipboardListener' });
const blob = await getFullBaseLayerBlob();
if (!blob) {

View File

@@ -21,11 +21,7 @@ type AnyControlAdapterParamChangeAction =
| ReturnType<typeof controlAdapterProcessortTypeChanged>
| ReturnType<typeof controlAdapterAutoConfigToggled>;
const predicate: AnyListenerPredicate<RootState> = (
action,
state,
prevState
) => {
const predicate: AnyListenerPredicate<RootState> = (action, state, prevState) => {
const isActionMatched =
controlAdapterProcessorParamsChanged.match(action) ||
controlAdapterModelChanged.match(action) ||
@@ -40,12 +36,7 @@ const predicate: AnyListenerPredicate<RootState> = (
const { id } = action.payload;
const prevCA = selectControlAdapterById(prevState.controlAdapters, id);
const ca = selectControlAdapterById(state.controlAdapters, id);
if (
!prevCA ||
!isControlNetOrT2IAdapter(prevCA) ||
!ca ||
!isControlNetOrT2IAdapter(ca)
) {
if (!prevCA || !isControlNetOrT2IAdapter(prevCA) || !ca || !isControlNetOrT2IAdapter(ca)) {
return false;
}

View File

@@ -64,37 +64,28 @@ export const addControlNetImageProcessedListener = () => {
);
const enqueueResult = await req.unwrap();
req.reset();
log.debug(
{ enqueueResult: parseify(enqueueResult) },
t('queue.graphQueued')
);
log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued'));
const [invocationCompleteAction] = await take(
(action): action is ReturnType<typeof socketInvocationComplete> =>
socketInvocationComplete.match(action) &&
action.payload.data.queue_batch_id ===
enqueueResult.batch.batch_id &&
action.payload.data.queue_batch_id === enqueueResult.batch.batch_id &&
action.payload.data.source_node_id === nodeId
);
// We still have to check the output type
if (isImageOutput(invocationCompleteAction.payload.data.result)) {
const { image_name } =
invocationCompleteAction.payload.data.result.image;
const { image_name } = invocationCompleteAction.payload.data.result.image;
// Wait for the ImageDTO to be received
const [{ payload }] = await take(
(action) =>
imagesApi.endpoints.getImageDTO.matchFulfilled(action) &&
action.payload.image_name === image_name
imagesApi.endpoints.getImageDTO.matchFulfilled(action) && action.payload.image_name === image_name
);
const processedControlImage = payload as ImageDTO;
log.debug(
{ controlNetId: action.payload, processedControlImage },
'ControlNet image processed'
);
log.debug({ controlNetId: action.payload, processedControlImage }, 'ControlNet image processed');
// Update the processed image in the store
dispatch(
@@ -105,10 +96,7 @@ export const addControlNetImageProcessedListener = () => {
);
}
} catch (error) {
log.error(
{ enqueueBatchArg: parseify(enqueueBatchArg) },
t('queue.graphFailedToQueue')
);
log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue'));
if (error instanceof Object) {
if ('data' in error && 'status' in error) {

View File

@@ -2,10 +2,7 @@ import { logger } from 'app/logging/logger';
import { enqueueRequested } from 'app/store/actions';
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
import { parseify } from 'common/util/serialize';
import {
canvasBatchIdAdded,
stagingAreaInitialized,
} from 'features/canvas/store/canvasSlice';
import { canvasBatchIdAdded, stagingAreaInitialized } from 'features/canvas/store/canvasSlice';
import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
import { getCanvasData } from 'features/canvas/util/getCanvasData';
import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode';
@@ -34,20 +31,14 @@ import { startAppListening } from '..';
export const addEnqueueRequestedCanvasListener = () => {
startAppListening({
predicate: (action): action is ReturnType<typeof enqueueRequested> =>
enqueueRequested.match(action) &&
action.payload.tabName === 'unifiedCanvas',
enqueueRequested.match(action) && action.payload.tabName === 'unifiedCanvas',
effect: async (action, { getState, dispatch }) => {
const log = logger('queue');
const { prepend } = action.payload;
const state = getState();
const {
layerState,
boundingBoxCoordinates,
boundingBoxDimensions,
isMaskEnabled,
shouldPreserveMaskedArea,
} = state.canvas;
const { layerState, boundingBoxCoordinates, boundingBoxDimensions, isMaskEnabled, shouldPreserveMaskedArea } =
state.canvas;
// Build canvas blobs
const canvasBlobsAndImageData = await getCanvasData(
@@ -63,14 +54,10 @@ export const addEnqueueRequestedCanvasListener = () => {
return;
}
const { baseBlob, baseImageData, maskBlob, maskImageData } =
canvasBlobsAndImageData;
const { baseBlob, baseImageData, maskBlob, maskImageData } = canvasBlobsAndImageData;
// Determine the generation mode
const generationMode = getCanvasGenerationMode(
baseImageData,
maskImageData
);
const generationMode = getCanvasGenerationMode(baseImageData, maskImageData);
if (state.system.enableImageDebugging) {
const baseDataURL = await blobToDataURL(baseBlob);
@@ -115,12 +102,7 @@ export const addEnqueueRequestedCanvasListener = () => {
).unwrap();
}
const graph = buildCanvasGraph(
state,
generationMode,
canvasInitImage,
canvasMaskImage
);
const graph = buildCanvasGraph(state, generationMode, canvasInitImage, canvasMaskImage);
log.debug({ graph: parseify(graph) }, `Canvas graph built`);

View File

@@ -11,9 +11,7 @@ import { startAppListening } from '..';
export const addEnqueueRequestedLinear = () => {
startAppListening({
predicate: (action): action is ReturnType<typeof enqueueRequested> =>
enqueueRequested.match(action) &&
(action.payload.tabName === 'txt2img' ||
action.payload.tabName === 'img2img'),
enqueueRequested.match(action) && (action.payload.tabName === 'txt2img' || action.payload.tabName === 'img2img'),
effect: async (action, { getState, dispatch }) => {
const state = getState();
const model = state.generation.model;

View File

@@ -32,8 +32,7 @@ export const addGalleryImageClickedListener = () => {
const { imageDTO, shiftKey, ctrlKey, metaKey } = action.payload;
const state = getState();
const queryArgs = selectListImagesQueryArgs(state);
const { data: listImagesData } =
imagesApi.endpoints.listImages.select(queryArgs)(state);
const { data: listImagesData } = imagesApi.endpoints.listImages.select(queryArgs)(state);
if (!listImagesData) {
// Should never happen if we have clicked a gallery image
@@ -46,12 +45,8 @@ export const addGalleryImageClickedListener = () => {
if (shiftKey) {
const rangeEndImageName = imageDTO.image_name;
const lastSelectedImage = selection[selection.length - 1]?.image_name;
const lastClickedIndex = imageDTOs.findIndex(
(n) => n.image_name === lastSelectedImage
);
const currentClickedIndex = imageDTOs.findIndex(
(n) => n.image_name === rangeEndImageName
);
const lastClickedIndex = imageDTOs.findIndex((n) => n.image_name === lastSelectedImage);
const currentClickedIndex = imageDTOs.findIndex((n) => n.image_name === rangeEndImageName);
if (lastClickedIndex > -1 && currentClickedIndex > -1) {
// We have a valid range!
const start = Math.min(lastClickedIndex, currentClickedIndex);
@@ -60,15 +55,8 @@ export const addGalleryImageClickedListener = () => {
dispatch(selectionChanged(selection.concat(imagesToSelect)));
}
} else if (ctrlKey || metaKey) {
if (
selection.some((i) => i.image_name === imageDTO.image_name) &&
selection.length > 1
) {
dispatch(
selectionChanged(
selection.filter((n) => n.image_name !== imageDTO.image_name)
)
);
if (selection.some((i) => i.image_name === imageDTO.image_name) && selection.length > 1) {
dispatch(selectionChanged(selection.filter((n) => n.image_name !== imageDTO.image_name)));
} else {
dispatch(selectionChanged(selection.concat(imageDTO)));
}

View File

@@ -43,31 +43,21 @@ export const addRequestedSingleImageDeletionListener = () => {
dispatch(isModalOpenChanged(false));
const state = getState();
const lastSelectedImage =
state.gallery.selection[state.gallery.selection.length - 1]?.image_name;
const lastSelectedImage = state.gallery.selection[state.gallery.selection.length - 1]?.image_name;
if (imageDTO && imageDTO?.image_name === lastSelectedImage) {
const { image_name } = imageDTO;
const baseQueryArgs = selectListImagesQueryArgs(state);
const { data } =
imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
const { data } = imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
const cachedImageDTOs = data ? imagesSelectors.selectAll(data) : [];
const deletedImageIndex = cachedImageDTOs.findIndex(
(i) => i.image_name === image_name
);
const deletedImageIndex = cachedImageDTOs.findIndex((i) => i.image_name === image_name);
const filteredImageDTOs = cachedImageDTOs.filter(
(i) => i.image_name !== image_name
);
const filteredImageDTOs = cachedImageDTOs.filter((i) => i.image_name !== image_name);
const newSelectedImageIndex = clamp(
deletedImageIndex,
0,
filteredImageDTOs.length - 1
);
const newSelectedImageIndex = clamp(deletedImageIndex, 0, filteredImageDTOs.length - 1);
const newSelectedImageDTO = filteredImageDTOs[newSelectedImageIndex];
@@ -85,9 +75,7 @@ export const addRequestedSingleImageDeletionListener = () => {
imageDTOs.forEach((imageDTO) => {
// reset init image if we deleted it
if (
getState().generation.initialImage?.imageName === imageDTO.image_name
) {
if (getState().generation.initialImage?.imageName === imageDTO.image_name) {
dispatch(clearInitialImage());
}
@@ -95,8 +83,7 @@ export const addRequestedSingleImageDeletionListener = () => {
forEach(selectControlAdapterAll(getState().controlAdapters), (ca) => {
if (
ca.controlImage === imageDTO.image_name ||
(isControlNetOrT2IAdapter(ca) &&
ca.processedControlImage === imageDTO.image_name)
(isControlNetOrT2IAdapter(ca) && ca.processedControlImage === imageDTO.image_name)
) {
dispatch(
controlAdapterImageChanged({
@@ -120,10 +107,7 @@ export const addRequestedSingleImageDeletionListener = () => {
}
forEach(node.data.inputs, (input) => {
if (
isImageFieldInputInstance(input) &&
input.value?.image_name === imageDTO.image_name
) {
if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) {
dispatch(
fieldImageValueChanged({
nodeId: node.data.id,
@@ -137,24 +121,16 @@ export const addRequestedSingleImageDeletionListener = () => {
});
// Delete from server
const { requestId } = dispatch(
imagesApi.endpoints.deleteImage.initiate(imageDTO)
);
const { requestId } = dispatch(imagesApi.endpoints.deleteImage.initiate(imageDTO));
// Wait for successful deletion, then trigger boards to re-fetch
const wasImageDeleted = await condition(
(action) =>
imagesApi.endpoints.deleteImage.matchFulfilled(action) &&
action.meta.requestId === requestId,
(action) => imagesApi.endpoints.deleteImage.matchFulfilled(action) && action.meta.requestId === requestId,
30000
);
if (wasImageDeleted) {
dispatch(
api.util.invalidateTags([
{ type: 'Board', id: imageDTO.board_id ?? 'none' },
])
);
dispatch(api.util.invalidateTags([{ type: 'Board', id: imageDTO.board_id ?? 'none' }]));
}
},
});
@@ -176,17 +152,12 @@ export const addRequestedMultipleImageDeletionListener = () => {
try {
// Delete from server
await dispatch(
imagesApi.endpoints.deleteImages.initiate({ imageDTOs })
).unwrap();
await dispatch(imagesApi.endpoints.deleteImages.initiate({ imageDTOs })).unwrap();
const state = getState();
const queryArgs = selectListImagesQueryArgs(state);
const { data } =
imagesApi.endpoints.listImages.select(queryArgs)(state);
const { data } = imagesApi.endpoints.listImages.select(queryArgs)(state);
const newSelectedImageDTO = data
? imagesSelectors.selectAll(data)[0]
: undefined;
const newSelectedImageDTO = data ? imagesSelectors.selectAll(data)[0] : undefined;
if (newSelectedImageDTO) {
dispatch(imageSelected(newSelectedImageDTO));
@@ -204,10 +175,7 @@ export const addRequestedMultipleImageDeletionListener = () => {
imageDTOs.forEach((imageDTO) => {
// reset init image if we deleted it
if (
getState().generation.initialImage?.imageName ===
imageDTO.image_name
) {
if (getState().generation.initialImage?.imageName === imageDTO.image_name) {
dispatch(clearInitialImage());
}
@@ -215,8 +183,7 @@ export const addRequestedMultipleImageDeletionListener = () => {
forEach(selectControlAdapterAll(getState().controlAdapters), (ca) => {
if (
ca.controlImage === imageDTO.image_name ||
(isControlNetOrT2IAdapter(ca) &&
ca.processedControlImage === imageDTO.image_name)
(isControlNetOrT2IAdapter(ca) && ca.processedControlImage === imageDTO.image_name)
) {
dispatch(
controlAdapterImageChanged({
@@ -240,10 +207,7 @@ export const addRequestedMultipleImageDeletionListener = () => {
}
forEach(node.data.inputs, (input) => {
if (
isImageFieldInputInstance(input) &&
input.value?.image_name === imageDTO.image_name
) {
if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) {
dispatch(
fieldImageValueChanged({
nodeId: node.data.id,
@@ -295,10 +259,7 @@ export const addImageDeletedRejectedListener = () => {
matcher: imagesApi.endpoints.deleteImage.matchRejected,
effect: (action) => {
const log = logger('images');
log.debug(
{ imageDTO: action.meta.arg.originalArgs },
'Unable to delete image'
);
log.debug({ imageDTO: action.meta.arg.originalArgs }, 'Unable to delete image');
},
});
};

View File

@@ -6,16 +6,10 @@ import {
controlAdapterImageChanged,
controlAdapterIsEnabledChanged,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import type {
TypesafeDraggableData,
TypesafeDroppableData,
} from 'features/dnd/types';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import {
initialImageChanged,
selectOptimalDimension,
} from 'features/parameters/store/generationSlice';
import { initialImageChanged, selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { imagesApi } from 'services/api/endpoints/images';
import { startAppListening } from '../';
@@ -35,15 +29,9 @@ export const addImageDroppedListener = () => {
if (activeData.payloadType === 'IMAGE_DTO') {
log.debug({ activeData, overData }, 'Image dropped');
} else if (activeData.payloadType === 'GALLERY_SELECTION') {
log.debug(
{ activeData, overData },
`Images (${getState().gallery.selection.length}) dropped`
);
log.debug({ activeData, overData }, `Images (${getState().gallery.selection.length}) dropped`);
} else if (activeData.payloadType === 'NODE_FIELD') {
log.debug(
{ activeData: parseify(activeData), overData: parseify(overData) },
'Node field dropped'
);
log.debug({ activeData: parseify(activeData), overData: parseify(overData) }, 'Node field dropped');
} else {
log.debug({ activeData, overData }, `Unknown payload dropped`);
}
@@ -104,12 +92,7 @@ export const addImageDroppedListener = () => {
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
dispatch(
setInitialCanvasImage(
activeData.payload.imageDTO,
selectOptimalDimension(getState())
)
);
dispatch(setInitialCanvasImage(activeData.payload.imageDTO, selectOptimalDimension(getState())));
return;
}
@@ -191,10 +174,7 @@ export const addImageDroppedListener = () => {
/**
* Multiple images dropped on user board
*/
if (
overData.actionType === 'ADD_TO_BOARD' &&
activeData.payloadType === 'GALLERY_SELECTION'
) {
if (overData.actionType === 'ADD_TO_BOARD' && activeData.payloadType === 'GALLERY_SELECTION') {
const imageDTOs = getState().gallery.selection;
const { boardId } = overData.context;
dispatch(
@@ -209,10 +189,7 @@ export const addImageDroppedListener = () => {
/**
* Multiple images dropped on 'none' board
*/
if (
overData.actionType === 'REMOVE_FROM_BOARD' &&
activeData.payloadType === 'GALLERY_SELECTION'
) {
if (overData.actionType === 'REMOVE_FROM_BOARD' && activeData.payloadType === 'GALLERY_SELECTION') {
const imageDTOs = getState().gallery.selection;
dispatch(
imagesApi.endpoints.removeImagesFromBoard.initiate({

View File

@@ -1,9 +1,6 @@
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { selectImageUsage } from 'features/deleteImageModal/store/selectors';
import {
imagesToDeleteSelected,
isModalOpenChanged,
} from 'features/deleteImageModal/store/slice';
import { imagesToDeleteSelected, isModalOpenChanged } from 'features/deleteImageModal/store/slice';
import { startAppListening } from '..';

View File

@@ -1,4 +1,4 @@
import type { UseToastOptions } from '@invoke-ai/ui';
import type { UseToastOptions } from '@invoke-ai/ui-library';
import { logger } from 'app/logging/logger';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import {
@@ -6,10 +6,7 @@ import {
controlAdapterIsEnabledChanged,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import {
initialImageChanged,
selectOptimalDimension,
} from 'features/parameters/store/generationSlice';
import { initialImageChanged, selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next';
import { omit } from 'lodash-es';
@@ -79,9 +76,7 @@ export const addImageUploadedFulfilledListener = () => {
}
if (postUploadAction?.type === 'SET_CANVAS_INITIAL_IMAGE') {
dispatch(
setInitialCanvasImage(imageDTO, selectOptimalDimension(state))
);
dispatch(setInitialCanvasImage(imageDTO, selectOptimalDimension(state)));
dispatch(
addToast({
...DEFAULT_UPLOADED_TOAST,
@@ -127,9 +122,7 @@ export const addImageUploadedFulfilledListener = () => {
if (postUploadAction?.type === 'SET_NODES_IMAGE') {
const { nodeId, fieldName } = postUploadAction;
dispatch(
fieldImageValueChanged({ nodeId, fieldName, value: imageDTO })
);
dispatch(fieldImageValueChanged({ nodeId, fieldName, value: imageDTO }));
dispatch(
addToast({
...DEFAULT_UPLOADED_TOAST,

View File

@@ -11,11 +11,7 @@ export const addInitialImageSelectedListener = () => {
actionCreator: initialImageSelected,
effect: (action, { dispatch }) => {
if (!action.payload) {
dispatch(
addToast(
makeToast({ title: t('toast.imageNotLoadedDesc'), status: 'error' })
)
);
dispatch(addToast(makeToast({ title: t('toast.imageNotLoadedDesc'), status: 'error' })));
return;
}

View File

@@ -5,10 +5,7 @@ import {
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { loraRemoved } from 'features/lora/store/loraSlice';
import { modelSelected } from 'features/parameters/store/actions';
import {
modelChanged,
vaeSelected,
} from 'features/parameters/store/generationSlice';
import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice';
import { zParameterModel } from 'features/parameters/types/parameterSchemas';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
@@ -27,18 +24,14 @@ export const addModelSelectedListener = () => {
const result = zParameterModel.safeParse(action.payload);
if (!result.success) {
log.error(
{ error: result.error.format() },
'Failed to parse main model'
);
log.error({ error: result.error.format() }, 'Failed to parse main model');
return;
}
const newModel = result.data;
const { base_model } = newModel;
const didBaseModelChange =
state.generation.model?.base_model !== base_model;
const newBaseModel = newModel.base_model;
const didBaseModelChange = state.generation.model?.base_model !== newBaseModel;
if (didBaseModelChange) {
// we may need to reset some incompatible submodels
@@ -46,7 +39,7 @@ export const addModelSelectedListener = () => {
// handle incompatible loras
forEach(state.lora.loras, (lora, id) => {
if (lora.base_model !== base_model) {
if (lora.base_model !== newBaseModel) {
dispatch(loraRemoved(id));
modelsCleared += 1;
}
@@ -54,17 +47,15 @@ export const addModelSelectedListener = () => {
// handle incompatible vae
const { vae } = state.generation;
if (vae && vae.base_model !== base_model) {
if (vae && vae.base_model !== newBaseModel) {
dispatch(vaeSelected(null));
modelsCleared += 1;
}
// handle incompatible controlnets
selectControlAdapterAll(state.controlAdapters).forEach((ca) => {
if (ca.model?.base_model !== base_model) {
dispatch(
controlAdapterIsEnabledChanged({ id: ca.id, isEnabled: false })
);
if (ca.model?.base_model !== newBaseModel) {
dispatch(controlAdapterIsEnabledChanged({ id: ca.id, isEnabled: false }));
modelsCleared += 1;
}
});

View File

@@ -6,41 +6,24 @@ import {
selectAllT2IAdapters,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { loraRemoved } from 'features/lora/store/loraSlice';
import {
modelChanged,
vaeSelected,
} from 'features/parameters/store/generationSlice';
import {
zParameterModel,
zParameterVAEModel,
} from 'features/parameters/types/parameterSchemas';
import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice';
import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas';
import { refinerModelChanged } from 'features/sdxl/store/sdxlSlice';
import { forEach, some } from 'lodash-es';
import {
mainModelsAdapterSelectors,
modelsApi,
vaeModelsAdapterSelectors,
} from 'services/api/endpoints/models';
import { mainModelsAdapterSelectors, modelsApi, vaeModelsAdapterSelectors } from 'services/api/endpoints/models';
import type { TypeGuardFor } from 'services/api/types';
import { startAppListening } from '..';
export const addModelsLoadedListener = () => {
startAppListening({
predicate: (
action
): action is TypeGuardFor<
typeof modelsApi.endpoints.getMainModels.matchFulfilled
> =>
predicate: (action): action is TypeGuardFor<typeof modelsApi.endpoints.getMainModels.matchFulfilled> =>
modelsApi.endpoints.getMainModels.matchFulfilled(action) &&
!action.meta.arg.originalArgs.includes('sdxl-refiner'),
effect: async (action, { getState, dispatch }) => {
// models loaded, we need to ensure the selected model is available and if not, select the first one
const log = logger('models');
log.info(
{ models: action.payload.entities },
`Main models loaded (${action.payload.ids.length})`
);
log.info({ models: action.payload.entities }, `Main models loaded (${action.payload.ids.length})`);
const currentModel = getState().generation.model;
const models = mainModelsAdapterSelectors.selectAll(action.payload);
@@ -67,10 +50,7 @@ export const addModelsLoadedListener = () => {
const result = zParameterModel.safeParse(models[0]);
if (!result.success) {
log.error(
{ error: result.error.format() },
'Failed to parse main model'
);
log.error({ error: result.error.format() }, 'Failed to parse main model');
return;
}
@@ -78,20 +58,12 @@ export const addModelsLoadedListener = () => {
},
});
startAppListening({
predicate: (
action
): action is TypeGuardFor<
typeof modelsApi.endpoints.getMainModels.matchFulfilled
> =>
modelsApi.endpoints.getMainModels.matchFulfilled(action) &&
action.meta.arg.originalArgs.includes('sdxl-refiner'),
predicate: (action): action is TypeGuardFor<typeof modelsApi.endpoints.getMainModels.matchFulfilled> =>
modelsApi.endpoints.getMainModels.matchFulfilled(action) && action.meta.arg.originalArgs.includes('sdxl-refiner'),
effect: async (action, { getState, dispatch }) => {
// models loaded, we need to ensure the selected model is available and if not, select the first one
const log = logger('models');
log.info(
{ models: action.payload.entities },
`SDXL Refiner models loaded (${action.payload.ids.length})`
);
log.info({ models: action.payload.entities }, `SDXL Refiner models loaded (${action.payload.ids.length})`);
const currentModel = getState().sdxl.refinerModel;
const models = mainModelsAdapterSelectors.selectAll(action.payload);
@@ -122,10 +94,7 @@ export const addModelsLoadedListener = () => {
effect: async (action, { getState, dispatch }) => {
// VAEs loaded, need to reset the VAE is it's no longer available
const log = logger('models');
log.info(
{ models: action.payload.entities },
`VAEs loaded (${action.payload.ids.length})`
);
log.info({ models: action.payload.entities }, `VAEs loaded (${action.payload.ids.length})`);
const currentVae = getState().generation.vae;
@@ -136,9 +105,7 @@ export const addModelsLoadedListener = () => {
const isCurrentVAEAvailable = some(
action.payload.entities,
(m) =>
m?.model_name === currentVae?.model_name &&
m?.base_model === currentVae?.base_model
(m) => m?.model_name === currentVae?.model_name && m?.base_model === currentVae?.base_model
);
if (isCurrentVAEAvailable) {
@@ -156,10 +123,7 @@ export const addModelsLoadedListener = () => {
const result = zParameterVAEModel.safeParse(firstModel);
if (!result.success) {
log.error(
{ error: result.error.format() },
'Failed to parse VAE model'
);
log.error({ error: result.error.format() }, 'Failed to parse VAE model');
return;
}
@@ -171,19 +135,14 @@ export const addModelsLoadedListener = () => {
effect: async (action, { getState, dispatch }) => {
// LoRA models loaded - need to remove missing LoRAs from state
const log = logger('models');
log.info(
{ models: action.payload.entities },
`LoRAs loaded (${action.payload.ids.length})`
);
log.info({ models: action.payload.entities }, `LoRAs loaded (${action.payload.ids.length})`);
const loras = getState().lora.loras;
forEach(loras, (lora, id) => {
const isLoRAAvailable = some(
action.payload.entities,
(m) =>
m?.model_name === lora?.model_name &&
m?.base_model === lora?.base_model
(m) => m?.model_name === lora?.model_name && m?.base_model === lora?.base_model
);
if (isLoRAAvailable) {
@@ -199,17 +158,12 @@ export const addModelsLoadedListener = () => {
effect: async (action, { getState, dispatch }) => {
// ControlNet models loaded - need to remove missing ControlNets from state
const log = logger('models');
log.info(
{ models: action.payload.entities },
`ControlNet models loaded (${action.payload.ids.length})`
);
log.info({ models: action.payload.entities }, `ControlNet models loaded (${action.payload.ids.length})`);
selectAllControlNets(getState().controlAdapters).forEach((ca) => {
const isModelAvailable = some(
action.payload.entities,
(m) =>
m?.model_name === ca?.model?.model_name &&
m?.base_model === ca?.model?.base_model
(m) => m?.model_name === ca?.model?.model_name && m?.base_model === ca?.model?.base_model
);
if (isModelAvailable) {
@@ -225,17 +179,12 @@ export const addModelsLoadedListener = () => {
effect: async (action, { getState, dispatch }) => {
// ControlNet models loaded - need to remove missing ControlNets from state
const log = logger('models');
log.info(
{ models: action.payload.entities },
`T2I Adapter models loaded (${action.payload.ids.length})`
);
log.info({ models: action.payload.entities }, `T2I Adapter models loaded (${action.payload.ids.length})`);
selectAllT2IAdapters(getState().controlAdapters).forEach((ca) => {
const isModelAvailable = some(
action.payload.entities,
(m) =>
m?.model_name === ca?.model?.model_name &&
m?.base_model === ca?.model?.base_model
(m) => m?.model_name === ca?.model?.model_name && m?.base_model === ca?.model?.base_model
);
if (isModelAvailable) {
@@ -251,17 +200,12 @@ export const addModelsLoadedListener = () => {
effect: async (action, { getState, dispatch }) => {
// ControlNet models loaded - need to remove missing ControlNets from state
const log = logger('models');
log.info(
{ models: action.payload.entities },
`IP Adapter models loaded (${action.payload.ids.length})`
);
log.info({ models: action.payload.entities }, `IP Adapter models loaded (${action.payload.ids.length})`);
selectAllIPAdapters(getState().controlAdapters).forEach((ca) => {
const isModelAvailable = some(
action.payload.entities,
(m) =>
m?.model_name === ca?.model?.model_name &&
m?.base_model === ca?.model?.base_model
(m) => m?.model_name === ca?.model?.model_name && m?.base_model === ca?.model?.base_model
);
if (isModelAvailable) {
@@ -276,10 +220,7 @@ export const addModelsLoadedListener = () => {
matcher: modelsApi.endpoints.getTextualInversionModels.matchFulfilled,
effect: async (action) => {
const log = logger('models');
log.info(
{ models: action.payload.entities },
`Embeddings loaded (${action.payload.ids.length})`
);
log.info({ models: action.payload.entities }, `Embeddings loaded (${action.payload.ids.length})`);
},
});
};

View File

@@ -15,21 +15,12 @@ import { socketConnected } from 'services/events/actions';
import { startAppListening } from '..';
const matcher = isAnyOf(
setPositivePrompt,
combinatorialToggled,
maxPromptsChanged,
maxPromptsReset,
socketConnected
);
const matcher = isAnyOf(setPositivePrompt, combinatorialToggled, maxPromptsChanged, maxPromptsReset, socketConnected);
export const addDynamicPromptsListener = () => {
startAppListening({
matcher,
effect: async (
action,
{ dispatch, getState, cancelActiveListeners, delay }
) => {
effect: async (action, { dispatch, getState, cancelActiveListeners, delay }) => {
cancelActiveListeners();
const state = getState();
const { positivePrompt } = state.generation;
@@ -46,14 +37,14 @@ export const addDynamicPromptsListener = () => {
if (cachedPrompts) {
dispatch(promptsChanged(cachedPrompts.prompts));
dispatch(parsingErrorChanged(cachedPrompts.error));
return;
}
if (!getShouldProcessPrompt(state.generation.positivePrompt)) {
if (state.dynamicPrompts.isLoading) {
dispatch(isLoadingChanged(false));
}
dispatch(promptsChanged([state.generation.positivePrompt]));
dispatch(parsingErrorChanged(undefined));
dispatch(isErrorChanged(false));
return;
}
@@ -78,7 +69,6 @@ export const addDynamicPromptsListener = () => {
dispatch(promptsChanged(res.prompts));
dispatch(parsingErrorChanged(res.error));
dispatch(isErrorChanged(false));
dispatch(isLoadingChanged(false));
} catch {
dispatch(isErrorChanged(true));
dispatch(isLoadingChanged(false));

View File

@@ -17,16 +17,9 @@ export const addReceivedOpenAPISchemaListener = () => {
log.debug({ schemaJSON }, 'Received OpenAPI schema');
const { nodesAllowlist, nodesDenylist } = getState().config;
const nodeTemplates = parseSchema(
schemaJSON,
nodesAllowlist,
nodesDenylist
);
const nodeTemplates = parseSchema(schemaJSON, nodesAllowlist, nodesDenylist);
log.debug(
{ nodeTemplates: parseify(nodeTemplates) },
`Built ${size(nodeTemplates)} node templates`
);
log.debug({ nodeTemplates: parseify(nodeTemplates) }, `Built ${size(nodeTemplates)} node templates`);
dispatch(nodeTemplatesBuilt(nodeTemplates));
},
@@ -36,10 +29,7 @@ export const addReceivedOpenAPISchemaListener = () => {
actionCreator: receivedOpenAPISchema.rejected,
effect: (action) => {
const log = logger('system');
log.error(
{ error: parseify(action.error) },
'Problem retrieving OpenAPI Schema'
);
log.error({ error: parseify(action.error) }, 'Problem retrieving OpenAPI Schema');
},
});
};

View File

@@ -16,10 +16,7 @@ const $isFirstConnection = atom(true);
export const addSocketConnectedEventListener = () => {
startAppListening({
actionCreator: socketConnected,
effect: async (
action,
{ dispatch, getState, cancelActiveListeners, delay }
) => {
effect: async (action, { dispatch, getState, cancelActiveListeners, delay }) => {
log.debug('Connected');
/**
@@ -86,10 +83,7 @@ export const addSocketConnectedEventListener = () => {
effect: async (action, { dispatch, getState }) => {
const { nodeTemplates, config } = getState();
// We only want to re-fetch the schema if we don't have any node templates
if (
!size(nodeTemplates.templates) &&
!config.disabledTabs.includes('nodes')
) {
if (!size(nodeTemplates.templates) && !config.disabledTabs.includes('nodes')) {
// This request is a createAsyncThunk - resetting API state as in the above listener
// will not trigger this request, so we need to manually do it.
dispatch(receivedOpenAPISchema());

View File

@@ -1,17 +1,10 @@
import { logger } from 'app/logging/logger';
import { parseify } from 'common/util/serialize';
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
import {
boardIdSelected,
galleryViewChanged,
imageSelected,
} from 'features/gallery/store/gallerySlice';
import { boardIdSelected, galleryViewChanged, imageSelected } from 'features/gallery/store/gallerySlice';
import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
import { isImageOutput } from 'features/nodes/types/common';
import {
LINEAR_UI_OUTPUT,
nodeIDDenyList,
} from 'features/nodes/util/graph/constants';
import { LINEAR_UI_OUTPUT, nodeIDDenyList } from 'features/nodes/util/graph/constants';
import { boardsApi } from 'services/api/endpoints/boards';
import { imagesApi } from 'services/api/endpoints/images';
import { imagesAdapter } from 'services/api/util';
@@ -29,19 +22,12 @@ export const addInvocationCompleteEventListener = () => {
actionCreator: socketInvocationComplete,
effect: async (action, { dispatch, getState }) => {
const { data } = action.payload;
log.debug(
{ data: parseify(data) },
`Invocation complete (${action.payload.data.node.type})`
);
log.debug({ data: parseify(data) }, `Invocation complete (${action.payload.data.node.type})`);
const { result, node, queue_batch_id, source_node_id } = data;
// This complete event has an associated image output
if (
isImageOutput(result) &&
!nodeTypeDenylist.includes(node.type) &&
!nodeIDDenyList.includes(source_node_id)
) {
if (isImageOutput(result) && !nodeTypeDenylist.includes(node.type) && !nodeIDDenyList.includes(source_node_id)) {
const { image_name } = result.image;
const { canvas, gallery } = getState();
@@ -56,10 +42,7 @@ export const addInvocationCompleteEventListener = () => {
imageDTORequest.unsubscribe();
// Add canvas images to the staging area
if (
canvas.batchIds.includes(queue_batch_id) &&
[LINEAR_UI_OUTPUT].includes(data.source_node_id)
) {
if (canvas.batchIds.includes(queue_batch_id) && [LINEAR_UI_OUTPUT].includes(data.source_node_id)) {
dispatch(addImageToStagingArea(imageDTO));
}
@@ -84,21 +67,13 @@ export const addInvocationCompleteEventListener = () => {
// update the total images for the board
dispatch(
boardsApi.util.updateQueryData(
'getBoardImagesTotal',
imageDTO.board_id ?? 'none',
(draft) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
draft.total += 1;
}
)
boardsApi.util.updateQueryData('getBoardImagesTotal', imageDTO.board_id ?? 'none', (draft) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
draft.total += 1;
})
);
dispatch(
imagesApi.util.invalidateTags([
{ type: 'Board', id: imageDTO.board_id ?? 'none' },
])
);
dispatch(imagesApi.util.invalidateTags([{ type: 'Board', id: imageDTO.board_id ?? 'none' }]));
const { shouldAutoSwitch } = gallery;
@@ -109,10 +84,7 @@ export const addInvocationCompleteEventListener = () => {
dispatch(galleryViewChanged('images'));
}
if (
imageDTO.board_id &&
imageDTO.board_id !== gallery.selectedBoardId
) {
if (imageDTO.board_id && imageDTO.board_id !== gallery.selectedBoardId) {
dispatch(
boardIdSelected({
boardId: imageDTO.board_id,

View File

@@ -9,10 +9,7 @@ export const addInvocationErrorEventListener = () => {
startAppListening({
actionCreator: socketInvocationError,
effect: (action) => {
log.error(
action.payload,
`Invocation error (${action.payload.data.node.type})`
);
log.error(action.payload, `Invocation error (${action.payload.data.node.type})`);
},
});
};

View File

@@ -9,10 +9,7 @@ export const addInvocationRetrievalErrorEventListener = () => {
startAppListening({
actionCreator: socketInvocationRetrievalError,
effect: (action) => {
log.error(
action.payload,
`Invocation retrieval error (${action.payload.data.graph_execution_state_id})`
);
log.error(action.payload, `Invocation retrieval error (${action.payload.data.graph_execution_state_id})`);
},
});
};

View File

@@ -9,10 +9,7 @@ export const addInvocationStartedEventListener = () => {
startAppListening({
actionCreator: socketInvocationStarted,
effect: (action) => {
log.debug(
action.payload,
`Invocation started (${action.payload.data.node.type})`
);
log.debug(action.payload, `Invocation started (${action.payload.data.node.type})`);
},
});
};

View File

@@ -1,8 +1,5 @@
import { logger } from 'app/logging/logger';
import {
socketModelLoadCompleted,
socketModelLoadStarted,
} from 'services/events/actions';
import { socketModelLoadCompleted, socketModelLoadStarted } from 'services/events/actions';
import { startAppListening } from '../..';
@@ -12,8 +9,7 @@ export const addModelLoadEventListener = () => {
startAppListening({
actionCreator: socketModelLoadStarted,
effect: (action) => {
const { base_model, model_name, model_type, submodel } =
action.payload.data;
const { base_model, model_name, model_type, submodel } = action.payload.data;
let message = `Model load started: ${base_model}/${model_type}/${model_name}`;
@@ -28,8 +24,7 @@ export const addModelLoadEventListener = () => {
startAppListening({
actionCreator: socketModelLoadCompleted,
effect: (action) => {
const { base_model, model_name, model_type, submodel } =
action.payload.data;
const { base_model, model_name, model_type, submodel } = action.payload.data;
let message = `Model load complete: ${base_model}/${model_type}/${model_name}`;

View File

@@ -13,10 +13,7 @@ export const addSocketQueueItemStatusChangedEventListener = () => {
// we've got new status for the queue item, batch and queue
const { queue_item, batch_status, queue_status } = action.payload.data;
log.debug(
action.payload,
`Queue item ${queue_item.item_id} status updated: ${queue_item.status}`
);
log.debug(action.payload, `Queue item ${queue_item.item_id} status updated: ${queue_item.status}`);
// Update this specific queue item in the list of queue items (this is the queue item DTO, without the session)
dispatch(
@@ -40,35 +37,23 @@ export const addSocketQueueItemStatusChangedEventListener = () => {
// Update the batch status
dispatch(
queueApi.util.updateQueryData(
'getBatchStatus',
{ batch_id: batch_status.batch_id },
() => batch_status
)
queueApi.util.updateQueryData('getBatchStatus', { batch_id: batch_status.batch_id }, () => batch_status)
);
// Update the queue item status (this is the full queue item, including the session)
dispatch(
queueApi.util.updateQueryData(
'getQueueItem',
queue_item.item_id,
(draft) => {
if (!draft) {
return;
}
Object.assign(draft, queue_item);
queueApi.util.updateQueryData('getQueueItem', queue_item.item_id, (draft) => {
if (!draft) {
return;
}
)
Object.assign(draft, queue_item);
})
);
// Invalidate caches for things we cannot update
// TODO: technically, we could possibly update the current session queue item, but feels safer to just request it again
dispatch(
queueApi.util.invalidateTags([
'CurrentSessionQueueItem',
'NextSessionQueueItem',
'InvocationCacheStatus',
])
queueApi.util.invalidateTags(['CurrentSessionQueueItem', 'NextSessionQueueItem', 'InvocationCacheStatus'])
);
},
});

View File

@@ -9,10 +9,7 @@ export const addSessionRetrievalErrorEventListener = () => {
startAppListening({
actionCreator: socketSessionRetrievalError,
effect: (action) => {
log.error(
action.payload,
`Session retrieval error (${action.payload.data.graph_execution_state_id})`
);
log.error(action.payload, `Session retrieval error (${action.payload.data.graph_execution_state_id})`);
},
});
};

View File

@@ -3,10 +3,7 @@ import { updateAllNodesRequested } from 'features/nodes/store/actions';
import { nodeReplaced } from 'features/nodes/store/nodesSlice';
import { NodeUpdateError } from 'features/nodes/types/error';
import { isInvocationNode } from 'features/nodes/types/invocation';
import {
getNeedsUpdate,
updateNode,
} from 'features/nodes/util/node/nodeUpdate';
import { getNeedsUpdate, updateNode } from 'features/nodes/util/node/nodeUpdate';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { t } from 'i18next';

View File

@@ -10,9 +10,7 @@ import type { BatchConfig, ImageDTO } from 'services/api/types';
import { startAppListening } from '..';
export const upscaleRequested = createAction<{ imageDTO: ImageDTO }>(
`upscale/upscaleRequested`
);
export const upscaleRequested = createAction<{ imageDTO: ImageDTO }>(`upscale/upscaleRequested`);
export const addUpscaleRequestedListener = () => {
startAppListening({
@@ -24,8 +22,7 @@ export const addUpscaleRequestedListener = () => {
const { image_name } = imageDTO;
const state = getState();
const { isAllowedToUpscale, detailTKey } =
createIsAllowedToUpscaleSelector(imageDTO)(state);
const { isAllowedToUpscale, detailTKey } = createIsAllowedToUpscaleSelector(imageDTO)(state);
// if we can't upscale, show a toast and return
if (!isAllowedToUpscale) {
@@ -66,21 +63,11 @@ export const addUpscaleRequestedListener = () => {
const enqueueResult = await req.unwrap();
req.reset();
log.debug(
{ enqueueResult: parseify(enqueueResult) },
t('queue.graphQueued')
);
log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued'));
} catch (error) {
log.error(
{ enqueueBatchArg: parseify(enqueueBatchArg) },
t('queue.graphFailedToQueue')
);
log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue'));
if (
error instanceof Object &&
'status' in error &&
error.status === 403
) {
if (error instanceof Object && 'status' in error && error.status === 403) {
return;
} else {
dispatch(

View File

@@ -1,14 +1,8 @@
import { logger } from 'app/logging/logger';
import { parseify } from 'common/util/serialize';
import {
workflowLoaded,
workflowLoadRequested,
} from 'features/nodes/store/actions';
import { workflowLoaded, workflowLoadRequested } from 'features/nodes/store/actions';
import { $flow } from 'features/nodes/store/reactFlowInstance';
import {
WorkflowMigrationError,
WorkflowVersionError,
} from 'features/nodes/types/error';
import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error';
import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
@@ -28,10 +22,7 @@ export const addWorkflowLoadRequestedListener = () => {
const nodeTemplates = getState().nodeTemplates.templates;
try {
const { workflow: validatedWorkflow, warnings } = validateWorkflow(
workflow,
nodeTemplates
);
const { workflow: validatedWorkflow, warnings } = validateWorkflow(workflow, nodeTemplates);
if (asCopy) {
// If we're loading a copy, we need to remove the ID so that the backend will create a new workflow
@@ -108,10 +99,7 @@ export const addWorkflowLoadRequestedListener = () => {
);
} else {
// Some other error occurred
log.error(
{ error: parseify(e) },
t('nodes.unknownErrorValidatingWorkflow')
);
log.error({ error: parseify(e) }, t('nodes.unknownErrorValidatingWorkflow'));
dispatch(
addToast(
makeToast({

View File

@@ -1,3 +0,0 @@
# nanostores
For non-serializable data that needs to be available throughout the app, or when redux is not appropriate, use nanostores.

View File

@@ -1,4 +1,3 @@
import { atom } from 'nanostores';
import type { ReactNode } from 'react';
export const $customNavComponent = atom<ReactNode | undefined>(undefined);
export const $customNavComponent = atom<undefined | (() => JSX.Element)>(undefined);

View File

@@ -1,4 +1,4 @@
import type { MenuItemProps } from '@invoke-ai/ui';
import type { MenuItemProps } from '@invoke-ai/ui-library';
import { atom } from 'nanostores';
export type CustomStarUi = {

View File

@@ -1,4 +1,3 @@
import { atom } from 'nanostores';
import type { ReactNode } from 'react';
export const $galleryHeader = atom<ReactNode | undefined>(undefined);
export const $galleryHeader = atom<undefined | (() => JSX.Element)>(undefined);

View File

@@ -1,4 +1,3 @@
import { atom } from 'nanostores';
import type { ReactNode } from 'react';
export const $logo = atom<ReactNode | undefined>(undefined);
export const $logo = atom<undefined | (() => JSX.Element)>(undefined);

View File

@@ -0,0 +1,3 @@
import { atom } from 'nanostores';
export const $openAPISchemaUrl = atom<string | undefined>(undefined);

View File

@@ -8,6 +8,4 @@ declare global {
}
}
export const $store = atom<
Readonly<ReturnType<typeof createStore>> | undefined
>();
export const $store = atom<Readonly<ReturnType<typeof createStore>> | undefined>();

View File

@@ -0,0 +1,4 @@
import type { WorkflowCategory } from 'features/nodes/types/workflow';
import { atom } from 'nanostores';
export const $workflowCategories = atom<WorkflowCategory[]>(['user', 'default']);

View File

@@ -1,17 +1,10 @@
import type { ThunkDispatch, UnknownAction } from '@reduxjs/toolkit';
import {
autoBatchEnhancer,
combineReducers,
configureStore,
} from '@reduxjs/toolkit';
import { autoBatchEnhancer, combineReducers, configureStore } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver';
import { errorHandler } from 'app/store/enhancers/reduxRemember/errors';
import { canvasPersistDenylist } from 'features/canvas/store/canvasPersistDenylist';
import canvasReducer, {
initialCanvasState,
migrateCanvasState,
} from 'features/canvas/store/canvasSlice';
import canvasReducer, { initialCanvasState, migrateCanvasState } from 'features/canvas/store/canvasSlice';
import changeBoardModalReducer from 'features/changeBoardModal/store/slice';
import { controlAdaptersPersistDenylist } from 'features/controlAdapters/store/controlAdaptersPersistDenylist';
import controlAdaptersReducer, {
@@ -25,32 +18,17 @@ import dynamicPromptsReducer, {
migrateDynamicPromptsState,
} from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { galleryPersistDenylist } from 'features/gallery/store/galleryPersistDenylist';
import galleryReducer, {
initialGalleryState,
migrateGalleryState,
} from 'features/gallery/store/gallerySlice';
import hrfReducer, {
initialHRFState,
migrateHRFState,
} from 'features/hrf/store/hrfSlice';
import loraReducer, {
initialLoraState,
migrateLoRAState,
} from 'features/lora/store/loraSlice';
import galleryReducer, { initialGalleryState, migrateGalleryState } from 'features/gallery/store/gallerySlice';
import hrfReducer, { initialHRFState, migrateHRFState } from 'features/hrf/store/hrfSlice';
import loraReducer, { initialLoraState, migrateLoRAState } from 'features/lora/store/loraSlice';
import modelmanagerReducer, {
initialModelManagerState,
migrateModelManagerState,
} from 'features/modelManager/store/modelManagerSlice';
import { nodesPersistDenylist } from 'features/nodes/store/nodesPersistDenylist';
import nodesReducer, {
initialNodesState,
migrateNodesState,
} from 'features/nodes/store/nodesSlice';
import nodesReducer, { initialNodesState, migrateNodesState } from 'features/nodes/store/nodesSlice';
import nodeTemplatesReducer from 'features/nodes/store/nodeTemplatesSlice';
import workflowReducer, {
initialWorkflowState,
migrateWorkflowState,
} from 'features/nodes/store/workflowSlice';
import workflowReducer, { initialWorkflowState, migrateWorkflowState } from 'features/nodes/store/workflowSlice';
import { generationPersistDenylist } from 'features/parameters/store/generationPersistDenylist';
import generationReducer, {
initialGenerationState,
@@ -62,21 +40,12 @@ import postprocessingReducer, {
migratePostprocessingState,
} from 'features/parameters/store/postprocessingSlice';
import queueReducer from 'features/queue/store/queueSlice';
import sdxlReducer, {
initialSDXLState,
migrateSDXLState,
} from 'features/sdxl/store/sdxlSlice';
import sdxlReducer, { initialSDXLState, migrateSDXLState } from 'features/sdxl/store/sdxlSlice';
import configReducer from 'features/system/store/configSlice';
import { systemPersistDenylist } from 'features/system/store/systemPersistDenylist';
import systemReducer, {
initialSystemState,
migrateSystemState,
} from 'features/system/store/systemSlice';
import systemReducer, { initialSystemState, migrateSystemState } from 'features/system/store/systemSlice';
import { uiPersistDenylist } from 'features/ui/store/uiPersistDenylist';
import uiReducer, {
initialUIState,
migrateUIState,
} from 'features/ui/store/uiSlice';
import uiReducer, { initialUIState, migrateUIState } from 'features/ui/store/uiSlice';
import { diff } from 'jsondiffpatch';
import { defaultsDeep, keys, omit, pick } from 'lodash-es';
import dynamicMiddlewares from 'redux-dynamic-middlewares';
@@ -206,10 +175,7 @@ const unserialize: UnserializeFunction = (data, key) => {
);
return transformed;
} catch (err) {
log.warn(
{ error: serializeError(err) },
`Error rehydrating slice "${key}", falling back to default initial state`
);
log.warn({ error: serializeError(err) }, `Error rehydrating slice "${key}", falling back to default initial state`);
return config.initialState;
}
};
@@ -229,10 +195,7 @@ const serializationDenylist: {
};
export const serialize: SerializeFunction = (data, key) => {
const result = omit(
data,
serializationDenylist[key as keyof typeof serializationDenylist] ?? []
);
const result = omit(data, serializationDenylist[key as keyof typeof serializationDenylist] ?? []);
return JSON.stringify(result);
};
@@ -256,9 +219,7 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
persistDebounce: 300,
serialize,
unserialize,
prefix: uniqueStoreKey
? `${STORAGE_PREFIX}${uniqueStoreKey}-`
: STORAGE_PREFIX,
prefix: uniqueStoreKey ? `${STORAGE_PREFIX}${uniqueStoreKey}-` : STORAGE_PREFIX,
errorHandler,
})
);
@@ -278,9 +239,7 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
},
});
export type AppGetState = ReturnType<
ReturnType<typeof createStore>['getState']
>;
export type AppGetState = ReturnType<ReturnType<typeof createStore>['getState']>;
export type RootState = ReturnType<ReturnType<typeof createStore>['getState']>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AppThunkDispatch = ThunkDispatch<RootState, any, UnknownAction>;

View File

@@ -1,17 +1,9 @@
import type { ChakraProps } from '@invoke-ai/ui';
import {
CompositeNumberInput,
Flex,
FormControl,
FormLabel,
} from '@invoke-ai/ui';
import type { ChakraProps } from '@invoke-ai/ui-library';
import { CompositeNumberInput, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { CSSProperties } from 'react';
import { memo, useCallback } from 'react';
import { RgbaColorPicker } from 'react-colorful';
import type {
ColorPickerBaseProps,
RgbaColor,
} from 'react-colorful/dist/types';
import type { ColorPickerBaseProps, RgbaColor } from 'react-colorful/dist/types';
import { useTranslation } from 'react-i18next';
type IAIColorPickerProps = ColorPickerBaseProps<RgbaColor> & {
@@ -39,30 +31,13 @@ const numberInputWidth: ChakraProps['w'] = '4.2rem';
const IAIColorPicker = (props: IAIColorPickerProps) => {
const { color, onChange, withNumberInput, ...rest } = props;
const { t } = useTranslation();
const handleChangeR = useCallback(
(r: number) => onChange({ ...color, r }),
[color, onChange]
);
const handleChangeG = useCallback(
(g: number) => onChange({ ...color, g }),
[color, onChange]
);
const handleChangeB = useCallback(
(b: number) => onChange({ ...color, b }),
[color, onChange]
);
const handleChangeA = useCallback(
(a: number) => onChange({ ...color, a }),
[color, onChange]
);
const handleChangeR = useCallback((r: number) => onChange({ ...color, r }), [color, onChange]);
const handleChangeG = useCallback((g: number) => onChange({ ...color, g }), [color, onChange]);
const handleChangeB = useCallback((b: number) => onChange({ ...color, b }), [color, onChange]);
const handleChangeA = useCallback((a: number) => onChange({ ...color, a }), [color, onChange]);
return (
<Flex sx={sx}>
<RgbaColorPicker
color={color}
onChange={onChange}
style={colorPickerStyles}
{...rest}
/>
<RgbaColorPicker color={color} onChange={onChange} style={colorPickerStyles} {...rest} />
{withNumberInput && (
<Flex>
<FormControl>

View File

@@ -1,22 +1,11 @@
import type { ChakraProps, FlexProps, SystemStyleObject } from '@invoke-ai/ui';
import { Flex, Icon, Image } from '@invoke-ai/ui';
import {
IAILoadingImageFallback,
IAINoContentFallback,
} from 'common/components/IAIImageFallback';
import type { ChakraProps, FlexProps, SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex, Icon, Image } from '@invoke-ai/ui-library';
import { IAILoadingImageFallback, IAINoContentFallback } from 'common/components/IAIImageFallback';
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import type {
TypesafeDraggableData,
TypesafeDroppableData,
} from 'features/dnd/types';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
import type {
MouseEvent,
ReactElement,
ReactNode,
SyntheticEvent,
} from 'react';
import type { MouseEvent, ReactElement, ReactNode, SyntheticEvent } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { PiImageBold, PiUploadSimpleBold } from 'react-icons/pi';
import type { ImageDTO, PostUploadAction } from 'services/api/types';
@@ -161,14 +150,8 @@ const IAIDndImage = (props: IAIDndImageProps) => {
<Image
src={thumbnail ? imageDTO.thumbnail_url : imageDTO.image_url}
fallbackStrategy="beforeLoadOrError"
fallbackSrc={
useThumbailFallback ? imageDTO.thumbnail_url : undefined
}
fallback={
useThumbailFallback ? undefined : (
<IAILoadingImageFallback image={imageDTO} />
)
}
fallbackSrc={useThumbailFallback ? imageDTO.thumbnail_url : undefined}
fallback={useThumbailFallback ? undefined : <IAILoadingImageFallback image={imageDTO} />}
onError={onError}
draggable={false}
w={imageDTO.width}
@@ -179,13 +162,8 @@ const IAIDndImage = (props: IAIDndImageProps) => {
sx={imageSx}
data-testid={dataTestId}
/>
{withMetadataOverlay && (
<ImageMetadataOverlay imageDTO={imageDTO} />
)}
<SelectionOverlay
isSelected={isSelected}
isHovered={withHoverOverlay ? isHovered : false}
/>
{withMetadataOverlay && <ImageMetadataOverlay imageDTO={imageDTO} />}
<SelectionOverlay isSelected={isSelected} isHovered={withHoverOverlay ? isHovered : false} />
</Flex>
)}
{!imageDTO && !isUploadDisabled && (
@@ -198,20 +176,10 @@ const IAIDndImage = (props: IAIDndImageProps) => {
)}
{!imageDTO && isUploadDisabled && noContentFallback}
{imageDTO && !isDragDisabled && (
<IAIDraggable
data={draggableData}
disabled={isDragDisabled || !imageDTO}
onClick={onClick}
/>
<IAIDraggable data={draggableData} disabled={isDragDisabled || !imageDTO} onClick={onClick} />
)}
{children}
{!isDropDisabled && (
<IAIDroppable
data={droppableData}
disabled={isDropDisabled}
dropLabel={dropLabel}
/>
)}
{!isDropDisabled && <IAIDroppable data={droppableData} disabled={isDropDisabled} dropLabel={dropLabel} />}
</Flex>
)}
</ImageContextMenu>

View File

@@ -1,5 +1,5 @@
import type { SystemStyleObject } from '@invoke-ai/ui';
import { IconButton } from '@invoke-ai/ui';
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { IconButton } from '@invoke-ai/ui-library';
import type { MouseEvent, ReactElement } from 'react';
import { memo, useMemo } from 'react';

View File

@@ -1,5 +1,5 @@
import type { BoxProps } from '@invoke-ai/ui';
import { Box } from '@invoke-ai/ui';
import type { BoxProps } from '@invoke-ai/ui-library';
import { Box } from '@invoke-ai/ui-library';
import { useDraggableTypesafe } from 'features/dnd/hooks/typesafeHooks';
import type { TypesafeDraggableData } from 'features/dnd/types';
import { memo, useRef } from 'react';

View File

@@ -1,4 +1,4 @@
import { Box, Flex } from '@invoke-ai/ui';
import { Box, Flex } from '@invoke-ai/ui-library';
import type { AnimationProps } from 'framer-motion';
import { motion } from 'framer-motion';
import type { ReactNode } from 'react';
@@ -27,12 +27,7 @@ const IAIDropOverlay = (props: Props) => {
const { isOver, label = t('gallery.drop') } = props;
const motionId = useRef(uuidv4());
return (
<motion.div
key={motionId.current}
initial={initial}
animate={animate}
exit={exit}
>
<motion.div key={motionId.current} initial={initial} animate={animate} exit={exit}>
<Flex position="absolute" top={0} insetInlineStart={0} w="full" h="full">
<Flex
position="absolute"

View File

@@ -1,4 +1,4 @@
import { Box } from '@invoke-ai/ui';
import { Box } from '@invoke-ai/ui-library';
import { useDroppableTypesafe } from 'features/dnd/hooks/typesafeHooks';
import type { TypesafeDroppableData } from 'features/dnd/types';
import { isValidDrop } from 'features/dnd/util/isValidDrop';
@@ -36,9 +36,7 @@ const IAIDroppable = (props: IAIDroppableProps) => {
pointerEvents={active ? 'auto' : 'none'}
>
<AnimatePresence>
{isValidDrop(data, active) && (
<IAIDropOverlay isOver={isOver} label={dropLabel} />
)}
{isValidDrop(data, active) && <IAIDropOverlay isOver={isOver} label={dropLabel} />}
</AnimatePresence>
</Box>
);

View File

@@ -1,5 +1,5 @@
import type { SystemStyleObject } from '@invoke-ai/ui';
import { Box, Skeleton } from '@invoke-ai/ui';
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Skeleton } from '@invoke-ai/ui-library';
import { memo } from 'react';
const skeletonStyles: SystemStyleObject = {
@@ -16,13 +16,7 @@ const skeletonStyles: SystemStyleObject = {
const IAIFillSkeleton = () => {
return (
<Skeleton sx={skeletonStyles}>
<Box
position="absolute"
top={0}
insetInlineStart={0}
height="full"
width="full"
/>
<Box position="absolute" top={0} insetInlineStart={0} height="full" width="full" />
</Skeleton>
);
};

View File

@@ -1,5 +1,5 @@
import type { As, ChakraProps, FlexProps } from '@invoke-ai/ui';
import { Flex, Icon, Skeleton, Spinner, Text } from '@invoke-ai/ui';
import type { As, ChakraProps, FlexProps } from '@invoke-ai/ui-library';
import { Flex, Icon, Skeleton, Spinner, Text } from '@invoke-ai/ui-library';
import { memo, useMemo } from 'react';
import { PiImageBold } from 'react-icons/pi';
import type { ImageDTO } from 'services/api/types';
@@ -19,15 +19,7 @@ export const IAILoadingImageFallback = memo((props: Props) => {
}
return (
<Flex
opacity={0.7}
w="full"
h="full"
alignItems="center"
justifyContent="center"
borderRadius="base"
bg="base.900"
>
<Flex opacity={0.7} w="full" h="full" alignItems="center" justifyContent="center" borderRadius="base" bg="base.900">
<Spinner size="xl" />
</Flex>
);
@@ -77,32 +69,30 @@ type IAINoImageFallbackWithSpinnerProps = FlexProps & {
label?: string;
};
export const IAINoContentFallbackWithSpinner = memo(
(props: IAINoImageFallbackWithSpinnerProps) => {
const { sx, ...rest } = props;
const styles = useMemo(
() => ({
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'base',
flexDir: 'column',
gap: 2,
userSelect: 'none',
opacity: 0.7,
color: 'base.500',
...sx,
}),
[sx]
);
export const IAINoContentFallbackWithSpinner = memo((props: IAINoImageFallbackWithSpinnerProps) => {
const { sx, ...rest } = props;
const styles = useMemo(
() => ({
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'base',
flexDir: 'column',
gap: 2,
userSelect: 'none',
opacity: 0.7,
color: 'base.500',
...sx,
}),
[sx]
);
return (
<Flex sx={styles} {...rest}>
<Spinner size="xl" />
{props.label && <Text textAlign="center">{props.label}</Text>}
</Flex>
);
}
);
return (
<Flex sx={styles} {...rest}>
<Spinner size="xl" />
{props.label && <Text textAlign="center">{props.label}</Text>}
</Flex>
);
});
IAINoContentFallbackWithSpinner.displayName = 'IAINoContentFallbackWithSpinner';

View File

@@ -1,4 +1,4 @@
import { Badge, Flex } from '@invoke-ai/ui';
import { Badge, Flex } from '@invoke-ai/ui-library';
import { memo } from 'react';
import type { ImageDTO } from 'services/api/types';

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