Compare commits

..

1 Commits

Author SHA1 Message Date
a50fad15f1 absolutize model paths returned by the web API 2023-07-17 07:16:45 -04:00
657 changed files with 18459 additions and 29602 deletions

View File

@ -20,13 +20,13 @@ def calc_images_mean_L1(image1_path, image2_path):
def parse_args(): def parse_args():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("image1_path") parser.add_argument('image1_path')
parser.add_argument("image2_path") parser.add_argument('image2_path')
args = parser.parse_args() args = parser.parse_args()
return args return args
if __name__ == "__main__": if __name__ == '__main__':
args = parse_args() args = parse_args()
mean_L1 = calc_images_mean_L1(args.image1_path, args.image2_path) mean_L1 = calc_images_mean_L1(args.image1_path, args.image2_path)
print(mean_L1) print(mean_L1)

View File

@ -1,2 +1 @@
b3dccfaeb636599c02effc377cdd8a87d658256c b3dccfaeb636599c02effc377cdd8a87d658256c
218b6d0546b990fc449c876fb99f44b50c4daa35

View File

@ -1,11 +1,11 @@
name: Close inactive issues name: Close inactive issues
on: on:
schedule: schedule:
- cron: "00 4 * * *" - cron: "00 6 * * *"
env: env:
DAYS_BEFORE_ISSUE_STALE: 30 DAYS_BEFORE_ISSUE_STALE: 14
DAYS_BEFORE_ISSUE_CLOSE: 14 DAYS_BEFORE_ISSUE_CLOSE: 28
jobs: jobs:
close-issues: close-issues:
@ -14,7 +14,7 @@ jobs:
issues: write issues: write
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/stale@v8 - uses: actions/stale@v5
with: with:
days-before-issue-stale: ${{ env.DAYS_BEFORE_ISSUE_STALE }} days-before-issue-stale: ${{ env.DAYS_BEFORE_ISSUE_STALE }}
days-before-issue-close: ${{ env.DAYS_BEFORE_ISSUE_CLOSE }} days-before-issue-close: ${{ env.DAYS_BEFORE_ISSUE_CLOSE }}
@ -23,6 +23,5 @@ jobs:
close-issue-message: "Due to inactivity, this issue was automatically closed. If you are still experiencing the issue, please recreate the issue." close-issue-message: "Due to inactivity, this issue was automatically closed. If you are still experiencing the issue, please recreate the issue."
days-before-pr-stale: -1 days-before-pr-stale: -1
days-before-pr-close: -1 days-before-pr-close: -1
exempt-issue-labels: "Active Issue"
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
operations-per-run: 500 operations-per-run: 500

View File

@ -2,6 +2,8 @@ name: Lint frontend
on: on:
pull_request: pull_request:
paths:
- 'invokeai/frontend/web/**'
types: types:
- 'ready_for_review' - 'ready_for_review'
- 'opened' - 'opened'
@ -9,6 +11,8 @@ on:
push: push:
branches: branches:
- 'main' - 'main'
paths:
- 'invokeai/frontend/web/**'
merge_group: merge_group:
workflow_dispatch: workflow_dispatch:

View File

@ -2,7 +2,7 @@ name: mkdocs-material
on: on:
push: push:
branches: branches:
- 'refs/heads/main' - 'refs/heads/v2.3'
permissions: permissions:
contents: write contents: write
@ -43,7 +43,7 @@ jobs:
--verbose --verbose
- name: deploy to gh-pages - name: deploy to gh-pages
if: ${{ github.ref == 'refs/heads/main' }} if: ${{ github.ref == 'refs/heads/v2.3' }}
run: | run: |
python -m \ python -m \
mkdocs gh-deploy \ mkdocs gh-deploy \

View File

@ -1,27 +0,0 @@
name: Black # TODO: add isort and flake8 later
on:
pull_request: {}
push:
branches: master
tags: "*"
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies with pip
run: |
pip install --upgrade pip wheel
pip install .[test]
# - run: isort --check-only .
- run: black --check .
# - run: flake8

1
.gitignore vendored
View File

@ -38,6 +38,7 @@ develop-eggs/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib/
lib64/ lib64/
parts/ parts/
sdist/ sdist/

View File

@ -1,10 +0,0 @@
# See https://pre-commit.com/ for usage and config
repos:
- repo: local
hooks:
- id: black
name: black
stages: [commit]
language: system
entry: black
types: [python]

View File

@ -1,290 +0,0 @@
Copyright (c) 2023 Stability AI
CreativeML Open RAIL++-M License dated July 26, 2023
Section I: PREAMBLE
Multimodal generative models are being widely adopted and used, and
have the potential to transform the way artists, among other
individuals, conceive and benefit from AI or ML technologies as a tool
for content creation.
Notwithstanding the current and potential benefits that these
artifacts can bring to society at large, there are also concerns about
potential misuses of them, either due to their technical limitations
or ethical considerations.
In short, this license strives for both the open and responsible
downstream use of the accompanying model. When it comes to the open
character, we took inspiration from open source permissive licenses
regarding the grant of IP rights. Referring to the downstream
responsible use, we added use-based restrictions not permitting the
use of the model in very specific scenarios, in order for the licensor
to be able to enforce the license in case potential misuses of the
Model may occur. At the same time, we strive to promote open and
responsible research on generative models for art and content
generation.
Even though downstream derivative versions of the model could be
released under different licensing terms, the latter will always have
to include - at minimum - the same use-based restrictions as the ones
in the original license (this license). We believe in the intersection
between open and responsible AI development; thus, this agreement aims
to strike a balance between both in order to enable responsible
open-science in the field of AI.
This CreativeML Open RAIL++-M License governs the use of the model
(and its derivatives) and is informed by the model card associated
with the model.
NOW THEREFORE, You and Licensor agree as follows:
Definitions
"License" means the terms and conditions for use, reproduction, and
Distribution as defined in this document.
"Data" means a collection of information and/or content extracted from
the dataset used with the Model, including to train, pretrain, or
otherwise evaluate the Model. The Data is not licensed under this
License.
"Output" means the results of operating a Model as embodied in
informational content resulting therefrom.
"Model" means any accompanying machine-learning based assemblies
(including checkpoints), consisting of learnt weights, parameters
(including optimizer states), corresponding to the model architecture
as embodied in the Complementary Material, that have been trained or
tuned, in whole or in part on the Data, using the Complementary
Material.
"Derivatives of the Model" means all modifications to the Model, works
based on the Model, or any other model which is created or initialized
by transfer of patterns of the weights, parameters, activations or
output of the Model, to the other model, in order to cause the other
model to perform similarly to the Model, including - but not limited
to - distillation methods entailing the use of intermediate data
representations or methods based on the generation of synthetic data
by the Model for training the other model.
"Complementary Material" means the accompanying source code and
scripts used to define, run, load, benchmark or evaluate the Model,
and used to prepare data for training or evaluation, if any. This
includes any accompanying documentation, tutorials, examples, etc, if
any.
"Distribution" means any transmission, reproduction, publication or
other sharing of the Model or Derivatives of the Model to a third
party, including providing the Model as a hosted service made
available by electronic or other remote means - e.g. API-based or web
access.
"Licensor" means the copyright owner or entity authorized by the
copyright owner that is granting the License, including the persons or
entities that may have rights in the Model and/or distributing the
Model.
"You" (or "Your") means an individual or Legal Entity exercising
permissions granted by this License and/or making use of the Model for
whichever purpose and in any field of use, including usage of the
Model in an end-use application - e.g. chatbot, translator, image
generator.
"Third Parties" means individuals or legal entities that are not under
common control with Licensor or You.
"Contribution" means any work of authorship, including the original
version of the Model and any modifications or additions to that Model
or Derivatives of the Model thereof, that is intentionally submitted
to Licensor for inclusion in the Model by the copyright owner or by an
individual or Legal Entity authorized to submit on behalf of the
copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent to
the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control
systems, and issue tracking systems that are managed by, or on behalf
of, the Licensor for the purpose of discussing and improving the
Model, but excluding communication that is conspicuously marked or
otherwise designated in writing by the copyright owner as "Not a
Contribution."
"Contributor" means Licensor and any individual or Legal Entity on
behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Model.
Section II: INTELLECTUAL PROPERTY RIGHTS
Both copyright and patent grants apply to the Model, Derivatives of
the Model and Complementary Material. The Model and Derivatives of the
Model are subject to additional terms as described in
Section III.
Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare, publicly display, publicly
perform, sublicense, and distribute the Complementary Material, the
Model, and Derivatives of the Model.
Grant of Patent License. Subject to the terms and conditions of this
License and where and as applicable, each Contributor hereby grants to
You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this paragraph) patent license to
make, have made, use, offer to sell, sell, import, and otherwise
transfer the Model and the Complementary Material, where such license
applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by
combination of their Contribution(s) with the Model to which such
Contribution(s) was submitted. If You institute patent litigation
against any entity (including a cross-claim or counterclaim in a
lawsuit) alleging that the Model and/or Complementary Material or a
Contribution incorporated within the Model and/or Complementary
Material constitutes direct or contributory patent infringement, then
any patent licenses granted to You under this License for the Model
and/or Work shall terminate as of the date such litigation is asserted
or filed.
Section III: CONDITIONS OF USAGE, DISTRIBUTION AND REDISTRIBUTION
Distribution and Redistribution. You may host for Third Party remote
access purposes (e.g. software-as-a-service), reproduce and distribute
copies of the Model or Derivatives of the Model thereof in any medium,
with or without modifications, provided that You meet the following
conditions: Use-based restrictions as referenced in paragraph 5 MUST
be included as an enforceable provision by You in any type of legal
agreement (e.g. a license) governing the use and/or distribution of
the Model or Derivatives of the Model, and You shall give notice to
subsequent users You Distribute to, that the Model or Derivatives of
the Model are subject to paragraph 5. This provision does not apply to
the use of Complementary Material. You must give any Third Party
recipients of the Model or Derivatives of the Model a copy of this
License; You must cause any modified files to carry prominent notices
stating that You changed the files; You must retain all copyright,
patent, trademark, and attribution notices excluding those notices
that do not pertain to any part of the Model, Derivatives of the
Model. You may add Your own copyright statement to Your modifications
and may provide additional or different license terms and conditions -
respecting paragraph 4.a. - for use, reproduction, or Distribution of
Your modifications, or for any such Derivatives of the Model as a
whole, provided Your use, reproduction, and Distribution of the Model
otherwise complies with the conditions stated in this License.
Use-based restrictions. The restrictions set forth in Attachment A are
considered Use-based restrictions. Therefore You cannot use the Model
and the Derivatives of the Model for the specified restricted
uses. You may use the Model subject to this License, including only
for lawful purposes and in accordance with the License. Use may
include creating any content with, finetuning, updating, running,
training, evaluating and/or reparametrizing the Model. You shall
require all of Your users who use the Model or a Derivative of the
Model to comply with the terms of this paragraph (paragraph 5).
The Output You Generate. Except as set forth herein, Licensor claims
no rights in the Output You generate using the Model. You are
accountable for the Output you generate and its subsequent uses. No
use of the output can contravene any provision as stated in the
License.
Section IV: OTHER PROVISIONS
Updates and Runtime Restrictions. To the maximum extent permitted by
law, Licensor reserves the right to restrict (remotely or otherwise)
usage of the Model in violation of this License.
Trademarks and related. Nothing in this License permits You to make
use of Licensors trademarks, trade names, logos or to otherwise
suggest endorsement or misrepresent the relationship between the
parties; and any rights not expressly granted herein are reserved by
the Licensors.
Disclaimer of Warranty. Unless required by applicable law or agreed to
in writing, Licensor provides the Model and the Complementary Material
(and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Model, Derivatives of
the Model, and the Complementary Material and assume any risks
associated with Your exercise of permissions under this License.
Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise, unless
required by applicable law (such as deliberate and grossly negligent
acts) or agreed to in writing, shall any Contributor be liable to You
for damages, including any direct, indirect, special, incidental, or
consequential damages of any character arising as a result of this
License or out of the use or inability to use the Model and the
Complementary Material (including but not limited to damages for loss
of goodwill, work stoppage, computer failure or malfunction, or any
and all other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
Accepting Warranty or Additional Liability. While redistributing the
Model, Derivatives of the Model and the Complementary Material
thereof, You may choose to offer, and charge a fee for, acceptance of
support, warranty, indemnity, or other liability obligations and/or
rights consistent with this License. However, in accepting such
obligations, You may act only on Your own behalf and on Your sole
responsibility, not on behalf of any other Contributor, and only if
You agree to indemnify, defend, and hold each Contributor harmless for
any liability incurred by, or claims asserted against, such
Contributor by reason of your accepting any such warranty or
additional liability.
If any provision of this License is held to be invalid, illegal or
unenforceable, the remaining provisions shall be unaffected thereby
and remain valid as if such provision had not been set forth herein.
END OF TERMS AND CONDITIONS
Attachment A
Use Restrictions
You agree not to use the Model or Derivatives of the Model:
* In any way that violates any applicable national, federal, state,
local or international law or regulation;
* For the purpose of exploiting, harming or attempting to exploit or
harm minors in any way;
* To generate or disseminate verifiably false information and/or
content with the purpose of harming others;
* To generate or disseminate personal identifiable information that
can be used to harm an individual;
* To defame, disparage or otherwise harass others;
* For fully automated decision making that adversely impacts an
individuals legal rights or otherwise creates or modifies a
binding, enforceable obligation;
* For any use intended to or which has the effect of discriminating
against or harming individuals or groups based on online or offline
social behavior or known or predicted personal or personality
characteristics;
* To exploit any of the vulnerabilities of a specific group of persons
based on their age, social, physical or mental characteristics, in
order to materially distort the behavior of a person pertaining to
that group in a manner that causes or is likely to cause that person
or another person physical or psychological harm;
* For any use intended to or which has the effect of discriminating
against individuals or groups based on legally protected
characteristics or categories;
* To provide medical advice and medical results interpretation;
* To generate or disseminate information for the purpose to be used
for administration of justice, law enforcement, immigration or
asylum processes, such as predicting an individual will commit
fraud/crime commitment (e.g. by text profiling, drawing causal
relationships between assertions made in documents, indiscriminate
and arbitrarily-targeted use).

View File

@ -36,6 +36,15 @@
</div> </div>
_**Note: This is an alpha release. Bugs are expected and not all
features are fully implemented. Please use the GitHub [Issues
pages](https://github.com/invoke-ai/InvokeAI/issues?q=is%3Aissue+is%3Aopen)
to report unexpected problems. Also note that InvokeAI root directory
which contains models, outputs and configuration files, has changed
between the 2.x and 3.x release. If you wish to use your v2.3 root
directory with v3.0, please follow the directions in [Migrating a 2.3
root directory to 3.0](#migrating-to-3).**_
InvokeAI is a leading creative engine built to empower professionals InvokeAI is a leading creative engine built to empower professionals
and enthusiasts alike. Generate and create stunning visual media using and enthusiasts alike. Generate and create stunning visual media using
the latest AI-driven technologies. InvokeAI offers an industry leading the latest AI-driven technologies. InvokeAI offers an industry leading
@ -123,7 +132,7 @@ and go to http://localhost:9090.
### Command-Line Installation (for developers and users familiar with Terminals) ### Command-Line Installation (for developers and users familiar with Terminals)
You must have Python 3.9 through 3.11 installed on your machine. Earlier or You must have Python 3.9 or 3.10 installed on your machine. Earlier or
later versions are not supported. later versions are not supported.
Node.js also needs to be installed along with yarn (can be installed with Node.js also needs to be installed along with yarn (can be installed with
the command `npm install -g yarn` if needed) the command `npm install -g yarn` if needed)
@ -255,24 +264,19 @@ old models directory (which contains the models selected at install
time) will be renamed `models.orig` and can be deleted once you have time) will be renamed `models.orig` and can be deleted once you have
confirmed that the migration was successful. confirmed that the migration was successful.
If you wish, you can pass the 2.3 root directory to both `--from` and
`--to` in order to update in place. Warning: this directory will no
longer be usable with InvokeAI 2.3.
#### Migrating in place #### Migrating in place
For the adventurous, you may do an in-place upgrade from 2.3 to 3.0 For the adventurous, you may do an in-place upgrade from 2.3 to 3.0
without touching the command line. ***This recipe does not work on without touching the command line. The recipe is as follows>
Windows platforms due to a bug in the Windows version of the 2.3
upgrade script.** See the next section for a Windows recipe.
##### For Mac and Linux Users:
1. Launch the InvokeAI launcher script in your current v2.3 root directory. 1. Launch the InvokeAI launcher script in your current v2.3 root directory.
2. Select option [9] "Update InvokeAI" to bring up the updater dialog. 2. Select option [9] "Update InvokeAI" to bring up the updater dialog.
3. Select option [1] to upgrade to the latest release. 3a. During the alpha release phase, select option [3] and manually
enter the tag name `v3.0.0+a2`.
3b. Once 3.0 is released, select option [1] to upgrade to the latest release.
4. Once the upgrade is finished you will be returned to the launcher 4. Once the upgrade is finished you will be returned to the launcher
menu. Select option [7] "Re-run the configure script to fix a broken menu. Select option [7] "Re-run the configure script to fix a broken
@ -291,33 +295,14 @@ worked, you can safely remove these files. Alternatively you can
restore a working v2.3 directory by removing the new files and restore a working v2.3 directory by removing the new files and
restoring the ".orig" files' original names. restoring the ".orig" files' original names.
##### For Windows Users:
Windows Users can upgrade with the
1. Enter the 2.3 root directory you wish to upgrade
2. Launch `invoke.sh` or `invoke.bat`
3. Select the "Developer's console" option [8]
4. Type the following commands
```
pip install "invokeai @ https://github.com/invoke-ai/InvokeAI/archive/refs/tags/v3.0.0" --use-pep517 --upgrade
invokeai-configure --root .
```
(Replace `v3.0.0` with the current release number if this document is out of date).
The first command will install and upgrade new software to run
InvokeAI. The second will prepare the 2.3 directory for use with 3.0.
You may now launch the WebUI in the usual way, by selecting option [1]
from the launcher script
#### Migration Caveats #### Migration Caveats
The migration script will migrate your invokeai settings and models, The migration script will migrate your invokeai settings and models,
including textual inversion models, LoRAs and merges that you may have including textual inversion models, LoRAs and merges that you may have
installed previously. However it does **not** migrate the generated installed previously. However it does **not** migrate the generated
images stored in your 2.3-format outputs directory. You will need to images stored in your 2.3-format outputs directory. The released
manually import selected images into the 3.0 gallery via drag-and-drop. version of 3.0 is expected to have an interface for importing an
entire directory of image files as a batch.
## Hardware Requirements ## Hardware Requirements
@ -329,12 +314,9 @@ AMD card (using the ROCm driver).
You will need one of the following: You will need one of the following:
- An NVIDIA-based graphics card with 4 GB or more VRAM memory. 6-8 GB - An NVIDIA-based graphics card with 4 GB or more VRAM memory.
of VRAM is highly recommended for rendering using the Stable
Diffusion XL models
- An Apple computer with an M1 chip. - An Apple computer with an M1 chip.
- An AMD-based graphics card with 4GB or more VRAM memory (Linux - An AMD-based graphics card with 4GB or more VRAM memory. (Linux only)
only), 6-8 GB for XL rendering.
We do not recommend the GTX 1650 or 1660 series video cards. They are We do not recommend the GTX 1650 or 1660 series video cards. They are
unable to run in half-precision mode and do not have sufficient VRAM unable to run in half-precision mode and do not have sufficient VRAM
@ -367,12 +349,13 @@ Invoke AI provides an organized gallery system for easily storing, accessing, an
### Other features ### Other features
- *Support for both ckpt and diffusers models* - *Support for both ckpt and diffusers models*
- *SD 2.0, 2.1, XL support* - *SD 2.0, 2.1 support*
- *Upscaling Tools* - *Upscaling Tools*
- *Embedding Manager & Support* - *Embedding Manager & Support*
- *Model Manager & Support* - *Model Manager & Support*
- *Node-Based Architecture* - *Node-Based Architecture*
- *Node-Based Plug-&-Play UI (Beta)* - *Node-Based Plug-&-Play UI (Beta)*
- *SDXL Support* (Coming soon)
### Latest Changes ### Latest Changes

View File

@ -617,6 +617,8 @@ sections describe what's new for InvokeAI.
- `dream.py` script renamed `invoke.py`. A `dream.py` script wrapper remains for - `dream.py` script renamed `invoke.py`. A `dream.py` script wrapper remains for
backward compatibility. backward compatibility.
- Completely new WebGUI - launch with `python3 scripts/invoke.py --web` - Completely new WebGUI - launch with `python3 scripts/invoke.py --web`
- Support for [inpainting](deprecated/INPAINTING.md) and
[outpainting](features/OUTPAINTING.md)
- img2img runs on all k\* samplers - img2img runs on all k\* samplers
- Support for - Support for
[negative prompts](features/PROMPTS.md#negative-and-unconditioned-prompts) [negative prompts](features/PROMPTS.md#negative-and-unconditioned-prompts)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 983 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 729 KiB

After

Width:  |  Height:  |  Size: 637 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 948 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 637 KiB

View File

@ -1,38 +1,42 @@
# How to Contribute
## Welcome to Invoke AI ## Welcome to Invoke AI
We're thrilled to have you here and we're excited for you to contribute.
Invoke AI originated as a project built by the community, and that vision carries forward today as we aim to build the best pro-grade tools available. We work together to incorporate the latest in AI/ML research, making these tools available in over 20 languages to artists and creatives around the world as part of our fully permissive OSS project designed for individual users to self-host and use. Invoke AI originated as a project built by the community, and that vision carries forward today as we aim to build the best pro-grade tools available. We work together to incorporate the latest in AI/ML research, making these tools available in over 20 languages to artists and creatives around the world as part of our fully permissive OSS project designed for individual users to self-host and use.
Here are some guidelines to help you get started:
## Contributing to Invoke AI ### Technical Prerequisites
Anyone who wishes to contribute to InvokeAI, whether features, bug fixes, code cleanup, testing, code reviews, documentation or translation is very much encouraged to do so.
To join, just raise your hand on the InvokeAI Discord server (#dev-chat) or the GitHub discussion board. Front-end: You'll need a working knowledge of React and TypeScript.
### Areas of contribution: Back-end: Depending on the scope of your contribution, you may need to know SQLite, FastAPI, Python, and Socketio. Also, a good majority of the backend logic involved in processing images is built in a modular way using a concept called "Nodes", which are isolated functions that carry out individual, discrete operations. This design allows for easy contributions of novel pipelines and capabilities.
#### Development ### How to Submit Contributions
If youd like to help with development, please see our [development guide](contribution_guides/development.md). If youre unfamiliar with contributing to open source projects, there is a tutorial contained within the development guide.
#### Documentation To start contributing, please follow these steps:
If youd like to help with documentation, please see our [documentation guide](contribution_guides/documenation.md).
#### Translation 1. Familiarize yourself with our roadmap and open projects to see where your skills and interests align. These documents can serve as a source of inspiration.
If you'd like to help with translation, please see our [translation guide](docs/contributing/.contribution_guides/translation.md). 2. Open a Pull Request (PR) with a clear description of the feature you're adding or the problem you're solving. Make sure your contribution aligns with the project's vision.
3. Adhere to general best practices. This includes assuming interoperability with other nodes, keeping the scope of your functions as small as possible, and organizing your code according to our architecture documents.
#### Tutorials ### Types of Contributions We're Looking For
Please reach out to @imic or @hipsterusername on [Discord](https://discord.gg/ZmtBAhwWhy) to help create tutorials for InvokeAI.
We hope you enjoy using our software as much as we enjoy creating it, and we hope that some of those of you who are reading this will elect to become part of our contributor community. We welcome all contributions that improve the project. Right now, we're especially looking for:
1. Quality of life (QOL) enhancements on the front-end.
2. New backend capabilities added through nodes.
3. Incorporating additional optimizations from the broader open-source software community.
### Contributors ### Communication and Decision-making Process
This project is a combined effort of dedicated people from across the world. [Check out the list of all these amazing people](https://invoke-ai.github.io/InvokeAI/other/CONTRIBUTORS/). We thank them for their time, hard work and effort. Project maintainers and code owners review PRs to ensure they align with the project's goals. They may provide design or architectural guidance, suggestions on user experience, or provide more significant feedback on the contribution itself. Expect to receive feedback on your submissions, and don't hesitate to ask questions or propose changes.
### Code of Conduct For more robust discussions, or if you're planning to add capabilities not currently listed on our roadmap, please reach out to us on our Discord server. That way, we can ensure your proposed contribution aligns with the project's direction before you start writing code.
The InvokeAI community is a welcoming place, and we want your help in maintaining that. Please review our [Code of Conduct](https://github.com/invoke-ai/InvokeAI/blob/main/CODE_OF_CONDUCT.md) to learn more - it's essential to maintaining a respectful and inclusive environment. ### Code of Conduct and Contribution Expectations
We want everyone in our community to have a positive experience. To facilitate this, we've established a code of conduct and a statement of values that we expect all contributors to adhere to. Please take a moment to review these documents—they're essential to maintaining a respectful and inclusive environment.
By making a contribution to this project, you certify that: By making a contribution to this project, you certify that:
@ -45,12 +49,6 @@ This disclaimer is not a license and does not grant any rights or permissions. Y
This disclaimer is provided "as is" without warranty of any kind, whether expressed or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, or non-infringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the contribution or the use or other dealings in the contribution. This disclaimer is provided "as is" without warranty of any kind, whether expressed or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, or non-infringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the contribution or the use or other dealings in the contribution.
### Support
For support, please use this repository's [GitHub Issues](https://github.com/invoke-ai/InvokeAI/issues), or join the [Discord](https://discord.gg/ZmtBAhwWhy).
Original portions of the software are Copyright (c) 2023 by respective contributors.
--- ---
Remember, your contributions help make this project great. We're excited to see what you'll bring to our community! Remember, your contributions help make this project great. We're excited to see what you'll bring to our community!

View File

@ -81,193 +81,3 @@ pytest --cov; open ./coverage/html/index.html
<!--#TODO: get input from blessedcoolant here, for the moment inserted the frontend README via snippets extension.--> <!--#TODO: get input from blessedcoolant here, for the moment inserted the frontend README via snippets extension.-->
--8<-- "invokeai/frontend/web/README.md" --8<-- "invokeai/frontend/web/README.md"
## Developing InvokeAI in VSCode
VSCode offers some nice tools:
- python debugger
- automatic `venv` activation
- remote dev (e.g. run InvokeAI on a beefy linux desktop while you type in
comfort on your macbook)
### Setup
You'll need the
[Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python)
and
[Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance)
extensions installed first.
It's also really handy to install the `Jupyter` extensions:
- [Jupyter](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter)
- [Jupyter Cell Tags](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.vscode-jupyter-cell-tags)
- [Jupyter Notebook Renderers](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter-renderers)
- [Jupyter Slide Show](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.vscode-jupyter-slideshow)
#### InvokeAI workspace
Creating a VSCode workspace for working on InvokeAI is highly recommended. It
can hold InvokeAI-specific settings and configs.
To make a workspace:
- Open the InvokeAI repo dir in VSCode
- `File` > `Save Workspace As` > save it _outside_ the repo
#### Default python interpreter (i.e. automatic virtual environment activation)
- Use command palette to run command
`Preferences: Open Workspace Settings (JSON)`
- Add `python.defaultInterpreterPath` to `settings`, pointing to your `venv`'s
python
Should look something like this:
```jsonc
{
// I like to have all InvokeAI-related folders in my workspace
"folders": [
{
// repo root
"path": "InvokeAI"
},
{
// InvokeAI root dir, where `invokeai.yaml` lives
"path": "/path/to/invokeai_root"
}
],
"settings": {
// Where your InvokeAI `venv`'s python executable lives
"python.defaultInterpreterPath": "/path/to/invokeai_root/.venv/bin/python"
}
}
```
Now when you open the VSCode integrated terminal, or do anything that needs to
run python, it will automatically be in your InvokeAI virtual environment.
Bonus: When you create a Jupyter notebook, when you run it, you'll be prompted
for the python interpreter to run in. This will default to your `venv` python,
and so you'll have access to the same python environment as the InvokeAI app.
This is _super_ handy.
#### Debugging configs with `launch.json`
Debugging configs are managed in a `launch.json` file. Like most VSCode configs,
these can be scoped to a workspace or folder.
Follow the [official guide](https://code.visualstudio.com/docs/python/debugging)
to set up your `launch.json` and try it out.
Now we can create the InvokeAI debugging configs:
```jsonc
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
// Run the InvokeAI backend & serve the pre-built UI
"name": "InvokeAI Web",
"type": "python",
"request": "launch",
"program": "scripts/invokeai-web.py",
"args": [
// Your InvokeAI root dir (where `invokeai.yaml` lives)
"--root",
"/path/to/invokeai_root",
// Access the app from anywhere on your local network
"--host",
"0.0.0.0"
],
"justMyCode": true
},
{
// Run the nodes-based CLI
"name": "InvokeAI CLI",
"type": "python",
"request": "launch",
"program": "scripts/invokeai-cli.py",
"justMyCode": true
},
{
// Run tests
"name": "InvokeAI Test",
"type": "python",
"request": "launch",
"module": "pytest",
"args": ["--capture=no"],
"justMyCode": true
},
{
// Run a single test
"name": "InvokeAI Single Test",
"type": "python",
"request": "launch",
"module": "pytest",
"args": [
// Change this to point to the specific test you are working on
"tests/nodes/test_invoker.py"
],
"justMyCode": true
},
{
// This is the default, useful to just run a single file
"name": "Python: File",
"type": "python",
"request": "launch",
"program": "${file}",
"justMyCode": true
}
]
}
```
You'll see these configs in the debugging configs drop down. Running them will
start InvokeAI with attached debugger, in the correct environment, and work just
like the normal app.
Enjoy debugging InvokeAI with ease (not that we have any bugs of course).
#### Remote dev
This is very easy to set up and provides the same very smooth experience as
local development. Environments and debugging, as set up above, just work,
though you'd need to recreate the workspace and debugging configs on the remote.
Consult the
[official guide](https://code.visualstudio.com/docs/remote/remote-overview) to
get it set up.
Suggest using VSCode's included settings sync so that your remote dev host has
all the same app settings and extensions automagically.
##### One remote dev gotcha
I've found the automatic port forwarding to be very flakey. You can disable it
in `Preferences: Open Remote Settings (ssh: hostname)`. Search for
`remote.autoForwardPorts` and untick the box.
To forward ports very reliably, use SSH on the remote dev client (e.g. your
macbook). Here's how to forward both backend API port (`9090`) and the frontend
live dev server port (`5173`):
```bash
ssh \
-L 9090:localhost:9090 \
-L 5173:localhost:5173 \
user@remote-dev-host
```
The forwarding stops when you close the terminal window, so suggest to do this
_outside_ the VSCode integrated terminal in case you need to restart VSCode for
an extension update or something
Now, on your remote dev client, you can open `localhost:9090` and access the UI,
now served from the remote dev host, just the same as if it was running on the
client.

View File

@ -1,91 +0,0 @@
# Development
## **What do I need to know to help?**
If you are looking to help to with a code contribution, InvokeAI uses several different technologies under the hood: Python (Pydantic, FastAPI, diffusers) and Typescript (React, Redux Toolkit, ChakraUI, Mantine, Konva). Familiarity with StableDiffusion and image generation concepts is helpful, but not essential.
For more information, please review our area specific documentation:
* #### [InvokeAI Architecure](../ARCHITECTURE.md)
* #### [Frontend Documentation](development_guides/contributingToFrontend.md)
* #### [Node Documentation](../INVOCATIONS.md)
* #### [Local Development](../LOCAL_DEVELOPMENT.md)
If you don't feel ready to make a code contribution yet, no problem! You can also help out in other ways, such as [documentation](documentation.md) or [translation](translation.md).
There are two paths to making a development contribution:
1. Choosing an open issue to address. Open issues can be found in the [Issues](https://github.com/invoke-ai/InvokeAI/issues?q=is%3Aissue+is%3Aopen) section of the InvokeAI repository. These are tagged by the issue type (bug, enhancement, etc.) along with the “good first issues” tag denoting if they are suitable for first time contributors.
1. Additional items can be found on our [roadmap](https://github.com/orgs/invoke-ai/projects/7). The roadmap is organized in terms of priority, and contains features of varying size and complexity. If there is an inflight item youd like to help with, reach out to the contributor assigned to the item to see how you can help.
2. Opening a new issue or feature to add. **Please make sure you have searched through existing issues before creating new ones.**
*Regardless of what you choose, please post in the [#dev-chat](https://discord.com/channels/1020123559063990373/1049495067846524939) channel of the Discord before you start development in order to confirm that the issue or feature is aligned with the current direction of the project. We value our contributors time and effort and want to ensure that no ones time is being misspent.*
## Best Practices:
* Keep your pull requests small. Smaller pull requests are more likely to be accepted and merged
* Comments! Commenting your code helps reviwers easily understand your contribution
* Use Python and Typescripts typing systems, and consider using an editor with [LSP](https://microsoft.github.io/language-server-protocol/) support to streamline development
* Make all communications public. This ensure knowledge is shared with the whole community
## **How do I make a contribution?**
Never made an open source contribution before? Wondering how contributions work in our project? Here's a quick rundown!
Before starting these steps, ensure you have your local environment [configured for development](../LOCAL_DEVELOPMENT.md).
1. Find a [good first issue](https://github.com/invoke-ai/InvokeAI/contribute) that you are interested in addressing or a feature that you would like to add. Then, reach out to our team in the [#dev-chat](https://discord.com/channels/1020123559063990373/1049495067846524939) channel of the Discord to ensure you are setup for success.
2. Fork the [InvokeAI](https://github.com/invoke-ai/InvokeAI) repository to your GitHub profile. This means that you will have a copy of the repository under **your-GitHub-username/InvokeAI**.
3. Clone the repository to your local machine using:
```bash
git clone https://github.com/your-GitHub-username/InvokeAI.git
```
If you're unfamiliar with using Git through the commandline, [GitHub Desktop](https://desktop.github.com) is a easy-to-use alternative with a UI. You can do all the same steps listed here, but through the interface.
4. Create a new branch for your fix using:
```bash
git checkout -b branch-name-here
```
5. Make the appropriate changes for the issue you are trying to address or the feature that you want to add.
6. Add the file contents of the changed files to the "snapshot" git uses to manage the state of the project, also known as the index:
```bash
git add insert-paths-of-changed-files-here
```
7. Store the contents of the index with a descriptive message.
```bash
git commit -m "Insert a short message of the changes made here"
```
8. Push the changes to the remote repository using
```markdown
git push origin branch-name-here
```
9. Submit a pull request to the **main** branch of the InvokeAI repository.
10. Title the pull request with a short description of the changes made and the issue or bug number associated with your change. For example, you can title an issue like so "Added more log outputting to resolve #1234".
11. In the description of the pull request, explain the changes that you made, any issues you think exist with the pull request you made, and any questions you have for the maintainer. It's OK if your pull request is not perfect (no pull request is), the reviewer will be able to help you fix any problems and improve it!
12. Wait for the pull request to be reviewed by other collaborators.
13. Make changes to the pull request if the reviewer(s) recommend them.
14. Celebrate your success after your pull request is merged!
If youd like to learn more about contributing to Open Source projects, here is a [Getting Started Guide](https://opensource.com/article/19/7/create-pull-request-github).
## **Where can I go for help?**
If you need help, you can ask questions in the [#dev-chat](https://discord.com/channels/1020123559063990373/1049495067846524939) channel of the Discord.
For frontend related work, **@pyschedelicious** is the best person to reach out to.
For backend related work, please reach out to **@blessedcoolant**, **@lstein**, **@StAlKeR7779** or **@pyschedelicious**.
## **What does the Code of Conduct mean for me?**
Our [Code of Conduct](CODE_OF_CONDUCT.md) means that you are responsible for treating everyone on the project with respect and courtesy regardless of their identity. If you are the victim of any inappropriate behavior or comments as described in our Code of Conduct, we are here for you and will do the best to ensure that the abuser is reprimanded appropriately, per our code.

View File

@ -1,75 +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 [yarn classic](https://classic.yarnpkg.com/lang/en/) and confirm it is installed by running this:
```bash
npm install --global yarn
yarn --version
```
From `invokeai/frontend/web/` run `yarn install` to get everything set up.
Start everything in dev mode:
1. Ensure your virtual environment is running
2. Start the dev server: `yarn 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 `yarn build`.

View File

@ -1,13 +0,0 @@
# Documentation
Documentation is an important part of any open source project. It provides a clear and concise way to communicate how the software works, how to use it, and how to troubleshoot issues. Without proper documentation, it can be difficult for users to understand the purpose and functionality of the project.
## Contributing
All documentation is maintained in the InvokeAI GitHub repository. If you come across documentation that is out of date or incorrect, please submit a pull request with the necessary changes.
When updating or creating documentation, please keep in mind InvokeAI is a tool for everyone, not just those who have familiarity with generative art.
## Help & Questions
Please ping @imic1 or @hipsterusername in the [Discord](https://discord.com/channels/1020123559063990373/1049495067846524939) if you have any questions.

View File

@ -1,19 +0,0 @@
# Translation
InvokeAI uses [Weblate](https://weblate.org/) for translation. Weblate is a FOSS project providing a scalable translation service. Weblate automates the tedious parts of managing translation of a growing project, and the service is generously provided at no cost to FOSS projects like InvokeAI.
## Contributing
If you'd like to contribute by adding or updating a translation, please visit our [Weblate project](https://hosted.weblate.org/engage/invokeai/). You'll need to sign in with your GitHub account (a number of other accounts are supported, including Google).
Once signed in, select a language and then the Web UI component. From here you can Browse and Translate strings from English to your chosen language. Zen mode offers a simpler translation experience.
Your changes will be attributed to you in the automated PR process; you don't need to do anything else.
## Help & Questions
Please check Weblate's [documentation](https://docs.weblate.org/en/latest/index.html) or ping @Harvestor on [Discord](https://discord.com/channels/1020123559063990373/1049495067846524939) if you have any questions.
## Thanks
Thanks to the InvokeAI community for their efforts to translate the project!

View File

@ -1,11 +0,0 @@
# Tutorials
Tutorials help new & existing users expand their abilty to use InvokeAI to the full extent of our features and services.
Currently, we have a set of tutorials available on our [YouTube channel](https://www.youtube.com/@invokeai), but as InvokeAI continues to evolve with new updates, we want to ensure that we are giving our users the resources they need to succeed.
Tutorials can be in the form of videos or article walkthroughs on a subject of your choice. We recommend focusing tutorials on the key image generation methods, or on a specific component within one of the image generation methods.
## Contributing
Please reach out to @imic or @hipsterusername on [Discord](https://discord.gg/ZmtBAhwWhy) to help create tutorials for InvokeAI.

View File

@ -1,8 +1,8 @@
--- ---
title: Textual Inversion Embeddings and LoRAs title: Concepts
--- ---
# :material-library-shelves: Textual Inversions and LoRAs # :material-library-shelves: The Hugging Face Concepts Library and Importing Textual Inversion files
With the advances in research, many new capabilities are available to customize the knowledge and understanding of novel concepts not originally contained in the base model. With the advances in research, many new capabilities are available to customize the knowledge and understanding of novel concepts not originally contained in the base model.
@ -64,25 +64,21 @@ select the embedding you'd like to use. This UI has type-ahead support, so you c
## Using LoRAs ## Using LoRAs
LoRA files are models that customize the output of Stable Diffusion LoRA files are models that customize the output of Stable Diffusion image generation.
image generation. Larger than embeddings, but much smaller than full Larger than embeddings, but much smaller than full models, they augment SD with improved
models, they augment SD with improved understanding of subjects and understanding of subjects and artistic styles.
artistic styles.
Unlike TI files, LoRAs do not introduce novel vocabulary into the Unlike TI files, LoRAs do not introduce novel vocabulary into the model's known tokens. Instead,
model's known tokens. Instead, LoRAs augment the model's weights that LoRAs augment the model's weights that are applied to generate imagery. LoRAs may be supplied
are applied to generate imagery. LoRAs may be supplied with a with a "trigger" word that they have been explicitly trained on, or may simply apply their
"trigger" word that they have been explicitly trained on, or may effect without being triggered.
simply apply their effect without being triggered.
LoRAs are typically stored in .safetensors files, which are the most LoRAs are typically stored in .safetensors files, which are the most secure way to store and transmit
secure way to store and transmit these types of weights. You may these types of weights. You may install any number of `.safetensors` LoRA files simply by copying them into
install any number of `.safetensors` LoRA files simply by copying them the `lora` directory of the corresponding InvokeAI models directory (usually `invokeai`
into the `autoimport/lora` directory of the corresponding InvokeAI models in your home directory). For example, you can simply move a Stable Diffusion 1.5 LoRA file to
directory (usually `invokeai` in your home directory). the `sd-1/lora` folder.
To use these when generating, open the LoRA menu item in the options To use these when generating, open the LoRA menu item in the options panel, select the LoRAs you want to apply
panel, select the LoRAs you want to apply and ensure that they have and ensure that they have the appropriate weight recommended by the model provider. Typically, most LoRAs perform best at a weight of .75-1.
the appropriate weight recommended by the model provider. Typically,
most LoRAs perform best at a weight of .75-1.

View File

@ -65,6 +65,7 @@ InvokeAI:
esrgan: true esrgan: true
internet_available: true internet_available: true
log_tokenization: false log_tokenization: false
nsfw_checker: false
patchmatch: true patchmatch: true
restore: true restore: true
... ...
@ -135,16 +136,19 @@ command-line options by giving the `--help` argument:
``` ```
(.venv) > invokeai-web --help (.venv) > invokeai-web --help
usage: InvokeAI [-h] [--host HOST] [--port PORT] [--allow_origins [ALLOW_ORIGINS ...]] [--allow_credentials | --no-allow_credentials] [--allow_methods [ALLOW_METHODS ...]] usage: InvokeAI [-h] [--host HOST] [--port PORT] [--allow_origins [ALLOW_ORIGINS ...]] [--allow_credentials | --no-allow_credentials]
[--allow_headers [ALLOW_HEADERS ...]] [--esrgan | --no-esrgan] [--internet_available | --no-internet_available] [--log_tokenization | --no-log_tokenization] [--allow_methods [ALLOW_METHODS ...]] [--allow_headers [ALLOW_HEADERS ...]] [--esrgan | --no-esrgan]
[--patchmatch | --no-patchmatch] [--restore | --no-restore] [--internet_available | --no-internet_available] [--log_tokenization | --no-log_tokenization]
[--always_use_cpu | --no-always_use_cpu] [--free_gpu_mem | --no-free_gpu_mem] [--max_loaded_models MAX_LOADED_MODELS] [--max_cache_size MAX_CACHE_SIZE] [--nsfw_checker | --no-nsfw_checker] [--patchmatch | --no-patchmatch] [--restore | --no-restore]
[--max_vram_cache_size MAX_VRAM_CACHE_SIZE] [--gpu_mem_reserved GPU_MEM_RESERVED] [--precision {auto,float16,float32,autocast}] [--always_use_cpu | --no-always_use_cpu] [--free_gpu_mem | --no-free_gpu_mem] [--max_cache_size MAX_CACHE_SIZE]
[--sequential_guidance | --no-sequential_guidance] [--xformers_enabled | --no-xformers_enabled] [--tiled_decode | --no-tiled_decode] [--root ROOT] [--max_vram_cache_size MAX_VRAM_CACHE_SIZE] [--precision {auto,float16,float32,autocast}]
[--autoimport_dir AUTOIMPORT_DIR] [--lora_dir LORA_DIR] [--embedding_dir EMBEDDING_DIR] [--controlnet_dir CONTROLNET_DIR] [--conf_path CONF_PATH] [--sequential_guidance | --no-sequential_guidance] [--xformers_enabled | --no-xformers_enabled]
[--models_dir MODELS_DIR] [--legacy_conf_dir LEGACY_CONF_DIR] [--db_dir DB_DIR] [--outdir OUTDIR] [--from_file FROM_FILE] [--tiled_decode | --no-tiled_decode] [--root ROOT] [--autoimport_dir AUTOIMPORT_DIR] [--lora_dir LORA_DIR]
[--use_memory_db | --no-use_memory_db] [--model MODEL] [--log_handlers [LOG_HANDLERS ...]] [--log_format {plain,color,syslog,legacy}] [--embedding_dir EMBEDDING_DIR] [--controlnet_dir CONTROLNET_DIR] [--conf_path CONF_PATH] [--models_dir MODELS_DIR]
[--log_level {debug,info,warning,error,critical}] [--version | --no-version] [--legacy_conf_dir LEGACY_CONF_DIR] [--db_dir DB_DIR] [--outdir OUTDIR] [--from_file FROM_FILE]
[--use_memory_db | --no-use_memory_db] [--model MODEL] [--log_handlers [LOG_HANDLERS ...]]
[--log_format {plain,color,syslog,legacy}] [--log_level {debug,info,warning,error,critical}]
...
``` ```
## The Configuration Settings ## The Configuration Settings
@ -174,6 +178,7 @@ These configuration settings allow you to enable and disable various InvokeAI fe
| `esrgan` | `true` | Activate the ESRGAN upscaling options| | `esrgan` | `true` | Activate the ESRGAN upscaling options|
| `internet_available` | `true` | When a resource is not available locally, try to fetch it via the internet | | `internet_available` | `true` | When a resource is not available locally, try to fetch it via the internet |
| `log_tokenization` | `false` | Before each text2image generation, print a color-coded representation of the prompt to the console; this can help understand why a prompt is not working as expected | | `log_tokenization` | `false` | Before each text2image generation, print a color-coded representation of the prompt to the console; this can help understand why a prompt is not working as expected |
| `nsfw_checker` | `true` | Activate the NSFW checker to blur out risque images |
| `patchmatch` | `true` | Activate the "patchmatch" algorithm for improved inpainting | | `patchmatch` | `true` | Activate the "patchmatch" algorithm for improved inpainting |
| `restore` | `true` | Activate the facial restoration features (DEPRECATED; restoration features will be removed in 3.0.0) | | `restore` | `true` | Activate the facial restoration features (DEPRECATED; restoration features will be removed in 3.0.0) |

View File

@ -8,64 +8,20 @@ title: ControlNet
ControlNet ControlNet
ControlNet is a powerful set of features developed by the open-source ControlNet is a powerful set of features developed by the open-source community (notably, Stanford researcher [**@ilyasviel**](https://github.com/lllyasviel)) that allows you to apply a secondary neural network model to your image generation process in Invoke.
community (notably, Stanford researcher
[**@ilyasviel**](https://github.com/lllyasviel)) that allows you to
apply a secondary neural network model to your image generation
process in Invoke.
With ControlNet, you can get more control over the output of your With ControlNet, you can get more control over the output of your image generation, providing you with a way to direct the network towards generating images that better fit your desired style or outcome.
image generation, providing you with a way to direct the network
towards generating images that better fit your desired style or
outcome.
### How it works ### How it works
ControlNet works by analyzing an input image, pre-processing that ControlNet works by analyzing an input image, pre-processing that image to identify relevant information that can be interpreted by each specific ControlNet model, and then inserting that control information into the generation process. This can be used to adjust the style, composition, or other aspects of the image to better achieve a specific result.
image to identify relevant information that can be interpreted by each
specific ControlNet model, and then inserting that control information
into the generation process. This can be used to adjust the style,
composition, or other aspects of the image to better achieve a
specific result.
### Models ### Models
InvokeAI provides access to a series of ControlNet models that provide As part of the model installation, ControlNet models can be selected including a variety of pre-trained models that have been added to achieve different effects or styles in your generated images. Further ControlNet models may require additional code functionality to also be incorporated into Invoke's Invocations folder. You should expect to follow any installation instructions for ControlNet models loaded outside the default models provided by Invoke. The default models include:
different effects or styles in your generated images. Currently
InvokeAI only supports "diffuser" style ControlNet models. These are
folders that contain the files `config.json` and/or
`diffusion_pytorch_model.safetensors` and
`diffusion_pytorch_model.fp16.safetensors`. The name of the folder is
the name of the model.
***InvokeAI does not currently support checkpoint-format
ControlNets. These come in the form of a single file with the
extension `.safetensors`.***
Diffuser-style ControlNet models are available at HuggingFace
(http://huggingface.co) and accessed via their repo IDs (identifiers
in the format "author/modelname"). The easiest way to install them is
to use the InvokeAI model installer application. Use the
`invoke.sh`/`invoke.bat` launcher to select item [5] and then navigate
to the CONTROLNETS section. Select the models you wish to install and
press "APPLY CHANGES". You may also enter additional HuggingFace
repo_ids in the "Additional models" textbox:
![Model Installer -
Controlnetl](../assets/installing-models/model-installer-controlnet.png){:width="640px"}
Command-line users can launch the model installer using the command
`invokeai-model-install`.
_Be aware that some ControlNet models require additional code
functionality in order to work properly, so just installing a
third-party ControlNet model may not have the desired effect._ Please
read and follow the documentation for installing a third party model
not currently included among InvokeAI's default list.
The models currently supported include:
**Canny**: **Canny**:

View File

@ -61,13 +61,11 @@ A noise scheduler (eg. DPM++ 2M Karras) schedules the subtraction of noise from
| ImageInverseLerp | Inverse linear interpolation of all pixels of an image | | ImageInverseLerp | Inverse linear interpolation of all pixels of an image |
| ImageLerp | Linear interpolation of all pixels of an image | | ImageLerp | Linear interpolation of all pixels of an image |
| ImageMultiply | Multiplies two images together using `PIL.ImageChops.Multiply()` | | ImageMultiply | Multiplies two images together using `PIL.ImageChops.Multiply()` |
| ImageNSFWBlurInvocation | Detects and blurs images that may contain sexually explicit content |
| ImagePaste | Pastes an image into another image | | ImagePaste | Pastes an image into another image |
| ImageProcessor | Base class for invocations that reprocess images for ControlNet | | ImageProcessor | Base class for invocations that reprocess images for ControlNet |
| ImageResize | Resizes an image to specific dimensions | | ImageResize | Resizes an image to specific dimensions |
| ImageScale | Scales an image by a factor | | ImageScale | Scales an image by a factor |
| ImageToLatents | Scales latents by a given factor | | ImageToLatents | Scales latents by a given factor |
| ImageWatermarkInvocation | Adds an invisible watermark to images |
| InfillColor | Infills transparent areas of an image with a solid color | | InfillColor | Infills transparent areas of an image with a solid color |
| InfillPatchMatch | Infills transparent areas of an image using the PatchMatch algorithm | | InfillPatchMatch | Infills transparent areas of an image using the PatchMatch algorithm |
| InfillTile | Infills transparent areas of an image with tiles of the image | | InfillTile | Infills transparent areas of an image with tiles of the image |
@ -118,49 +116,49 @@ There are several node grouping concepts that can be examined with a narrow focu
As described, an initial noise tensor is necessary for the latent diffusion process. As a result, all non-image *ToLatents nodes require a noise node input. As described, an initial noise tensor is necessary for the latent diffusion process. As a result, all non-image *ToLatents nodes require a noise node input.
![groupsnoise](../assets/nodes/groupsnoise.png) <img width="654" alt="groupsnoise" src="https://github.com/ymgenesis/InvokeAI/assets/25252829/2e8d297e-ad55-4d27-bc93-c119dad2a2c5">
### Conditioning ### Conditioning
As described, conditioning is necessary for the latent diffusion process, whether empty or not. As a result, all non-image *ToLatents nodes require positive and negative conditioning inputs. Conditioning is reliant on a CLIP tokenizer provided by the Model Loader node. As described, conditioning is necessary for the latent diffusion process, whether empty or not. As a result, all non-image *ToLatents nodes require positive and negative conditioning inputs. Conditioning is reliant on a CLIP tokenizer provided by the Model Loader node.
![groupsconditioning](../assets/nodes/groupsconditioning.png) <img width="1024" alt="groupsconditioning" src="https://github.com/ymgenesis/InvokeAI/assets/25252829/f8f7ad8a-8d9c-418e-b5ad-1437b774b27e">
### Image Space & VAE ### Image Space & VAE
The ImageToLatents node doesn't require a noise node input, but requires a VAE input to convert the image from image space into latent space. In reverse, the LatentsToImage node requires a VAE input to convert from latent space back into image space. The ImageToLatents node doesn't require a noise node input, but requires a VAE input to convert the image from image space into latent space. In reverse, the LatentsToImage node requires a VAE input to convert from latent space back into image space.
![groupsimgvae](../assets/nodes/groupsimgvae.png) <img width="637" alt="groupsimgvae" src="https://github.com/ymgenesis/InvokeAI/assets/25252829/dd99969c-e0a8-4f78-9b17-3ffe179cef9a">
### Defined & Random Seeds ### Defined & Random Seeds
It is common to want to use both the same seed (for continuity) and random seeds (for variance). To define a seed, simply enter it into the 'Seed' field on a noise node. Conversely, the RandomInt node generates a random integer between 'Low' and 'High', and can be used as input to the 'Seed' edge point on a noise node to randomize your seed. It is common to want to use both the same seed (for continuity) and random seeds (for variance). To define a seed, simply enter it into the 'Seed' field on a noise node. Conversely, the RandomInt node generates a random integer between 'Low' and 'High', and can be used as input to the 'Seed' edge point on a noise node to randomize your seed.
![groupsrandseed](../assets/nodes/groupsrandseed.png) <img width="922" alt="groupsrandseed" src="https://github.com/ymgenesis/InvokeAI/assets/25252829/af55bc20-60f6-438e-aba5-3ec871443710">
### Control ### Control
Control means to guide the diffusion process to adhere to a defined input or structure. Control can be provided as input to non-image *ToLatents nodes from ControlNet nodes. ControlNet nodes usually require an image processor which converts an input image for use with ControlNet. Control means to guide the diffusion process to adhere to a defined input or structure. Control can be provided as input to non-image *ToLatents nodes from ControlNet nodes. ControlNet nodes usually require an image processor which converts an input image for use with ControlNet.
![groupscontrol](../assets/nodes/groupscontrol.png) <img width="805" alt="groupscontrol" src="https://github.com/ymgenesis/InvokeAI/assets/25252829/cc9c5de7-23a7-46c8-bbad-1f3609d999a6">
### LoRA ### LoRA
The Lora Loader node lets you load a LoRA (say that ten times fast) and pass it as output to both the Prompt (Compel) and non-image *ToLatents nodes. A model's CLIP tokenizer is passed through the LoRA into Prompt (Compel), where it affects conditioning. A model's U-Net is also passed through the LoRA into a non-image *ToLatents node, where it affects noise prediction. The Lora Loader node lets you load a LoRA (say that ten times fast) and pass it as output to both the Prompt (Compel) and non-image *ToLatents nodes. A model's CLIP tokenizer is passed through the LoRA into Prompt (Compel), where it affects conditioning. A model's U-Net is also passed through the LoRA into a non-image *ToLatents node, where it affects noise prediction.
![groupslora](../assets/nodes/groupslora.png) <img width="993" alt="groupslora" src="https://github.com/ymgenesis/InvokeAI/assets/25252829/630962b0-d914-4505-b3ea-ccae9b0269da">
### Scaling ### Scaling
Use the ImageScale, ScaleLatents, and Upscale nodes to upscale images and/or latent images. The chosen method differs across contexts. However, be aware that latents are already noisy and compressed at their original resolution; scaling an image could produce more detailed results. Use the ImageScale, ScaleLatents, and Upscale nodes to upscale images and/or latent images. The chosen method differs across contexts. However, be aware that latents are already noisy and compressed at their original resolution; scaling an image could produce more detailed results.
![groupsallscale](../assets/nodes/groupsallscale.png) <img width="644" alt="groupsallscale" src="https://github.com/ymgenesis/InvokeAI/assets/25252829/99314f05-dd9f-4b6d-b378-31de55346a13">
### Iteration + Multiple Images as Input ### Iteration + Multiple Images as Input
Iteration is a common concept in any processing, and means to repeat a process with given input. In nodes, you're able to use the Iterate node to iterate through collections usually gathered by the Collect node. The Iterate node has many potential uses, from processing a collection of images one after another, to varying seeds across multiple image generations and more. This screenshot demonstrates how to collect several images and pass them out one at a time. Iteration is a common concept in any processing, and means to repeat a process with given input. In nodes, you're able to use the Iterate node to iterate through collections usually gathered by the Collect node. The Iterate node has many potential uses, from processing a collection of images one after another, to varying seeds across multiple image generations and more. This screenshot demonstrates how to collect several images and pass them out one at a time.
![groupsiterate](../assets/nodes/groupsiterate.png) <img width="788" alt="groupsiterate" src="https://github.com/ymgenesis/InvokeAI/assets/25252829/4af5ca27-82c9-4018-8c5b-024d3ee0a121">
### Multiple Image Generation + Random Seeds ### Multiple Image Generation + Random Seeds
@ -168,7 +166,7 @@ Multiple image generation in the node editor is done using the RandomRange node.
To control seeds across generations takes some care. The first row in the screenshot will generate multiple images with different seeds, but using the same RandomRange parameters across invocations will result in the same group of random seeds being used across the images, producing repeatable results. In the second row, adding the RandomInt node as input to RandomRange's 'Seed' edge point will ensure that seeds are varied across all images across invocations, producing varied results. To control seeds across generations takes some care. The first row in the screenshot will generate multiple images with different seeds, but using the same RandomRange parameters across invocations will result in the same group of random seeds being used across the images, producing repeatable results. In the second row, adding the RandomInt node as input to RandomRange's 'Seed' edge point will ensure that seeds are varied across all images across invocations, producing varied results.
![groupsmultigenseeding](../assets/nodes/groupsmultigenseeding.png) <img width="1027" alt="groupsmultigenseeding" src="https://github.com/ymgenesis/InvokeAI/assets/25252829/518d1b2b-fed1-416b-a052-ab06552521b3">
## Examples ## Examples
@ -176,7 +174,7 @@ With our knowledge of node grouping and the diffusion process, lets break dow
### Basic text-to-image Node Graph ### Basic text-to-image Node Graph
![nodest2i](../assets/nodes/nodest2i.png) <img width="875" alt="nodest2i" src="https://github.com/ymgenesis/InvokeAI/assets/25252829/17c67720-c376-4db8-94f0-5e00381a61ee">
- Model Loader: A necessity to generating images (as weve read above). We choose our model from the dropdown. It outputs a U-Net, CLIP tokenizer, and VAE. - Model Loader: A necessity to generating images (as weve read above). We choose our model from the dropdown. It outputs a U-Net, CLIP tokenizer, and VAE.
- Prompt (Compel): Another necessity. Two prompt nodes are created. One will output positive conditioning (what you want, dog), one will output negative (what you dont want, cat). They both input the CLIP tokenizer that the Model Loader node outputs. - Prompt (Compel): Another necessity. Two prompt nodes are created. One will output positive conditioning (what you want, dog), one will output negative (what you dont want, cat). They both input the CLIP tokenizer that the Model Loader node outputs.
@ -186,7 +184,7 @@ With our knowledge of node grouping and the diffusion process, lets break dow
### Basic image-to-image Node Graph ### Basic image-to-image Node Graph
![nodesi2i](../assets/nodes/nodesi2i.png) <img width="998" alt="nodesi2i" src="https://github.com/ymgenesis/InvokeAI/assets/25252829/3f2c95d5-cee7-4415-9b79-b46ee60a92fe">
- Model Loader: Choose a model from the dropdown. - Model Loader: Choose a model from the dropdown.
- Prompt (Compel): Two prompt nodes. One positive (dog), one negative (dog). Same CLIP inputs from the Model Loader node as before. - Prompt (Compel): Two prompt nodes. One positive (dog), one negative (dog). Same CLIP inputs from the Model Loader node as before.
@ -197,7 +195,7 @@ With our knowledge of node grouping and the diffusion process, lets break dow
### Basic ControlNet Node Graph ### Basic ControlNet Node Graph
![nodescontrol](../assets/nodes/nodescontrol.png) <img width="703" alt="nodescontrol" src="https://github.com/ymgenesis/InvokeAI/assets/25252829/b02ded86-ceb4-44a2-9910-e19ad184d471">
- Model Loader - Model Loader
- Prompt (Compel) - Prompt (Compel)

View File

@ -1,40 +1,12 @@
--- ---
title: Watermarking, NSFW Image Checking title: The NSFW Checker
--- ---
# :material-image-off: Invisible Watermark and the NSFW Checker # :material-image-off: NSFW Checker
## Watermarking
InvokeAI does not apply watermarking to images by default. However,
many computer scientists working in the field of generative AI worry
that a flood of computer-generated imagery will contaminate the image
data sets needed to train future generations of generative models.
InvokeAI offers an optional watermarking mode that writes a small bit
of text, **InvokeAI**, into each image that it generates using an
"invisible" watermarking library that spreads the information
throughout the image in a way that is not perceptible to the human
eye. If you are planning to share your generated images on
internet-accessible services, we encourage you to activate the
invisible watermark mode in order to help preserve the digital image
environment.
The downside of watermarking is that it increases the size of the
image moderately, and has been reported by some individuals to degrade
image quality. Your mileage may vary.
To read the watermark in an image, activate the InvokeAI virtual
environment (called the "developer's console" in the launcher) and run
the command:
```
invisible-watermark -a decode -t bytes -m dwtDct -l 64 /path/to/image.png
```
## The NSFW ("Safety") Checker ## The NSFW ("Safety") Checker
Stable Diffusion 1.5-based image generation models will produce sexual The Stable Diffusion image generation models will produce sexual
imagery if deliberately prompted, and will occasionally produce such imagery if deliberately prompted, and will occasionally produce such
images when this is not intended. Such images are colloquially known images when this is not intended. Such images are colloquially known
as "Not Safe for Work" (NSFW). This behavior is due to the nature of as "Not Safe for Work" (NSFW). This behavior is due to the nature of
@ -46,17 +18,35 @@ jurisdictions it may be illegal to publicly distribute such imagery,
including mounting a publicly-available server that provides including mounting a publicly-available server that provides
unfiltered images to the public. Furthermore, the [Stable Diffusion unfiltered images to the public. Furthermore, the [Stable Diffusion
weights weights
License](https://github.com/invoke-ai/InvokeAI/blob/main/LICENSE-SD1+SD2.txt), License](https://github.com/invoke-ai/InvokeAI/blob/main/LICENSE-ModelWeights.txt)
and the [Stable Diffusion XL forbids the model from being used to "exploit any of the
License][https://github.com/invoke-ai/InvokeAI/blob/main/LICENSE-SDXL.txt]
both forbid the models from being used to "exploit any of the
vulnerabilities of a specific group of persons." vulnerabilities of a specific group of persons."
For these reasons Stable Diffusion offers a "safety checker," a For these reasons Stable Diffusion offers a "safety checker," a
machine learning model trained to recognize potentially disturbing machine learning model trained to recognize potentially disturbing
imagery. When a potentially NSFW image is detected, the checker will imagery. When a potentially NSFW image is detected, the checker will
blur the image and paste a warning icon on top. The checker can be blur the image and paste a warning icon on top. The checker can be
turned on and off in the Web interface under Settings. turned on and off on the command line using `--nsfw_checker` and
`--no-nsfw_checker`.
At installation time, InvokeAI will ask whether the checker should be
activated by default (neither argument given on the command line). The
response is stored in the InvokeAI initialization file
(`invokeai.yaml` in the InvokeAI root directory). You can change the
default at any time by opening this file in a text editor and
changing the line `nsfw_checker:` from true to false or vice-versa:
```
...
Features:
esrgan: true
internet_available: true
log_tokenization: false
nsfw_checker: true
patchmatch: true
restore: true
```
## Caveats ## Caveats
@ -94,3 +84,10 @@ are encouraged to turn **off** intermediate image rendering when you
are using the checker. Future versions of InvokeAI will apply are using the checker. Future versions of InvokeAI will apply
additional blurring to intermediate images when the checker is active. additional blurring to intermediate images when the checker is active.
### Watermarking
InvokeAI does not apply any sort of watermark to images it
generates. However, it does write metadata into the PNG data area,
including the prompt used to generate the image and relevant parameter
settings. These fields can be examined using the `sd-metadata.py`
script that comes with the InvokeAI package.

View File

@ -16,24 +16,21 @@ Output Example:
--- ---
## **Invisible Watermark** ## **Seamless Tiling**
In keeping with the principles for responsible AI generation, and to The seamless tiling mode causes generated images to seamlessly tile
help AI researchers avoid synthetic images contaminating their with itself creating repetitive wallpaper-like patterns. To use it,
training sets, InvokeAI adds an invisible watermark to each of the activate the Seamless Tiling option in the Web GUI and then select
final images it generates. The watermark consists of the text whether to tile on the X (horizontal) and/or Y (vertical) axes. Tiling
"InvokeAI" and can be viewed using the will then be active for the next set of generations.
[invisible-watermarks](https://github.com/ShieldMnt/invisible-watermark)
tool.
Watermarking is controlled using the `invisible-watermark` setting in A nice prompt to test seamless tiling with is:
`invokeai.yaml`. To turn it off, add the following line under the `Features`
category.
``` ```
invisible_watermark: false pond garden with lotus by claude monet"
``` ```
---
## **Weighted Prompts** ## **Weighted Prompts**
@ -42,10 +39,34 @@ priority to them, by adding `:<percent>` to the end of the section you wish to u
example consider this prompt: example consider this prompt:
```bash ```bash
(tabby cat):0.25 (white duck):0.75 hybrid tabby cat:0.25 white duck:0.75 hybrid
``` ```
This will tell the sampler to invest 25% of its effort on the tabby cat aspect of the image and 75% This will tell the sampler to invest 25% of its effort on the tabby cat aspect of the image and 75%
on the white duck aspect (surprisingly, this example actually works). The prompt weights can use any on the white duck aspect (surprisingly, this example actually works). The prompt weights can use any
combination of integers and floating point numbers, and they do not need to add up to 1. combination of integers and floating point numbers, and they do not need to add up to 1.
## **Thresholding and Perlin Noise Initialization Options**
Under the Noise section of the Web UI, you will find two options named
Perlin Noise and Noise Threshold. [Perlin
noise](https://en.wikipedia.org/wiki/Perlin_noise) is a type of
structured noise used to simulate terrain and other natural
textures. The slider controls the percentage of perlin noise that will
be mixed into the image at the beginning of generation. Adding a little
perlin noise to a generation will alter the image substantially.
The noise threshold limits the range of the latent values during
sampling and helps combat the oversharpening seem with higher CFG
scale values.
For better intuition into what these options do in practice:
![here is a graphic demonstrating them both](../assets/truncation_comparison.jpg)
In generating this graphic, perlin noise at initialization was
programmatically varied going across on the diagram by values 0.0,
0.1, 0.2, 0.4, 0.5, 0.6, 0.8, 0.9, 1.0; and the threshold was varied
going down from 0, 1, 2, 3, 4, 5, 10, 20, 100. The other options are
fixed using the prompt "a portrait of a beautiful young lady" a CFG of
20, 100 steps, and a seed of 1950357039.

View File

@ -4,19 +4,15 @@ title: InvokeAI Web Server
# :material-web: InvokeAI Web Server # :material-web: InvokeAI Web Server
## Quick guided walkthrough of the WebUI's features As of version 2.0.0, this distribution comes with a full-featured web server
(see screenshot).
While most of the WebUI's features are intuitive, here is a guided walkthrough To use it, launch the `invoke.sh`/`invoke.bat` script and select
through its various components. option (2). Alternatively, with the InvokeAI environment active, run
the `invokeai` script by adding the `--web` option:
### Launching the WebUI
To run the InvokeAI web server, start the `invoke.sh`/`invoke.bat`
script and select option (1). Alternatively, with the InvokeAI
environment active, run `invokeai-web`:
```bash ```bash
invokeai-web invokeai --web
``` ```
You can then connect to the server by pointing your web browser at You can then connect to the server by pointing your web browser at
@ -32,32 +28,33 @@ invoke.sh --host 0.0.0.0
or or
```bash ```bash
invokeai-web --host 0.0.0.0 invokeai --web --host 0.0.0.0
``` ```
### The InvokeAI Web Interface ## Quick guided walkthrough of the WebUI's features
While most of the WebUI's features are intuitive, here is a guided walkthrough
through its various components.
![Invoke Web Server - Major Components](../assets/invoke-web-server-1.png){:width="640px"} ![Invoke Web Server - Major Components](../assets/invoke-web-server-1.png){:width="640px"}
The screenshot above shows the Text to Image tab of the WebUI. There are three The screenshot above shows the Text to Image tab of the WebUI. There are three
main sections: main sections:
1. A **control panel** on the left, which contains various settings 1. A **control panel** on the left, which contains various settings for text to
for text to image generation. The most important part is the text image generation. The most important part is the text field (currently
field (currently showing `fantasy painting, horned demon`) for showing `strawberry sushi`) for entering the text prompt, and the camera icon
entering the positive text prompt, another text field right below it for an directly underneath that will render the image. We'll call this the _Invoke_
optional negative text prompt (concepts to exclude), and a _Invoke_ button button from now on.
to begin the image rendering process.
2. The **current image** section in the middle, which shows a large 2. The **current image** section in the middle, which shows a large format
format version of the image you are currently working on. A series version of the image you are currently working on. A series of buttons at the
of buttons at the top lets you modify and manipulate the image in top ("image to image", "Use All", "Use Seed", etc) lets you modify the image
various ways. in various ways.
3. A **gallery** section on the left that contains a history of the images you 3. A \*_gallery_ section on the left that contains a history of the images you
have generated. These images are read and written to the directory specified have generated. These images are read and written to the directory specified
in the `INVOKEAIROOT/invokeai.yaml` initialization file, usually a directory at launch time in `--outdir`.
named `outputs` in `INVOKEAIROOT`.
In addition to these three elements, there are a series of icons for changing In addition to these three elements, there are a series of icons for changing
global settings, reporting bugs, and changing the theme on the upper right. global settings, reporting bugs, and changing the theme on the upper right.
@ -79,10 +76,14 @@ From top to bottom, these are:
with outpainting,and modify interior portions of the image with with outpainting,and modify interior portions of the image with
inpainting, erase portions of a starting image and have the AI fill in inpainting, erase portions of a starting image and have the AI fill in
the erased region from a text prompt. the erased region from a text prompt.
4. Node Editor - (experimental) this panel allows you to create 4. Workflow Management (not yet implemented) - this panel will allow you to create
pipelines of common operations and combine them into workflows. pipelines of common operations and combine them into workflows.
5. Model Manager - this panel allows you to import and configure new 5. Training (not yet implemented) - this panel will provide an interface to [textual
models using URLs, local paths, or HuggingFace diffusers repo_ids. inversion training](TEXTUAL_INVERSION.md) and fine tuning.
The inpainting, outpainting and postprocessing tabs are currently in
development. However, limited versions of their features can already be accessed
through the Text to Image and Image to Image tabs.
## Walkthrough ## Walkthrough
@ -91,54 +92,43 @@ feature set.
### Text to Image ### Text to Image
1. Launch the WebUI using launcher option [1] and connect to it with 1. Launch the WebUI using `python scripts/invoke.py --web` and connect to it
your browser by accessing `http://localhost:9090`. If the browser with your browser by accessing `http://localhost:9090`. If the browser and
and server are running on different machines on your LAN, add the server are running on different machines on your LAN, add the option
option `--host 0.0.0.0` to the `invoke.sh` launch command line and connect to `--host 0.0.0.0` to the launch command line and connect to the machine
the machine hosting the web server using its IP address or domain hosting the web server using its IP address or domain name.
name.
2. If all goes well, the WebUI should come up and you'll see a green dot 2. If all goes well, the WebUI should come up and you'll see a green
meaning `connected` on the upper right. `connected` message on the upper right.
![Invoke Web Server - Control Panel](../assets/invoke-control-panel-1.png){ align=right width=300px }
#### Basics #### Basics
1. Generate an image by typing _bluebird_ into the large prompt field 1. Generate an image by typing _strawberry sushi_ into the large prompt field
on the upper left and then clicking on the Invoke button or pressing on the upper left and then clicking on the Invoke button (the one with the
the return button. Camera icon). After a short wait, you'll see a large image of sushi in the
After a short wait, you'll see a large image of a bluebird in the
image panel, and a new thumbnail in the gallery on the right. image panel, and a new thumbnail in the gallery on the right.
If you need more room on the screen, you can turn the gallery off If you need more room on the screen, you can turn the gallery off by
by typing the **g** hotkey. You can turn it back on later by clicking the clicking on the **x** to the right of "Your Invocations". You can turn it
image icon that appears in the gallery's place. The list of hotkeys can back on later by clicking the image icon that appears in the gallery's
be found by clicking on the keyboard icon above the image gallery. place.
2. Generate a bunch of bluebird images by increasing the number of The images are written into the directory indicated by the `--outdir` option
requested images by adjusting the Images counter just below the Invoke provided at script launch time. By default, this is `outputs/img-samples`
under the InvokeAI directory.
2. Generate a bunch of strawberry sushi images by increasing the number of
requested images by adjusting the Images counter just below the Camera
button. As each is generated, it will be added to the gallery. You can button. As each is generated, it will be added to the gallery. You can
switch the active image by clicking on the gallery thumbnails. switch the active image by clicking on the gallery thumbnails.
If you'd like to watch the image generation progress, click the hourglass 3. Try playing with different settings, including image width and height, the
icon above the main image area. As generation progresses, you'll see Sampler, the Steps and the CFG scale.
increasingly detailed versions of the ultimate image.
3. Try playing with different settings, including changing the main
model, the image width and height, the Scheduler, the Steps and
the CFG scale.
The _Model_ changes the main model. Thousands of custom models are
now available, which generate a variety of image styles and
subjects. While InvokeAI comes with a few starter models, it is
easy to import new models into the application. See [Installing
Models](../installation/050_INSTALLING_MODELS.md) for more details.
Image _Width_ and _Height_ do what you'd expect. However, be aware that Image _Width_ and _Height_ do what you'd expect. However, be aware that
larger images consume more VRAM memory and take longer to generate. larger images consume more VRAM memory and take longer to generate.
The _Scheduler_ controls how the AI selects the image to display. Some The _Sampler_ controls how the AI selects the image to display. Some
samplers are more "creative" than others and will produce a wider range of samplers are more "creative" than others and will produce a wider range of
variations (see next section). Some samplers run faster than others. variations (see next section). Some samplers run faster than others.
@ -152,27 +142,17 @@ feature set.
to the input prompt. You can go as high or low as you like, but generally to the input prompt. You can go as high or low as you like, but generally
values greater than 20 won't improve things much, and values lower than 5 values greater than 20 won't improve things much, and values lower than 5
will produce unexpected images. There are complex interactions between will produce unexpected images. There are complex interactions between
_Steps_, _CFG Scale_ and the _Scheduler_, so experiment to find out what works _Steps_, _CFG Scale_ and the _Sampler_, so experiment to find out what works
for you. for you.
The _Seed_ controls the series of values returned by InvokeAI's 4. To regenerate a previously-generated image, select the image you want and
random number generator. Each unique seed value will generate a different click _Use All_. This loads the text prompt and other original settings into
image. To regenerate a previous image, simply use the original image's the control panel. If you then press _Invoke_ it will regenerate the image
seed value. A slider to the right of the _Seed_ field will change the exactly. You can also selectively modify the prompt or other settings to
seed each time an image is generated. tweak the image.
![Invoke Web Server - Control Panel 2](../assets/control-panel-2.png){ align=right width=400px } Alternatively, you may click on _Use Seed_ to load just the image's seed,
and leave other settings unchanged.
4. To regenerate a previously-generated image, select the image you
want and click the asterisk ("*") button at the top of the
image. This loads the text prompt and other original settings into
the control panel. If you then press _Invoke_ it will regenerate
the image exactly. You can also selectively modify the prompt or
other settings to tweak the image.
Alternatively, you may click on the "sprouting plant icon" to load
just the image's seed, and leave other settings unchanged or the
quote icon to load just the positive and negative prompts.
5. To regenerate a Stable Diffusion image that was generated by another SD 5. To regenerate a Stable Diffusion image that was generated by another SD
package, you need to know its text prompt and its _Seed_. Copy-paste the package, you need to know its text prompt and its _Seed_. Copy-paste the
@ -182,21 +162,61 @@ feature set.
not be exact unless you also set the correct values for the original not be exact unless you also set the correct values for the original
sampler, CFG, steps and dimensions, but it will (usually) be close. sampler, CFG, steps and dimensions, but it will (usually) be close.
6. To save an image, right click on it to bring up a menu that will #### Variations on a theme
let you download the image, save it to a named image gallery, and
copy it to the clipboard, among other things.
#### Upscaling 1. Let's try generating some variations. Select your favorite sushi image from
the gallery to load it. Then select "Use All" from the list of buttons
above. This will load up all the settings used to generate this image,
including its unique seed.
![Invoke Web Server - Upscaling](../assets/upscaling.png){ align=right width=400px } Go down to the Variations section of the Control Panel and set the button to
On. Set Variation Amount to 0.2 to generate a modest number of variations on
the image, and also set the Image counter to `4`. Press the `invoke` button.
This will generate a series of related images. To obtain smaller variations,
just lower the Variation Amount. You may also experiment with changing the
Sampler. Some samplers generate more variability than others. _k_euler_a_ is
particularly creative, while _ddim_ is pretty conservative.
"Upscaling" is the process of increasing the size of an image while 2. For even more variations, experiment with increasing the setting for
retaining the sharpness. InvokeAI uses an external library called _Perlin_. This adds a bit of noise to the image generation process. Note
"ESRGAN" to do this. To invoke upscaling, simply select an image that values of Perlin noise greater than 0.15 produce poor images for
and press the "expanding arrows" button above it. You can select several of the samplers.
between 2X and 4X upscaling, and adjust the upscaling strength,
which has much the same meaning as in facial reconstruction. Try #### Facial reconstruction and upscaling
running this on one of your previously-generated images.
Stable Diffusion frequently produces mangled faces, particularly when there are
multiple figures in the same scene. Stable Diffusion has particular issues with
generating reallistic eyes. InvokeAI provides the ability to reconstruct faces
using either the GFPGAN or CodeFormer libraries. For more information see
[POSTPROCESS](POSTPROCESS.md).
1. Invoke a prompt that generates a mangled face. A prompt that often gives
this is "portrait of a lawyer, 3/4 shot" (this is not intended as a slur
against lawyers!) Once you have an image that needs some touching up, load
it into the Image panel, and press the button with the face icon
(highlighted in the first screenshot below). A dialog box will appear. Leave
_Strength_ at 0.8 and press \*Restore Faces". If all goes well, the eyes and
other aspects of the face will be improved (see the second screenshot)
![Invoke Web Server - Original Image](../assets/invoke-web-server-3.png)
![Invoke Web Server - Retouched Image](../assets/invoke-web-server-4.png)
The facial reconstruction _Strength_ field adjusts how aggressively the face
library will try to alter the face. It can be as high as 1.0, but be aware
that this often softens the face airbrush style, losing some details. The
default 0.8 is usually sufficient.
2. "Upscaling" is the process of increasing the size of an image while
retaining the sharpness. InvokeAI uses an external library called "ESRGAN"
to do this. To invoke upscaling, simply select an image and press the _HD_
button above it. You can select between 2X and 4X upscaling, and adjust the
upscaling strength, which has much the same meaning as in facial
reconstruction. Try running this on one of your previously-generated images.
3. Finally, you can run facial reconstruction and/or upscaling automatically
after each Invocation. Go to the Advanced Options section of the Control
Panel and turn on _Restore Face_ and/or _Upscale_.
### Image to Image ### Image to Image
@ -204,14 +224,24 @@ InvokeAI lets you take an existing image and use it as the basis for a new
creation. You can use any sort of image, including a photograph, a scanned creation. You can use any sort of image, including a photograph, a scanned
sketch, or a digital drawing, as long as it is in PNG or JPEG format. sketch, or a digital drawing, as long as it is in PNG or JPEG format.
For this tutorial, we'll use the file named For this tutorial, we'll use files named
[Lincoln-and-Parrot-512.png](../assets/Lincoln-and-Parrot-512.png). [Lincoln-and-Parrot-512.png](../assets/Lincoln-and-Parrot-512.png), and
[Lincoln-and-Parrot-512-transparent.png](../assets/Lincoln-and-Parrot-512-transparent.png).
Download these images to your local machine now to continue with the
walkthrough.
1. Click on the _Image to Image_ tab icon, which is the second icon 1. Click on the _Image to Image_ tab icon, which is the second icon from the
from the top on the left-hand side of the screen. This will bring top on the left-hand side of the screen:
you to a screen similar to the one shown here:
![Invoke Web Server - Image to Image Tab](../assets/invoke-web-server-6.png){ width="640px" } <figure markdown>
![Invoke Web Server - Image to Image Icon](../assets/invoke-web-server-5.png)
</figure>
This will bring you to a screen similar to the one shown here:
<figure markdown>
![Invoke Web Server - Image to Image Tab](../assets/invoke-web-server-6.png){:width="640px"}
</figure>
2. Drag-and-drop the Lincoln-and-Parrot image into the Image panel, or click 2. Drag-and-drop the Lincoln-and-Parrot image into the Image panel, or click
the blank area to get an upload dialog. The image will load into an area the blank area to get an upload dialog. The image will load into an area
@ -225,99 +255,120 @@ For this tutorial, we'll use the file named
![Invoke Web Server - Image to Image example](../assets/invoke-web-server-7.png){:width="640px"} ![Invoke Web Server - Image to Image example](../assets/invoke-web-server-7.png){:width="640px"}
4. Experiment with the different settings. The most influential one in Image to 4. Experiment with the different settings. The most influential one in Image to
Image is _Denoising Strength_ located about midway down the control Image is _Image to Image Strength_ located about midway down the control
panel. By default it is set to 0.75, but can range from 0.0 to 0.99. The panel. By default it is set to 0.75, but can range from 0.0 to 0.99. The
higher the value, the more of the original image the AI will replace. A higher the value, the more of the original image the AI will replace. A
value of 0 will leave the initial image completely unchanged, while 0.99 value of 0 will leave the initial image completely unchanged, while 0.99
will replace it completely. However, the _Scheduler_ and _CFG Scale_ also will replace it completely. However, the Sampler and CFG Scale also
influence the final result. You can also generate variations in the same way influence the final result. You can also generate variations in the same way
as described in Text to Image. as described in Text to Image.
5. What if we only want to change certain part(s) of the image and 5. What if we only want to change certain part(s) of the image and leave the
leave the rest intact? This is called Inpainting, and you can do rest intact? This is called Inpainting, and a future version of the InvokeAI
it in the [Unified Canvas](UNIFIED_CANVAS.md). The Unified Canvas web server will provide an interactive painting canvas on which you can
also allows you to extend borders of the image and fill in the directly draw the areas you wish to Inpaint into. For now, you can achieve
blank areas, a process called outpainting. this effect by using an external photoeditor tool to make one or more
regions of the image transparent as described in [INPAINTING.md] and
uploading that.
The file
[Lincoln-and-Parrot-512-transparent.png](../assets/Lincoln-and-Parrot-512-transparent.png)
is a version of the earlier image in which the area around the parrot has
been replaced with transparency. Click on the "x" in the upper right of the
Initial Image and upload the transparent version. Using the same prompt "old
sea captain with raven on shoulder" try Invoking an image. This time, only
the parrot will be replaced, leaving the rest of the original image intact:
<figure markdown>
![Invoke Web Server - Inpainting](../assets/invoke-web-server-8.png){:width="640px"}
</figure>
6. Would you like to modify a previously-generated image using the Image to 6. Would you like to modify a previously-generated image using the Image to
Image facility? Easy! While in the Image to Image panel, drag and drop any Image facility? Easy! While in the Image to Image panel, hover over any of
image in the gallery into the Initial Image area, and it will be ready for the gallery images to see a little menu of icons pop up. Click the picture
use. You can do the same thing with the main image display. Click on the icon to instantly send the selected image to Image to Image as the initial
_Send to_ icon to get a menu of image.
commands and choose "Send to Image to Image".
![Send To Icon](../assets/send-to-icon.png) You can do the same from the Text to Image tab by clicking on the picture icon
above the central image panel. The screenshot below shows where the "use as
initial image" icons are located.
### Textual Inversion, LoRA and ControlNet ![Invoke Web Server - Use as Image Links](../assets/invoke-web-server-9.png){:width="640px"}
InvokeAI supports several different types of model files that ### Unified Canvas
extending the capabilities of the main model by adding artistic
styles, special effects, or subjects. By mixing and matching textual
inversion, LoRA and ControlNet models, you can achieve many
interesting and beautiful effects.
We will give an example using a LoRA model named "Ink Scenery". This See the [Unified Canvas Guide](UNIFIED_CANVAS.md)
LoRA, which can be downloaded from Civitai (civitai.com), is
specialized to paint landscapes that look like they were made with
dripping india ink. To install this LoRA, we first download it and
put it into the `autoimport/lora` folder located inside the
`invokeai` root directory. After restarting the web server, the
LoRA will now become available for use.
To see this LoRA at work, we'll first generate an image without it ## Reference
using the standard `stable-diffusion-v1-5` model. Choose this
model and enter the prompt "mountains, ink". Here is a typical
generated image, a mountain range rendered in ink and watercolor
wash:
![Ink Scenery without LoRA](../assets/lora-example-0.png){ width=512px } ### Additional Options
Now let's install and activate the Ink Scenery LoRA. Go to | parameter <img width=160 align="right"> | effect |
https://civitai.com/models/78605/ink-scenery-or and download the LoRA | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
model file to `invokeai/autoimport/lora` and restart the web | `--web_develop` | Starts the web server in development mode. |
server. (Alternatively, you can use [InvokeAI's Web Model | `--web_verbose` | Enables verbose logging |
Manager](../installation/050_INSTALLING_MODELS.md) to download and | `--cors [CORS ...]` | Additional allowed origins, comma-separated |
install the LoRA directly by typing its URL into the _Import | `--host HOST` | Web server: Host or IP to listen on. Set to 0.0.0.0 to accept traffic from other devices on your network. |
Models_->_Location_ field). | `--port PORT` | Web server: Port to listen on |
| `--certfile CERTFILE` | Web server: Path to certificate file to use for SSL. Use together with --keyfile |
| `--keyfile KEYFILE` | Web server: Path to private key file to use for SSL. Use together with --certfile' |
| `--gui` | Start InvokeAI GUI - This is the "desktop mode" version of the web app. It uses Flask to create a desktop app experience of the webserver. |
Scroll down the control panel until you get to the LoRA accordion ### Web Specific Features
section, and open it:
![LoRA Section](../assets/lora-example-1.png){ width=512px } The web experience offers an incredibly easy-to-use experience for interacting
with the InvokeAI toolkit. For detailed guidance on individual features, see the
Feature-specific help documents available in this directory. Note that the
latest functionality available in the CLI may not always be available in the Web
interface.
Click the popup menu and select "Ink scenery". (If it isn't there, then #### Dark Mode & Light Mode
the model wasn't installed to the right place, or perhaps you forgot
to restart the web server.) The LoRA section will change to look like this:
![LoRA Section Loaded](../assets/lora-example-2.png){ width=512px } The InvokeAI interface is available in a nano-carbon black & purple Dark Mode,
and a "burn your eyes out Nosferatu" Light Mode. These can be toggled by
clicking the Sun/Moon icons at the top right of the interface.
Note that there is now a slider control for _Ink scenery_. The slider ![InvokeAI Web Server - Dark Mode](../assets/invoke_web_dark.png)
controls how much influence the LoRA model will have on the generated
image.
Run the "mountains, ink" prompt again and observe the change in style: ![InvokeAI Web Server - Light Mode](../assets/invoke_web_light.png)
![Ink Scenery](../assets/lora-example-3.png){ width=512px } #### Invocation Toolbar
Try adjusting the weight slider for larger and smaller weights and The left side of the InvokeAI interface is available for customizing the prompt
generate the image after each adjustment. The higher the weight, the and the settings used for invoking your new image. Typing your prompt into the
more influence the LoRA will have. open text field and clicking the Invoke button will produce the image based on
the settings configured in the toolbar.
To remove the LoRA completely, just click on its trash can icon. See below for additional documentation related to each feature:
Multiple LoRAs can be added simultaneously and combined with textual - [Variations](./VARIATIONS.md)
inversions and ControlNet models. Please see [Textual Inversions and - [Upscaling](./POSTPROCESS.md#upscaling)
LoRAs](CONCEPTS.md) and [Using ControlNet](CONTROLNET.md) for details. - [Image to Image](./IMG2IMG.md)
- [Other](./OTHER.md)
## Summary #### Invocation Gallery
This walkthrough just skims the surface of the many things InvokeAI The currently selected --outdir (or the default outputs folder) will display all
can do. Please see [Features](index.md) for more detailed reference previously generated files on load. As new invocations are generated, these will
guides. be dynamically added to the gallery, and can be previewed by selecting them.
Each image also has a simple set of actions (e.g., Delete, Use Seed, Use All
Parameters, etc.) that can be accessed by hovering over the image.
#### Image Workspace
When an image from the Invocation Gallery is selected, or is generated, the
image will be displayed within the center of the interface. A quickbar of common
image interactions are displayed along the top of the image, including:
- Use image in the `Image to Image` workflow
- Initialize Face Restoration on the selected file
- Initialize Upscaling on the selected file
- View File metadata and details
- Delete the file
## Acknowledgements ## Acknowledgements
A huge shout-out to the core team working to make the Web GUI a reality, A huge shout-out to the core team working to make this vision a reality,
including [psychedelicious](https://github.com/psychedelicious), including [psychedelicious](https://github.com/psychedelicious),
[Kyle0654](https://github.com/Kyle0654) and [Kyle0654](https://github.com/Kyle0654) and
[blessedcoolant](https://github.com/blessedcoolant). [blessedcoolant](https://github.com/blessedcoolant).

View File

@ -4,9 +4,6 @@ title: Overview
Here you can find the documentation for InvokeAI's various features. Here you can find the documentation for InvokeAI's various features.
## The [Getting Started Guide](../help/gettingStartedWithAI)
A getting started guide for those new to AI image generation.
## The Basics ## The Basics
### * The [Web User Interface](WEB.md) ### * The [Web User Interface](WEB.md)
Guide to the Web interface. Also see the [WebUI Hotkeys Reference Guide](WEBUIHOTKEYS.md) Guide to the Web interface. Also see the [WebUI Hotkeys Reference Guide](WEBUIHOTKEYS.md)
@ -20,12 +17,8 @@ a single convenient digital artist-optimized user interface.
### * [Prompt Engineering](PROMPTS.md) ### * [Prompt Engineering](PROMPTS.md)
Get the images you want with the InvokeAI prompt engineering language. Get the images you want with the InvokeAI prompt engineering language.
### * The [LoRA, LyCORIS and Textual Inversion Models](CONCEPTS.md) ## * The [Concepts Library](CONCEPTS.md)
Add custom subjects and styles using a variety of fine-tuned models. Add custom subjects and styles using HuggingFace's repository of embeddings.
### * [ControlNet](CONTROLNET.md)
Learn how to install and use ControlNet models for fine control over
image output.
### * [Image-to-Image Guide](IMG2IMG.md) ### * [Image-to-Image Guide](IMG2IMG.md)
Use a seed image to build new creations in the CLI. Use a seed image to build new creations in the CLI.
@ -36,28 +29,26 @@ are the ticket.
## Model Management ## Model Management
### * [Model Installation](../installation/050_INSTALLING_MODELS.md) ## * [Model Installation](../installation/050_INSTALLING_MODELS.md)
Learn how to import third-party models and switch among them. This Learn how to import third-party models and switch among them. This
guide also covers optimizing models to load quickly. guide also covers optimizing models to load quickly.
### * [Merging Models](MODEL_MERGING.md) ## * [Merging Models](MODEL_MERGING.md)
Teach an old model new tricks. Merge 2-3 models together to create a Teach an old model new tricks. Merge 2-3 models together to create a
new model that combines characteristics of the originals. new model that combines characteristics of the originals.
### * [Textual Inversion](TRAINING.md) ## * [Textual Inversion](TEXTUAL_INVERSION.md)
Personalize models by adding your own style or subjects. Personalize models by adding your own style or subjects.
## Other Features # Other Features
### * [The NSFW Checker](WATERMARK+NSFW.md) ## * [The NSFW Checker](NSFW.md)
Prevent InvokeAI from displaying unwanted racy images. Prevent InvokeAI from displaying unwanted racy images.
### * [Controlling Logging](LOGGING.md) ## * [Controlling Logging](LOGGING.md)
Control how InvokeAI logs status messages. Control how InvokeAI logs status messages.
<!-- OUT OF DATE ## * [Miscellaneous](OTHER.md)
### * [Miscellaneous](OTHER.md)
Run InvokeAI on Google Colab, generate images with repeating patterns, Run InvokeAI on Google Colab, generate images with repeating patterns,
batch process a file of prompts, increase the "creativity" of image batch process a file of prompts, increase the "creativity" of image
generation by adding initial noise, and more! generation by adding initial noise, and more!
-->

View File

@ -1,95 +0,0 @@
# Getting Started with AI Image Generation
New to image generation with AI? Youre in the right place!
This is a high level walkthrough of some of the concepts and terms youll see as you start using InvokeAI. Please note, this is not an exhaustive guide and may be out of date due to the rapidly changing nature of the space.
## Using InvokeAI
### **Prompt Crafting**
- Prompts are the basis of using InvokeAI, providing the models directions on what to generate. As a general rule of thumb, the more detailed your prompt is, the better your result will be.
*To get started, heres an easy template to use for structuring your prompts:*
- Subject, Style, Quality, Aesthetic
- **Subject:** What your image will be about. E.g. “a futuristic city with trains”, “penguins floating on icebergs”, “friends sharing beers”
- **Style:** The style or medium in which your image will be in. E.g. “photograph”, “pencil sketch”, “oil paints”, or “pop art”, “cubism”, “abstract”
- **Quality:** A particular aspect or trait that you would like to see emphasized in your image. E.g. "award-winning", "featured in {relevant set of high quality works}", "professionally acclaimed". Many people often use "masterpiece".
- **Aesthetics:** The visual impact and design of the artwork. This can be colors, mood, lighting, setting, etc.
- There are two prompt boxes: *Positive Prompt* & *Negative Prompt*.
- A **Positive** Prompt includes words you want the model to reference when creating an image.
- Negative Prompt is for anything you want the model to eliminate when creating an image. It doesnt always interpret things exactly the way you would, but helps control the generation process. Always try to include a few terms - you can typically use lower quality image terms like “blurry” or “distorted” with good success.
- Some examples prompts you can try on your own:
- A detailed oil painting of a tranquil forest at sunset with vibrant+ colors and soft, golden light filtering through the trees
- friends sharing beers in a busy city, realistic colored pencil sketch, twilight, masterpiece, bright, lively
### Generation Workflows
- Invoke offers a number of different workflows for interacting with models to produce images. Each is extremely powerful on its own, but together provide you an unparalleled way of producing high quality creative outputs that align with your vision.
- **Text to Image:** The text to image tab focuses on the key workflow of using a prompt to generate a new image. It includes other features that help control the generation process as well.
- **Image to Image:** With image to image, you provide an image as a reference (called the “initial image”), which provides more guidance around color and structure to the AI as it generates a new image. This is provided alongside the same features as Text to Image.
- **Unified Canvas:** The Unified Canvas is an advanced AI-first image editing tool that is easy to use, but hard to master. Drag an image onto the canvas from your gallery in order to regenerate certain elements, edit content or colors (known as inpainting), or extend the image with an exceptional degree of consistency and clarity (called outpainting).
### Improving Image Quality
- Fine tuning your prompt - the more specific you are, the closer the image will turn out to what is in your head! Adding more details in the Positive Prompt or Negative Prompt can help add / remove pieces of your image to improve it - You can also use advanced techniques like upweighting and downweighting to control the influence of certain words. [Learn more here](https://invoke-ai.github.io/InvokeAI/features/PROMPTS/#prompt-syntax-features).
- **Tip: If youre seeing poor results, try adding the things you dont like about the image to your negative prompt may help. E.g. distorted, low quality, unrealistic, etc.**
- Explore different models - Other models can produce different results due to the data theyve been trained on. Each model has specific language and settings it works best with; a models documentation is your friend here. Play around with some and see what works best for you!
- Increasing Steps - The number of steps used controls how much time the model is given to produce an image, and depends on the “Scheduler” used. The schedule controls how each step is processed by the model. More steps tends to mean better results, but will take longer - We recommend at least 30 steps for most
- Tweak and Iterate - Remember, its best to change one thing at a time so you know what is working and what isn't. Sometimes you just need to try a new image, and other times using a new prompt might be the ticket. For testing, consider turning off the “random” Seed - Using the same seed with the same settings will produce the same image, which makes it the perfect way to learn exactly what your changes are doing.
- Explore Advanced Settings - InvokeAI has a full suite of tools available to allow you complete control over your image creation process - Check out our [docs if you want to learn more](https://invoke-ai.github.io/InvokeAI/features/).
## Terms & Concepts
If you're interested in learning more, check out [this presentation](https://docs.google.com/presentation/d/1IO78i8oEXFTZ5peuHHYkVF-Y3e2M6iM5tCnc-YBfcCM/edit?usp=sharing) from one of our maintainers (@lstein).
### Stable Diffusion
Stable Diffusion is deep learning, text-to-image model that is the foundation of the capabilities found in InvokeAI. Since the release of Stable Diffusion, there have been many subsequent models created based on Stable Diffusion that are designed to generate specific types of images.
### Prompts
Prompts provide the models directions on what to generate. As a general rule of thumb, the more detailed your prompt is, the better your result will be.
### Models
Models are the magic that power InvokeAI. These files represent the output of training a machine on understanding massive amounts of images - providing them with the capability to generate new images using just a text description of what youd like to see. (Like Stable Diffusion!)
Invoke offers a simple way to download several different models upon installation, but many more can be discovered online, including at ****. Each model can produce a unique style of output, based on the images it was trained on - Try out different models to see which best fits your creative vision!
- *Models that contain “inpainting” in the name are designed for use with the inpainting feature of the Unified Canvas*
### Scheduler
Schedulers guide the process of removing noise (de-noising) from data. They determine:
1. The number of steps to take to remove the noise.
2. Whether the steps are random (stochastic) or predictable (deterministic).
3. The specific method (algorithm) used for de-noising.
Experimenting with different schedulers is recommended as each will produce different outputs!
### Steps
The number of de-noising steps each generation through.
Schedulers can be intricate and there's often a balance to strike between how quickly they can de-noise data and how well they can do it. It's typically advised to experiment with different schedulers to see which one gives the best results. There has been a lot written on the internet about different schedulers, as well as exploring what the right level of "steps" are for each. You can save generation time by reducing the number of steps used, but you'll want to make sure that you are satisfied with the quality of images produced!
### Low-Rank Adaptations / LoRAs
Low-Rank Adaptations (LoRAs) are like a smaller, more focused version of models, intended to focus on training a better understanding of how a specific character, style, or concept looks.
### Textual Inversion Embeddings
Textual Inversion Embeddings, like LoRAs, assist with more easily prompting for certain characters, styles, or concepts. However, embeddings are trained to update the relationship between a specific word (known as the “trigger”) and the intended output.
### ControlNet
ControlNets are neural network models that are able to extract key features from an existing image and use these features to guide the output of the image generation model.
### VAE
Variational auto-encoder (VAE) is a encode/decode model that translates the "latents" image produced during the image generation procees to the large pixel images that we see.

View File

@ -11,33 +11,6 @@ title: Home
``` ```
--> -->
<!-- CSS styling -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.2.1/css/fontawesome.min.css">
<style>
.button {
width: 300px;
height: 50px;
background-color: #448AFF;
color: #fff;
font-size: 16px;
border: none;
cursor: pointer;
border-radius: 0.2rem;
}
.button-container {
display: grid;
grid-template-columns: repeat(3, 300px);
gap: 20px;
}
.button:hover {
background-color: #526CFE;
}
</style>
<div align="center" markdown> <div align="center" markdown>
@ -51,7 +24,7 @@ title: Home
[![CI checks on main badge]][ci checks on main link] [![CI checks on main badge]][ci checks on main link]
[![CI checks on dev badge]][ci checks on dev link] [![CI checks on dev badge]][ci checks on dev link]
<!-- [![latest commit to dev badge]][latest commit to dev link] --> [![latest commit to dev badge]][latest commit to dev link]
[![github open issues badge]][github open issues link] [![github open issues badge]][github open issues link]
[![github open prs badge]][github open prs link] [![github open prs badge]][github open prs link]
@ -81,10 +54,10 @@ title: Home
[github stars badge]: [github stars badge]:
https://flat.badgen.net/github/stars/invoke-ai/InvokeAI?icon=github https://flat.badgen.net/github/stars/invoke-ai/InvokeAI?icon=github
[github stars link]: https://github.com/invoke-ai/InvokeAI/stargazers [github stars link]: https://github.com/invoke-ai/InvokeAI/stargazers
<!-- [latest commit to dev badge]: [latest commit to dev badge]:
https://flat.badgen.net/github/last-commit/invoke-ai/InvokeAI/development?icon=github&color=yellow&label=last%20dev%20commit&cache=900 https://flat.badgen.net/github/last-commit/invoke-ai/InvokeAI/development?icon=github&color=yellow&label=last%20dev%20commit&cache=900
[latest commit to dev link]: [latest commit to dev link]:
https://github.com/invoke-ai/InvokeAI/commits/main --> https://github.com/invoke-ai/InvokeAI/commits/development
[latest release badge]: [latest release badge]:
https://flat.badgen.net/github/release/invoke-ai/InvokeAI/development?icon=github https://flat.badgen.net/github/release/invoke-ai/InvokeAI/development?icon=github
[latest release link]: https://github.com/invoke-ai/InvokeAI/releases [latest release link]: https://github.com/invoke-ai/InvokeAI/releases
@ -97,24 +70,61 @@ image-to-image generator. It provides a streamlined process with various new
features and options to aid the image generation process. It runs on Windows, features and options to aid the image generation process. It runs on Windows,
Mac and Linux machines, and runs on GPU cards with as little as 4 GB of RAM. Mac and Linux machines, and runs on GPU cards with as little as 4 GB of RAM.
**Quick links**: [<a href="https://discord.gg/ZmtBAhwWhy">Discord Server</a>]
[<a href="https://github.com/invoke-ai/InvokeAI/">Code and Downloads</a>] [<a
href="https://github.com/invoke-ai/InvokeAI/issues">Bug Reports</a>] [<a
href="https://github.com/invoke-ai/InvokeAI/discussions">Discussion, Ideas &
Q&A</a>]
<div align="center"><img src="assets/invoke-web-server-1.png" width=640></div> <div align="center"><img src="assets/invoke-web-server-1.png" width=640></div>
!!! Note !!! note
This project is rapidly evolving. Please use the [Issues tab](https://github.com/invoke-ai/InvokeAI/issues) to report bugs and make feature requests. Be sure to use the provided templates as it will help aid response time. This fork is rapidly evolving. Please use the [Issues tab](https://github.com/invoke-ai/InvokeAI/issues) to report bugs and make feature requests. Be sure to use the provided templates. They will help aid diagnose issues faster.
## :octicons-link-24: Quick Links ## :fontawesome-solid-computer: Hardware Requirements
<div class="button-container"> ### :octicons-cpu-24: System
<a href="installation/INSTALLATION"> <button class="button">Installation</button> </a>
<a href="features/"> <button class="button">Features</button> </a>
<a href="help/gettingStartedWithAI/"> <button class="button">Getting Started</button> </a>
<a href="contributing/CONTRIBUTING/"> <button class="button">Contributing</button> </a>
<a href="https://github.com/invoke-ai/InvokeAI/"> <button class="button">Code and Downloads</button> </a>
<a href="https://github.com/invoke-ai/InvokeAI/issues"> <button class="button">Bug Reports </button> </a>
<a href="https://discord.gg/ZmtBAhwWhy"> <button class="button"> Join the Discord Server!</button> </a>
</div>
You wil need one of the following:
- :simple-nvidia: An NVIDIA-based graphics card with 4 GB or more VRAM memory.
- :simple-amd: An AMD-based graphics card with 4 GB or more VRAM memory (Linux
only)
- :fontawesome-brands-apple: An Apple computer with an M1 chip.
We do **not recommend** the following video cards due to issues with their
running in half-precision mode and having insufficient VRAM to render 512x512
images in full-precision mode:
- NVIDIA 10xx series cards such as the 1080ti
- GTX 1650 series cards
- GTX 1660 series cards
### :fontawesome-solid-memory: Memory and Disk
- At least 12 GB Main Memory RAM.
- At least 18 GB of free disk space for the machine learning model, Python, and
all its dependencies.
## :octicons-package-dependencies-24: Installation
This fork is supported across Linux, Windows and Macintosh. Linux users can use
either an Nvidia-based card (with CUDA support) or an AMD card (using the ROCm
driver).
### [Installation Getting Started Guide](installation)
#### [Automated Installer](installation/010_INSTALL_AUTOMATED.md)
This method is recommended for 1st time users
#### [Manual Installation](installation/020_INSTALL_MANUAL.md)
This method is recommended for experienced users and developers
#### [Docker Installation](installation/040_INSTALL_DOCKER.md)
This method is recommended for those familiar with running Docker containers
### Other Installation Guides
- [PyPatchMatch](installation/060_INSTALL_PATCHMATCH.md)
- [XFormers](installation/070_INSTALL_XFORMERS.md)
- [CUDA and ROCm Drivers](installation/030_INSTALL_CUDA_AND_ROCM.md)
- [Installing New Models](installation/050_INSTALLING_MODELS.md)
## :octicons-gift-24: InvokeAI Features ## :octicons-gift-24: InvokeAI Features
@ -135,9 +145,9 @@ Mac and Linux machines, and runs on GPU cards with as little as 4 GB of RAM.
### Model Management ### Model Management
- [Installing](installation/050_INSTALLING_MODELS.md) - [Installing](installation/050_INSTALLING_MODELS.md)
- [Model Merging](features/MODEL_MERGING.md) - [Model Merging](features/MODEL_MERGING.md)
- [ControlNet Models](features/CONTROLNET.md)
- [Style/Subject Concepts and Embeddings](features/CONCEPTS.md) - [Style/Subject Concepts and Embeddings](features/CONCEPTS.md)
- [Watermarking and the Not Safe for Work (NSFW) Checker](features/WATERMARK+NSFW.md) - [Textual Inversion](features/TEXTUAL_INVERSION.md)
- [Not Safe for Work (NSFW) Checker](features/NSFW.md)
<!-- seperator --> <!-- seperator -->
### Prompt Engineering ### Prompt Engineering
- [Prompt Syntax](features/PROMPTS.md) - [Prompt Syntax](features/PROMPTS.md)
@ -212,14 +222,18 @@ get solutions for common installation problems and other issues.
Anyone who wishes to contribute to this project, whether documentation, Anyone who wishes to contribute to this project, whether documentation,
features, bug fixes, code cleanup, testing, or code reviews, is very much features, bug fixes, code cleanup, testing, or code reviews, is very much
encouraged to do so. encouraged to do so. If you are unfamiliar with how to contribute to GitHub
projects, here is a
[Getting Started Guide](https://opensource.com/article/19/7/create-pull-request-github).
[Please take a look at our Contribution documentation to learn more about contributing to InvokeAI. A full set of contribution guidelines, along with templates, are in progress,
](contributing/CONTRIBUTING.md) but for now the most important thing is to **make your pull request against the
"development" branch**, and not against "main". This will help keep public
breakage to a minimum and will allow you to propose more radical changes.
## :octicons-person-24: Contributors ## :octicons-person-24: Contributors
This software is a combined effort of various people from across the world. This fork is a combined effort of various people from across the world.
[Check out the list of all these amazing people](other/CONTRIBUTORS.md). We [Check out the list of all these amazing people](other/CONTRIBUTORS.md). We
thank them for their time, hard work and effort. thank them for their time, hard work and effort.

View File

@ -40,8 +40,10 @@ experimental versions later.
this, open up a command-line window ("Terminal" on Linux and this, open up a command-line window ("Terminal" on Linux and
Macintosh, "Command" or "Powershell" on Windows) and type `python Macintosh, "Command" or "Powershell" on Windows) and type `python
--version`. If Python is installed, it will print out the version --version`. If Python is installed, it will print out the version
number. If it is version `3.9.*`, `3.10.*` or `3.11.*` you meet number. If it is version `3.9.*` or `3.10.*`, you meet
requirements. requirements. We do not recommend using Python 3.11 or higher,
as not all the libraries that InvokeAI depends on work properly
with this version.
!!! warning "What to do if you have an unsupported version" !!! warning "What to do if you have an unsupported version"
@ -122,9 +124,9 @@ experimental versions later.
[latest release](https://github.com/invoke-ai/InvokeAI/releases/latest), [latest release](https://github.com/invoke-ai/InvokeAI/releases/latest),
and look for a file named: and look for a file named:
- InvokeAI-installer-v3.X.X.zip - InvokeAI-installer-v2.X.X.zip
where "3.X.X" is the latest released version. The file is located where "2.X.X" is the latest released version. The file is located
at the very bottom of the release page, under **Assets**. at the very bottom of the release page, under **Assets**.
4. **Unpack the installer**: Unpack the zip file into a convenient directory. This will create a new 4. **Unpack the installer**: Unpack the zip file into a convenient directory. This will create a new
@ -213,6 +215,17 @@ experimental versions later.
Generally the defaults are fine, and you can come back to this screen at Generally the defaults are fine, and you can come back to this screen at
any time to tweak your system. Here are the options you can adjust: any time to tweak your system. Here are the options you can adjust:
- ***Output directory for images***
This is the path to a directory in which InvokeAI will store all its
generated images.
- ***NSFW checker***
If checked, InvokeAI will test images for potential sexual content
and blur them out if found. Note that the NSFW checker consumes
an additional 0.6 GB of VRAM on top of the 2-3 GB of VRAM used
by most image models. If you have a low VRAM GPU (4-6 GB), you
can reduce out of memory errors by disabling the checker.
- ***HuggingFace Access Token*** - ***HuggingFace Access Token***
InvokeAI has the ability to download embedded styles and subjects InvokeAI has the ability to download embedded styles and subjects
from the HuggingFace Concept Library on-demand. However, some of from the HuggingFace Concept Library on-demand. However, some of
@ -244,30 +257,20 @@ experimental versions later.
and graphics cards. The "autocast" option is deprecated and and graphics cards. The "autocast" option is deprecated and
shouldn't be used unless you are asked to by a member of the team. shouldn't be used unless you are asked to by a member of the team.
- **Size of the RAM cache used for fast model switching*** - ***Number of models to cache in CPU memory***
This allows you to keep models in memory and switch rapidly among This allows you to keep models in memory and switch rapidly among
them rather than having them load from disk each time. This slider them rather than having them load from disk each time. This slider
controls how many models to keep loaded at once. A typical SD-1 or SD-2 model controls how many models to keep loaded at once. Each
uses 2-3 GB of memory. A typical SDXL model uses 6-7 GB. Providing more model will use 2-4 GB of RAM, so use this cautiously
RAM will allow more models to be co-resident.
- ***Output directory for images*** - ***Directory containing embedding/textual inversion files***
This is the path to a directory in which InvokeAI will store all its This is the directory in which you can place custom embedding
generated images. files (.pt or .bin). During startup, this directory will be
scanned and InvokeAI will print out the text terms that
- ***Autoimport Folder*** are available to trigger the embeddings.
This is the directory in which you can place models you have
downloaded and wish to load into InvokeAI. You can place a variety
of models in this directory, including diffusers folders, .ckpt files,
.safetensors files, as well as LoRAs, ControlNet and Textual Inversion
files (both folder and file versions). To help organize this folder,
you can create several levels of subfolders and drop your models into
whichever ones you want.
- ***Autoimport FolderLICENSE***
At the bottom of the screen you will see a checkbox for accepting At the bottom of the screen you will see a checkbox for accepting
the CreativeML Responsible AI Licenses. You need to accept the license the CreativeML Responsible AI License. You need to accept the license
in order to download Stable Diffusion models from the next screen. in order to download Stable Diffusion models from the next screen.
_You can come back to the startup options form_ as many times as you like. _You can come back to the startup options form_ as many times as you like.
@ -351,8 +354,8 @@ experimental versions later.
12. **InvokeAI Options**: You can launch InvokeAI with several different command-line arguments that 12. **InvokeAI Options**: You can launch InvokeAI with several different command-line arguments that
customize its behavior. For example, you can change the location of the customize its behavior. For example, you can change the location of the
image output directory or balance memory usage vs performance. See image output directory, or select your favorite sampler. See the
[Configuration](../features/CONFIGURATION.md) for a full list of the options. [Command-Line Interface](../features/CLI.md) for a full list of the options.
- To set defaults that will take effect every time you launch InvokeAI, - To set defaults that will take effect every time you launch InvokeAI,
use a text editor (e.g. Notepad) to exit the file use a text editor (e.g. Notepad) to exit the file
@ -372,71 +375,8 @@ experimental versions later.
Once InvokeAI is installed, do not move or remove this directory." Once InvokeAI is installed, do not move or remove this directory."
<a name="troubleshooting"></a>
## Troubleshooting ## Troubleshooting
### _OSErrors on Windows while installing dependencies_
During a zip file installation or an online update, installation stops
with an error like this:
![broken-dependency-screenshot](../assets/troubleshooting/broken-dependency.png){:width="800px"}
This seems to happen particularly often with the `pydantic` and
`numpy` packages. The most reliable solution requires several manual
steps to complete installation.
Open up a Powershell window and navigate to the `invokeai` directory
created by the installer. Then give the following series of commands:
```cmd
rm .\.venv -r -force
python -mvenv .venv
.\.venv\Scripts\activate
pip install invokeai
invokeai-configure --yes --root .
```
If you see anything marked as an error during this process please stop
and seek help on the Discord [installation support
channel](https://discord.com/channels/1020123559063990373/1041391462190956654). A
few warning messages are OK.
If you are updating from a previous version, this should restore your
system to a working state. If you are installing from scratch, there
is one additional command to give:
```cmd
wget -O invoke.bat https://raw.githubusercontent.com/invoke-ai/InvokeAI/main/installer/templates/invoke.bat.in
```
This will create the `invoke.bat` script needed to launch InvokeAI and
its related programs.
### _Stable Diffusion XL Generation Fails after Trying to Load unet_
InvokeAI is working in other respects, but when trying to generate
images with Stable Diffusion XL you get a "Server Error". The text log
in the launch window contains this log line above several more lines of
error messages:
```INFO --> Loading model:D:\LONG\PATH\TO\MODEL, type sdxl:main:unet```
This failure mode occurs when there is a network glitch during
downloading the very large SDXL model.
To address this, first go to the Web Model Manager and delete the
Stable-Diffusion-XL-base-1.X model. Then navigate to HuggingFace and
manually download the .safetensors version of the model. The 1.0
version is located at
https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/tree/main
and the file is named `sd_xl_base_1.0.safetensors`.
Save this file to disk and then reenter the Model Manager. Navigate to
Import Models->Add Model, then type (or drag-and-drop) the path to the
.safetensors file. Press "Add Model".
### _Package dependency conflicts_ ### _Package dependency conflicts_
If you have previously installed InvokeAI or another Stable Diffusion If you have previously installed InvokeAI or another Stable Diffusion

View File

@ -32,7 +32,7 @@ gaming):
* **Python** * **Python**
version 3.9 through 3.11 version 3.9 or 3.10 (3.11 is not recommended).
* **CUDA Tools** * **CUDA Tools**
@ -65,7 +65,7 @@ gaming):
To install InvokeAI with virtual environments and the PIP package To install InvokeAI with virtual environments and the PIP package
manager, please follow these steps: manager, please follow these steps:
1. Please make sure you are using Python 3.9 through 3.11. The rest of the install 1. Please make sure you are using Python 3.9 or 3.10. The rest of the install
procedure depends on this and will not work with other versions: procedure depends on this and will not work with other versions:
```bash ```bash
@ -256,7 +256,7 @@ manager, please follow these steps:
10. Render away! 10. Render away!
Browse the [features](../features/index.md) section to learn about all the Browse the [features](../features/CLI.md) section to learn about all the
things you can do with InvokeAI. things you can do with InvokeAI.
@ -270,7 +270,7 @@ manager, please follow these steps:
12. Other scripts 12. Other scripts
The [Textual Inversion](../features/TRAINING.md) script can be launched with the command: The [Textual Inversion](../features/TEXTUAL_INVERSION.md) script can be launched with the command:
```bash ```bash
invokeai-ti --gui invokeai-ti --gui

View File

@ -43,7 +43,24 @@ InvokeAI comes with support for a good set of starter models. You'll
find them listed in the master models file find them listed in the master models file
`configs/INITIAL_MODELS.yaml` in the InvokeAI root directory. The `configs/INITIAL_MODELS.yaml` in the InvokeAI root directory. The
subset that are currently installed are found in subset that are currently installed are found in
`configs/models.yaml`. `configs/models.yaml`. As of v2.3.1, the list of starter models is:
|Model Name | HuggingFace Repo ID | Description | URL |
|---------- | ---------- | ----------- | --- |
|stable-diffusion-1.5|runwayml/stable-diffusion-v1-5|Stable Diffusion version 1.5 diffusers model (4.27 GB)|https://huggingface.co/runwayml/stable-diffusion-v1-5 |
|sd-inpainting-1.5|runwayml/stable-diffusion-inpainting|RunwayML SD 1.5 model optimized for inpainting, diffusers version (4.27 GB)|https://huggingface.co/runwayml/stable-diffusion-inpainting |
|stable-diffusion-2.1|stabilityai/stable-diffusion-2-1|Stable Diffusion version 2.1 diffusers model, trained on 768 pixel images (5.21 GB)|https://huggingface.co/stabilityai/stable-diffusion-2-1 |
|sd-inpainting-2.0|stabilityai/stable-diffusion-2-inpainting|Stable Diffusion version 2.0 inpainting model (5.21 GB)|https://huggingface.co/stabilityai/stable-diffusion-2-inpainting |
|analog-diffusion-1.0|wavymulder/Analog-Diffusion|An SD-1.5 model trained on diverse analog photographs (2.13 GB)|https://huggingface.co/wavymulder/Analog-Diffusion |
|deliberate-1.0|XpucT/Deliberate|Versatile model that produces detailed images up to 768px (4.27 GB)|https://huggingface.co/XpucT/Deliberate |
|d&d-diffusion-1.0|0xJustin/Dungeons-and-Diffusion|Dungeons & Dragons characters (2.13 GB)|https://huggingface.co/0xJustin/Dungeons-and-Diffusion |
|dreamlike-photoreal-2.0|dreamlike-art/dreamlike-photoreal-2.0|A photorealistic model trained on 768 pixel images based on SD 1.5 (2.13 GB)|https://huggingface.co/dreamlike-art/dreamlike-photoreal-2.0 |
|inkpunk-1.0|Envvi/Inkpunk-Diffusion|Stylized illustrations inspired by Gorillaz, FLCL and Shinkawa; prompt with "nvinkpunk" (4.27 GB)|https://huggingface.co/Envvi/Inkpunk-Diffusion |
|openjourney-4.0|prompthero/openjourney|An SD 1.5 model fine tuned on Midjourney; prompt with "mdjrny-v4 style" (2.13 GB)|https://huggingface.co/prompthero/openjourney |
|portrait-plus-1.0|wavymulder/portraitplus|An SD-1.5 model trained on close range portraits of people; prompt with "portrait+" (2.13 GB)|https://huggingface.co/wavymulder/portraitplus |
|seek-art-mega-1.0|coreco/seek.art_MEGA|A general use SD-1.5 "anything" model that supports multiple styles (2.1 GB)|https://huggingface.co/coreco/seek.art_MEGA |
|trinart-2.0|naclbit/trinart_stable_diffusion_v2|An SD-1.5 model finetuned with ~40K assorted high resolution manga/anime-style images (2.13 GB)|https://huggingface.co/naclbit/trinart_stable_diffusion_v2 |
|waifu-diffusion-1.4|hakurei/waifu-diffusion|An SD-1.5 model trained on 680k anime/manga-style images (2.13 GB)|https://huggingface.co/hakurei/waifu-diffusion |
Note that these files are covered by an "Ethical AI" license which Note that these files are covered by an "Ethical AI" license which
forbids certain uses. When you initially download them, you are asked forbids certain uses. When you initially download them, you are asked
@ -54,7 +71,8 @@ with the model terms by visiting the URLs in the table above.
## Community-Contributed Models ## Community-Contributed Models
[HuggingFace](https://huggingface.co/models?library=diffusers) There are too many to list here and more are being contributed every
day. [HuggingFace](https://huggingface.co/models?library=diffusers)
is a great resource for diffusers models, and is also the home of a is a great resource for diffusers models, and is also the home of a
[fast-growing repository](https://huggingface.co/sd-concepts-library) [fast-growing repository](https://huggingface.co/sd-concepts-library)
of embedding (".bin") models that add subjects and/or styles to your of embedding (".bin") models that add subjects and/or styles to your
@ -68,106 +86,310 @@ only `.safetensors` and `.ckpt` models, but they can be easily loaded
into InvokeAI and/or converted into optimized `diffusers` models. Be into InvokeAI and/or converted into optimized `diffusers` models. Be
aware that CIVITAI hosts many models that generate NSFW content. aware that CIVITAI hosts many models that generate NSFW content.
!!! note
InvokeAI 2.3.x does not support directly importing and
running Stable Diffusion version 2 checkpoint models. You may instead
convert them into `diffusers` models using the conversion methods
described below.
## Installation ## Installation
There are two ways to install and manage models: There are multiple ways to install and manage models:
1. The `invokeai-model-install` script which will download and install 1. The `invokeai-configure` script which will download and install them for you.
them for you. In addition to supporting main models, you can install
ControlNet, LoRA and Textual Inversion models.
2. The web interface (WebUI) has a GUI for importing and managing 2. The command-line tool (CLI) has commands that allows you to import, configure and modify
models files.
3. The web interface (WebUI) has a GUI for importing and managing
models. models.
3. By placing models (or symbolic links to models) inside one of the ### Installation via `invokeai-configure`
InvokeAI root directory's `autoimport` folder.
### Installation via `invokeai-model-install` From the `invoke` launcher, choose option (6) "re-run the configure
script to download new models." This will launch the same script that
prompted you to select models at install time. You can use this to add
models that you skipped the first time around. It is all right to
specify a model that was previously downloaded; the script will just
confirm that the files are complete.
From the `invoke` launcher, choose option [5] "Download and install ### Installation via the CLI
models." This will launch the same script that prompted you to select
models at install time. You can use this to add models that you
skipped the first time around. It is all right to specify a model that
was previously downloaded; the script will just confirm that the files
are complete.
The installer has different panels for installing main models from You can install a new model, including any of the community-supported ones, via
HuggingFace, models from Civitai and other arbitrary web sites, the command-line client's `!import_model` command.
ControlNet models, LoRA/LyCORIS models, and Textual Inversion
embeddings. Each section has a text box in which you can enter a new
model to install. You can refer to a model using its:
1. Local path to the .ckpt, .safetensors or diffusers folder on your local machine #### Installing individual `.ckpt` and `.safetensors` models
2. A directory on your machine that contains multiple models
3. A URL that points to a downloadable model
4. A HuggingFace repo id
Previously-installed models are shown with checkboxes. Uncheck a box If the model is already downloaded to your local disk, use
to unregister the model from InvokeAI. Models that are physically `!import_model /path/to/file.ckpt` to load it. For example:
installed inside the InvokeAI root directory will be deleted and
purged (after a confirmation warning). Models that are located outside
the InvokeAI root directory will be unregistered but not deleted.
Note: The installer script uses a console-based text interface that requires ```bash
significant amounts of horizontal and vertical space. If the display invoke> !import_model C:/Users/fred/Downloads/martians.safetensors
looks messed up, just enlarge the terminal window and/or relaunch the
script.
If you wish you can script model addition and deletion, as well as
listing installed models. Start the "developer's console" and give the
command `invokeai-model-install --help`. This will give you a series
of command-line parameters that will let you control model
installation. Examples:
```
# (list all controlnet models)
invokeai-model-install --list controlnet
# (install the model at the indicated URL)
invokeai-model-install --add http://civitai.com/2860
# (delete the named model)
invokeai-model-install --delete sd-1/main/analog-diffusion
``` ```
### Installation via the Web GUI !!! tip "Forward Slashes"
On Windows systems, use forward slashes rather than backslashes
in your file paths.
If you do use backslashes,
you must double them like this:
`C:\\Users\\fred\\Downloads\\martians.safetensors`
To install a new model using the Web GUI, do the following: Alternatively you can directly import the file using its URL:
1. Open the InvokeAI Model Manager (cube at the bottom of the ```bash
left-hand panel) and navigate to *Import Models* invoke> !import_model https://example.org/sd_models/martians.safetensors
```
2. In the field labeled *Location* type in the path to the model you For this to work, the URL must not be password-protected. Otherwise
wish to install. You may use a URL, HuggingFace repo id, or a path on you will receive a 404 error.
your local disk.
3. Alternatively, the *Scan for Models* button allows you to paste in When you import a legacy model, the CLI will first ask you what type
the path to a folder somewhere on your machine. It will be scanned for of model this is. You can indicate whether it is a model based on
importable models and prompt you to add the ones of your choice. Stable Diffusion 1.x (1.4 or 1.5), one based on Stable Diffusion 2.x,
or a 1.x inpainting model. Be careful to indicate the correct model
type, or it will not load correctly. You can correct the model type
after the fact using the `!edit_model` command.
4. Press *Add Model* and wait for confirmation that the model The system will then ask you a few other questions about the model,
was added. including what size image it was trained on (usually 512x512), what
name and description you wish to use for it, and whether you would
like to install a custom VAE (variable autoencoder) file for the
model. For recent models, the answer to the VAE question is usually
"no," but it won't hurt to answer "yes".
To delete a model, Select *Model Manager* to list all the currently After importing, the model will load. If this is successful, you will
installed models. Press the trash can icons to delete any models you be asked if you want to keep the model loaded in memory to start
wish to get rid of. Models whose weights are located inside the generating immediately. You'll also be asked if you wish to make this
InvokeAI `models` directory will be purged from disk, while those the default model on startup. You can change this later using
located outside will be unregistered from InvokeAI, but not deleted. `!edit_model`.
You can see where model weights are located by clicking on the model name. #### Importing a batch of `.ckpt` and `.safetensors` models from a directory
This will bring up an editable info panel showing the model's characteristics,
including the `Model Location` of its files.
### Installation via the `autoimport` function You may also point `!import_model` to a directory containing a set of
`.ckpt` or `.safetensors` files. They will be imported _en masse_.
In the InvokeAI root directory you will find a series of folders under !!! example
`autoimport`, one each for main models, controlnets, embeddings and
Loras. Any models that you add to these directories will be scanned
at startup time and registered automatically.
You may create symbolic links from these folders to models located ```console
elsewhere on disk and they will be autoimported. You can also create invoke> !import_model C:/Users/fred/Downloads/civitai_models/
subfolders and organize them as you wish. ```
The location of the autoimport directories are controlled by settings You will be given the option to import all models found in the
in `invokeai.yaml`. See [Configuration](../features/CONFIGURATION.md). directory, or select which ones to import. If there are subfolders
within the directory, they will be searched for models to import.
#### Installing `diffusers` models
You can install a `diffusers` model from the HuggingFace site using
`!import_model` and the HuggingFace repo_id for the model:
```bash
invoke> !import_model andite/anything-v4.0
```
Alternatively, you can download the model to disk and import it from
there. The model may be distributed as a ZIP file, or as a Git
repository:
```bash
invoke> !import_model C:/Users/fred/Downloads/andite--anything-v4.0
```
!!! tip "The CLI supports file path autocompletion"
Type a bit of the path name and hit ++tab++ in order to get a choice of
possible completions.
!!! tip "On Windows, you can drag model files onto the command-line"
Once you have typed in `!import_model `, you can drag the
model file or directory onto the command-line to insert the model path. This way, you don't need to
type it or copy/paste. However, you will need to reverse or
double backslashes as noted above.
Before installing, the CLI will ask you for a short name and
description for the model, whether to make this the default model that
is loaded at InvokeAI startup time, and whether to replace its
VAE. Generally the answer to the latter question is "no".
### Converting legacy models into `diffusers`
The CLI `!convert_model` will convert a `.safetensors` or `.ckpt`
models file into `diffusers` and install it.This will enable the model
to load and run faster without loss of image quality.
The usage is identical to `!import_model`. You may point the command
to either a downloaded model file on disk, or to a (non-password
protected) URL:
```bash
invoke> !convert_model C:/Users/fred/Downloads/martians.safetensors
```
After a successful conversion, the CLI will offer you the option of
deleting the original `.ckpt` or `.safetensors` file.
### Optimizing a previously-installed model
Lastly, if you have previously installed a `.ckpt` or `.safetensors`
file and wish to convert it into a `diffusers` model, you can do this
without re-downloading and converting the original file using the
`!optimize_model` command. Simply pass the short name of an existing
installed model:
```bash
invoke> !optimize_model martians-v1.0
```
The model will be converted into `diffusers` format and replace the
previously installed version. You will again be offered the
opportunity to delete the original `.ckpt` or `.safetensors` file.
### Related CLI Commands
There are a whole series of additional model management commands in
the CLI that you can read about in [Command-Line
Interface](../features/CLI.md). These include:
* `!models` - List all installed models
* `!switch <model name>` - Switch to the indicated model
* `!edit_model <model name>` - Edit the indicated model to change its name, description or other properties
* `!del_model <model name>` - Delete the indicated model
### Manually editing `configs/models.yaml`
If you are comfortable with a text editor then you may simply edit `models.yaml`
directly.
You will need to download the desired `.ckpt/.safetensors` file and
place it somewhere on your machine's filesystem. Alternatively, for a
`diffusers` model, record the repo_id or download the whole model
directory. Then using a **text** editor (e.g. the Windows Notepad
application), open the file `configs/models.yaml`, and add a new
stanza that follows this model:
#### A legacy model
A legacy `.ckpt` or `.safetensors` entry will look like this:
```yaml
arabian-nights-1.0:
description: A great fine-tune in Arabian Nights style
weights: ./path/to/arabian-nights-1.0.ckpt
config: ./configs/stable-diffusion/v1-inference.yaml
format: ckpt
width: 512
height: 512
default: false
```
Note that `format` is `ckpt` for both `.ckpt` and `.safetensors` files.
#### A diffusers model
A stanza for a `diffusers` model will look like this for a HuggingFace
model with a repository ID:
```yaml
arabian-nights-1.1:
description: An even better fine-tune of the Arabian Nights
repo_id: captahab/arabian-nights-1.1
format: diffusers
default: true
```
And for a downloaded directory:
```yaml
arabian-nights-1.1:
description: An even better fine-tune of the Arabian Nights
path: /path/to/captahab-arabian-nights-1.1
format: diffusers
default: true
```
There is additional syntax for indicating an external VAE to use with
this model. See `INITIAL_MODELS.yaml` and `models.yaml` for examples.
After you save the modified `models.yaml` file relaunch
`invokeai`. The new model will now be available for your use.
### Installation via the WebUI
To access the WebUI Model Manager, click on the button that looks like
a cube in the upper right side of the browser screen. This will bring
up a dialogue that lists the models you have already installed, and
allows you to load, delete or edit them:
<figure markdown>
![model-manager](../assets/installing-models/webui-models-1.png)
</figure>
To add a new model, click on **+ Add New** and select to either a
checkpoint/safetensors model, or a diffusers model:
<figure markdown>
![model-manager-add-new](../assets/installing-models/webui-models-2.png)
</figure>
In this example, we chose **Add Diffusers**. As shown in the figure
below, a new dialogue prompts you to enter the name to use for the
model, its description, and either the location of the `diffusers`
model on disk, or its Repo ID on the HuggingFace web site. If you
choose to enter a path to disk, the system will autocomplete for you
as you type:
<figure markdown>
![model-manager-add-diffusers](../assets/installing-models/webui-models-3.png)
</figure>
Press **Add Model** at the bottom of the dialogue (scrolled out of
site in the figure), and the model will be downloaded, imported, and
registered in `models.yaml`.
The **Add Checkpoint/Safetensor Model** option is similar, except that
in this case you can choose to scan an entire folder for
checkpoint/safetensors files to import. Simply type in the path of the
directory and press the "Search" icon. This will display the
`.ckpt` and `.safetensors` found inside the directory and its
subfolders, and allow you to choose which ones to import:
<figure markdown>
![model-manager-add-checkpoint](../assets/installing-models/webui-models-4.png)
</figure>
## Model Management Startup Options
The `invoke` launcher and the `invokeai` script accept a series of
command-line arguments that modify InvokeAI's behavior when loading
models. These can be provided on the command line, or added to the
InvokeAI root directory's `invokeai.init` initialization file.
The arguments are:
* `--model <model name>` -- Start up with the indicated model loaded
* `--ckpt_convert` -- When a checkpoint/safetensors model is loaded, convert it into a `diffusers` model in memory. This does not permanently save the converted model to disk.
* `--autoconvert <path/to/directory>` -- Scan the indicated directory path for new checkpoint/safetensors files, convert them into `diffusers` models, and import them into InvokeAI.
Here is an example of providing an argument on the command line using
the `invoke.sh` launch script:
```bash
invoke.sh --autoconvert /home/fred/stable-diffusion-checkpoints
```
And here is what the same argument looks like in `invokeai.init`:
```bash
--outdir="/home/fred/invokeai/outputs
--no-nsfw_checker
--autoconvert /home/fred/stable-diffusion-checkpoints
```

View File

@ -1,4 +1,6 @@
# Overview ---
title: Overview
---
We offer several ways to install InvokeAI, each one suited to your We offer several ways to install InvokeAI, each one suited to your
experience and preferences. We suggest that everyone start by experience and preferences. We suggest that everyone start by
@ -13,57 +15,7 @@ See the [troubleshooting
section](010_INSTALL_AUTOMATED.md#troubleshooting) of the automated section](010_INSTALL_AUTOMATED.md#troubleshooting) of the automated
install guide for frequently-encountered installation issues. install guide for frequently-encountered installation issues.
This fork is supported across Linux, Windows and Macintosh. Linux users can use ## Main Application
either an Nvidia-based card (with CUDA support) or an AMD card (using the ROCm
driver).
### [Installation Getting Started Guide](installation)
#### **[Automated Installer](010_INSTALL_AUTOMATED.md)**
✅ This is the recommended installation method for first-time users.
#### [Manual Installation](020_INSTALL_MANUAL.md)
This method is recommended for experienced users and developers
#### [Docker Installation](040_INSTALL_DOCKER.md)
This method is recommended for those familiar with running Docker containers
### Other Installation Guides
- [PyPatchMatch](installation/060_INSTALL_PATCHMATCH.md)
- [XFormers](installation/070_INSTALL_XFORMERS.md)
- [CUDA and ROCm Drivers](installation/030_INSTALL_CUDA_AND_ROCM.md)
- [Installing New Models](installation/050_INSTALLING_MODELS.md)
## :fontawesome-solid-computer: Hardware Requirements
### :octicons-cpu-24: System
You wil need one of the following:
- :simple-nvidia: An NVIDIA-based graphics card with 4 GB or more VRAM memory.
- :simple-amd: An AMD-based graphics card with 4 GB or more VRAM memory (Linux
only)
- :fontawesome-brands-apple: An Apple computer with an M1 chip.
** SDXL 1.0 Requirements*
To use SDXL, user must have one of the following:
- :simple-nvidia: An NVIDIA-based graphics card with 8 GB or more VRAM memory.
- :simple-amd: An AMD-based graphics card with 16 GB or more VRAM memory (Linux
only)
- :fontawesome-brands-apple: An Apple computer with an M1 chip.
### :fontawesome-solid-memory: Memory and Disk
- At least 12 GB Main Memory RAM.
- At least 18 GB of free disk space for the machine learning model, Python, and
all its dependencies.
We do **not recommend** the following video cards due to issues with their
running in half-precision mode and having insufficient VRAM to render 512x512
images in full-precision mode:
- NVIDIA 10xx series cards such as the 1080ti
- GTX 1650 series cards
- GTX 1660 series cards
## Installation options
1. [Automated Installer](010_INSTALL_AUTOMATED.md) 1. [Automated Installer](010_INSTALL_AUTOMATED.md)
@ -72,9 +24,6 @@ images in full-precision mode:
"developer console" which will help us debug problems with you and "developer console" which will help us debug problems with you and
give you to access experimental features. give you to access experimental features.
✅ This is the recommended option for first time users.
2. [Manual Installation](020_INSTALL_MANUAL.md) 2. [Manual Installation](020_INSTALL_MANUAL.md)
In this method you will manually run the commands needed to install In this method you will manually run the commands needed to install

View File

@ -1,53 +0,0 @@
# Community Nodes
These are nodes that have been developed by the community, for the community. If you're not sure what a node is, you can learn more about nodes [here](overview.md).
If you'd like to submit a node for the community, please refer to the [node creation overview](./overview.md#contributing-nodes).
To download a node, simply download the `.py` node file from the link and add it to the `invokeai/app/invocations/` folder in your Invoke AI install location. Along with the node, an example node graph should be provided to help you get started with the node.
To use a community node graph, download the the `.json` node graph file and load it into Invoke AI via the **Load Nodes** button on the Node Editor.
## Disclaimer
The nodes linked below have been developed and contributed by members of the Invoke AI community. While we strive to ensure the quality and safety of these contributions, we do not guarantee the reliability or security of the nodes. If you have issues or concerns with any of the nodes below, please raise it on GitHub or in the Discord.
## List of Nodes
### FaceTools
**Description:** FaceTools is a collection of nodes created to manipulate faces as you would in Unified Canvas. It includes FaceMask, FaceOff, and FacePlace. FaceMask autodetects a face in the image using MediaPipe and creates a mask from it. FaceOff similarly detects a face, then takes the face off of the image by adding a square bounding box around it and cropping/scaling it. FacePlace puts the bounded face image from FaceOff back onto the original image. Using these nodes with other inpainting node(s), you can put new faces on existing things, put new things around existing faces, and work closer with a face as a bounded image. Additionally, you can supply X and Y offset values to scale/change the shape of the mask for finer control on FaceMask and FaceOff. See GitHub repository below for usage examples.
**Node Link:** https://github.com/ymgenesis/FaceTools/
**FaceMask Output Examples**
![5cc8abce-53b0-487a-b891-3bf94dcc8960](https://github.com/invoke-ai/InvokeAI/assets/25252829/43f36d24-1429-4ab1-bd06-a4bedfe0955e)
![b920b710-1882-49a0-8d02-82dff2cca907](https://github.com/invoke-ai/InvokeAI/assets/25252829/7660c1ed-bf7d-4d0a-947f-1fc1679557ba)
![71a91805-fda5-481c-b380-264665703133](https://github.com/invoke-ai/InvokeAI/assets/25252829/f8f6a2ee-2b68-4482-87da-b90221d5c3e2)
<hr>
### 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
--------------------------------
### Example Node Template
**Description:** This node allows you to do super cool things with InvokeAI.
**Node Link:** https://github.com/invoke-ai/InvokeAI/fake_node.py
**Example Node Graph:** https://github.com/invoke-ai/InvokeAI/fake_node_graph.json
**Output Examples**
![Example Image](https://invoke-ai.github.io/InvokeAI/assets/invoke_ai_banner.png){: style="height:115px;width:240px"}
## Help
If you run into any issues with a node, please post in the [InvokeAI Discord](https://discord.gg/ZmtBAhwWhy).

View File

@ -1,42 +0,0 @@
# Nodes
## What are Nodes?
An Node is simply a single operation that takes in some inputs and gives
out some outputs. We can then chain multiple nodes together to create more
complex functionality. All InvokeAI features are added through nodes.
This means nodes can be used to easily extend the image generation capabilities of InvokeAI, and allow you build workflows to suit your needs.
You can read more about nodes and the node editor [here](../features/NODES.md).
## Downloading Nodes
To download a new node, visit our list of [Community Nodes](communityNodes.md). These are nodes that have been created by the community, for the community.
## Contributing Nodes
To learn about creating a new node, please visit our [Node creation documenation](../contributing/INVOCATIONS.md).
Once youve created a node and confirmed that it behaves as expected locally, follow these steps:
* Make sure the node is contained in a new Python (.py) file
* Submit a pull request with a link to your node in GitHub against the `nodes` branch to add the node to the [Community Nodes](Community Nodes) list
* Make sure you are following the template below and have provided all relevant details about the node and what it does.
* A maintainer will review the pull request and node. If the node is aligned with the direction of the project, you might be asked for permission to include it in the core project.
### Community Node Template
```markdown
--------------------------------
### Super Cool Node Template
**Description:** This node allows you to do super cool things with InvokeAI.
**Node Link:** https://github.com/invoke-ai/InvokeAI/fake_node.py
**Example Node Graph:** https://github.com/invoke-ai/InvokeAI/fake_node_graph.json
**Output Examples**
![InvokeAI](https://invoke-ai.github.io/InvokeAI/assets/invoke_ai_banner.png)
```

View File

@ -17,267 +17,67 @@ We thank them for all of their time and hard work.
* @lstein (Lincoln Stein) - Co-maintainer * @lstein (Lincoln Stein) - Co-maintainer
* @blessedcoolant - Co-maintainer * @blessedcoolant - Co-maintainer
* @hipsterusername (Kent Keirsey) - Co-maintainer, CEO, Positive Vibes * @hipsterusername (Kent Keirsey) - Product Manager
* @psychedelicious (Spencer Mabrito) - Web Team Leader * @psychedelicious - Web Team Leader
* @Kyle0654 (Kyle Schouviller) - Node Architect and General Backend Wizard * @Kyle0654 (Kyle Schouviller) - Node Architect and General Backend Wizard
* @damian0815 - Attention Systems and Compel Maintainer * @damian0815 - Attention Systems and Gameplay Engineer
* @mauwii (Matthias Wild) - Continuous integration and product maintenance engineer
* @Netsvetaev (Artur Netsvetaev) - UI/UX Developer
* @tildebyte - General gadfly and resident (self-appointed) know-it-all
* @keturn - Lead for Diffusers port
* @ebr (Eugene Brodsky) - Cloud/DevOps/Sofware engineer; your friendly neighbourhood cluster-autoscaler * @ebr (Eugene Brodsky) - Cloud/DevOps/Sofware engineer; your friendly neighbourhood cluster-autoscaler
* @genomancer (Gregg Helt) - Controlnet support * @jpphoto (Jonathan Pollack) - Inference and rendering engine optimization
* @StAlKeR7779 (Sergey Borisov) - Torch stack, ONNX, model management, optimization * @genomancer (Gregg Helt) - Model training and merging
* @cheerio (Mary Rogers) - Lead Engineer & Web App Development
* @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
* @keturn (Kevin Turner) - Diffusers
* @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
## **Full List of Contributors by Commit Name** ## **Contributions by**
- AbdBarho - [Sean McLellan](https://github.com/Oceanswave)
- ablattmann - [Kevin Gibbons](https://github.com/bakkot)
- AdamOStark - [Tesseract Cat](https://github.com/TesseractCat)
- Adam Rice - [blessedcoolant](https://github.com/blessedcoolant)
- Airton Silva - [David Ford](https://github.com/david-ford)
- Alexander Eichhorn - [yunsaki](https://github.com/yunsaki)
- Alexandre D. Roberge - [James Reynolds](https://github.com/magnusviri)
- Andreas Rozek - [David Wager](https://github.com/maddavid123)
- Andre LaBranche - [Jason Toffaletti](https://github.com/toffaletti)
- Andy Bearman - [tildebyte](https://github.com/tildebyte)
- Andy Luhrs - [Cragin Godley](https://github.com/cgodley)
- Andy Pilate - [BlueAmulet](https://github.com/BlueAmulet)
- Any-Winter-4079 - [Benjamin Warner](https://github.com/warner-benjamin)
- apolinario - [Cora Johnson-Roberson](https://github.com/corajr)
- ArDiouscuros - [veprogames](https://github.com/veprogames)
- Armando C. Santisbon - [JigenD](https://github.com/JigenD)
- Arthur Holstvoogd - [Niek van der Maas](https://github.com/Niek)
- artmen1516 - [Henry van Megen](https://github.com/hvanmegen)
- Artur - [Håvard Gulldahl](https://github.com/havardgulldahl)
- Arturo Mendivil - [greentext2](https://github.com/greentext2)
- Ben Alkov - [Simon Vans-Colina](https://github.com/simonvc)
- Benjamin Warner - [Gabriel Rotbart](https://github.com/gabrielrotbart)
- Bernard Maltais - [Eric Khun](https://github.com/erickhun)
- blessedcoolant - [Brent Ozar](https://github.com/BrentOzar)
- blhook - [nderscore](https://github.com/nderscore)
- BlueAmulet - [Mikhail Tishin](https://github.com/tishin)
- Bouncyknighter - [Tom Elovi Spruce](https://github.com/ilovecomputers)
- Brandon Rising - [spezialspezial](https://github.com/spezialspezial)
- Brent Ozar - [Yosuke Shinya](https://github.com/shinya7y)
- Brian Racer - [Andy Pilate](https://github.com/Cubox)
- bsilvereagle - [Muhammad Usama](https://github.com/SMUsamaShah)
- c67e708d - [Arturo Mendivil](https://github.com/artmen1516)
- CapableWeb - [Paul Sajna](https://github.com/sajattack)
- Carson Katri - [Samuel Husso](https://github.com/shusso)
- Chloe - [nicolai256](https://github.com/nicolai256)
- Chris Dawson - [Mihai](https://github.com/mh-dm)
- Chris Hayes - [Any Winter](https://github.com/any-winter-4079)
- Chris Jones - [Doggettx](https://github.com/doggettx)
- chromaticist - [Matthias Wild](https://github.com/mauwii)
- Claus F. Strasburger - [Kyle Schouviller](https://github.com/kyle0654)
- cmdr2 - [rabidcopy](https://github.com/rabidcopy)
- cody - [Dominic Letz](https://github.com/dominicletz)
- Conor Reid - [Dmitry T.](https://github.com/ArDiouscuros)
- Cora Johnson-Roberson - [Kent Keirsey](https://github.com/hipsterusername)
- coreco - [psychedelicious](https://github.com/psychedelicious)
- cosmii02 - [damian0815](https://github.com/damian0815)
- cpacker - [Eugene Brodsky](https://github.com/ebr)
- Cragin Godley
- creachec
- Damian Stewart
- Daniel Manzke
- Danny Beer
- Dan Sully
- David Burnett
- David Ford
- David Regla
- David Wager
- Daya Adianto
- db3000
- Denis Olshin
- Dennis
- Dominic Letz
- DrGunnarMallon
- Edward Johan
- elliotsayes
- Elrik
- ElrikUnderlake
- Eric Khun
- Eric Wolf
- Eugene Brodsky
- ExperimentalCyborg
- Fabian Bahl
- Fabio 'MrWHO' Torchetti
- fattire
- Felipe Nogueira
- Félix Sanz
- figgefigge
- Gabriel Mackievicz Telles
- gabrielrotbart
- gallegonovato
- Gérald LONLAS
- GitHub Actions Bot
- gogurtenjoyer
- greentext2
- Gregg Helt
- H4rk
- Håvard Gulldahl
- henry
- Henry van Megen
- hipsterusername
- hj
- Hosted Weblate
- Iman Karim
- ismail ihsan bülbül
- Ivan Efimov
- jakehl
- Jakub Kolčář
- JamDon2
- James Reynolds
- Jan Skurovec
- Jari Vetoniemi
- Jason Toffaletti
- Jaulustus
- Jeff Mahoney
- jeremy
- Jeremy Clark
- JigenD
- Jim Hays
- Johan Roxendal
- Johnathon Selstad
- Jonathan
- Joseph Dries III
- JPPhoto
- jspraul
- Justin Wong
- Juuso V
- Kaspar Emanuel
- Katsuyuki-Karasawa
- Kent Keirsey
- Kevin Coakley
- Kevin Gibbons
- Kevin Schaul
- Kevin Turner
- krummrey
- Kyle Lacy
- Kyle Schouviller
- Lawrence Norton
- LemonDouble
- Leo Pasanen
- Lincoln Stein
- LoganPederson
- Lynne Whitehorn
- majick
- Marco Labarile
- Martin Kristiansen
- Mary Hipp Rogers
- mastercaster9000
- Matthias Wild
- michaelk71
- mickr777
- Mihai
- Mihail Dumitrescu
- Mikhail Tishin
- Millun Atluri
- Minjune Song
- mitien
- mofuzz
- Muhammad Usama
- Name
- _nderscore
- Netzer R
- Nicholas Koh
- Nicholas Körfer
- nicolai256
- Niek van der Maas
- noodlebox
- Nuno Coração
- ofirkris
- Olivier Louvignes
- owenvincent
- Patrick Esser
- Patrick Tien
- Patrick von Platen
- Paul Sajna
- pejotr
- Peter Baylies
- Peter Lin
- plucked
- prixt
- psychedelicious
- Rainer Bernhardt
- Riccardo Giovanetti
- Rich Jones
- rmagur1203
- Rob Baines
- Robert Bolender
- Robin Rombach
- Rohan Barar
- rpagliuca
- rromb
- Rupesh Sreeraman
- Ryan Cao
- Saifeddine
- Saifeddine ALOUI
- SammCheese
- Sammy
- sammyf
- Samuel Husso
- Scott Lahteine
- Sean McLellan
- Sebastian Aigner
- Sergey Borisov
- Sergey Krashevich
- Shapor Naghibzadeh
- Shawn Zhong
- Simon Vans-Colina
- skunkworxdark
- slashtechno
- spezialspezial
- ssantos
- StAlKeR7779
- Stephan Koglin-Fischer
- SteveCaruso
- Steve Martinelli
- Steven Frank
- System X - Files
- Taylor Kems
- techicode
- techybrain-dev
- tesseractcat
- thealanle
- Thomas
- tildebyte
- Tim Cabbage
- Tom
- Tom Elovi Spruce
- Tom Gouville
- tomosuto
- Travco
- Travis Palmer
- tyler
- unknown
- user1
- Vedant Madane
- veprogames
- wa.code
- wfng92
- whosawhatsis
- Will
- William Becher
- William Chong
- xra
- Yeung Yiu Hung
- ymgenesis
- Yorzaren
- Yosuke Shinya
- yun saki
- Zadagu
- zeptofine
- 冯不游
- 唐澤 克幸
## **Original CompVis Authors** ## **Original CompVis Authors**

25
flake.lock generated
View File

@ -1,25 +0,0 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1690630721,
"narHash": "sha256-Y04onHyBQT4Erfr2fc82dbJTfXGYrf4V0ysLUYnPOP8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d2b52322f35597c62abf56de91b0236746b2a03d",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View File

@ -1,81 +0,0 @@
# Important note: this flake does not attempt to create a fully isolated, 'pure'
# Python environment for InvokeAI. Instead, it depends on local invocations of
# virtualenv/pip to install the required (binary) packages, most importantly the
# prebuilt binary pytorch packages with CUDA support.
# ML Python packages with CUDA support, like pytorch, are notoriously expensive
# to compile so it's purposefuly not what this flake does.
{
description = "An (impure) flake to develop on InvokeAI.";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
};
python = pkgs.python310;
mkShell = { dir, install }:
let
setupScript = pkgs.writeScript "setup-invokai" ''
# This must be sourced using 'source', not executed.
${python}/bin/python -m venv ${dir}
${dir}/bin/python -m pip install ${install}
# ${dir}/bin/python -c 'import torch; assert(torch.cuda.is_available())'
source ${dir}/bin/activate
'';
in
pkgs.mkShell rec {
buildInputs = with pkgs; [
# Backend: graphics, CUDA.
cudaPackages.cudnn
cudaPackages.cuda_nvrtc
cudatoolkit
freeglut
glib
gperf
procps
libGL
libGLU
linuxPackages.nvidia_x11
python
stdenv.cc
stdenv.cc.cc.lib
xorg.libX11
xorg.libXext
xorg.libXi
xorg.libXmu
xorg.libXrandr
xorg.libXv
zlib
# Pre-commit hooks.
black
# Frontend.
yarn
nodejs
];
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs;
CUDA_PATH = pkgs.cudatoolkit;
EXTRA_LDFLAGS = "-L${pkgs.linuxPackages.nvidia_x11}/lib";
shellHook = ''
if [[ -f "${dir}/bin/activate" ]]; then
source "${dir}/bin/activate"
echo "Using Python: $(which python)"
else
echo "Use 'source ${setupScript}' to set up the environment."
fi
'';
};
in
{
devShells.${system} = rec {
develop = mkShell { dir = "venv"; install = "-e '.[xformers]' --extra-index-url https://download.pytorch.org/whl/cu118"; };
default = develop;
};
};
}

View File

@ -24,8 +24,7 @@ read -e -p "Tag this repo with '${VERSION}' and '${LATEST_TAG}'? [n]: " input
RESPONSE=${input:='n'} RESPONSE=${input:='n'}
if [ "$RESPONSE" == 'y' ]; then if [ "$RESPONSE" == 'y' ]; then
git push origin :refs/tags/$VERSION if ! git tag $VERSION ; then
if ! git tag -fa $VERSION ; then
echo "Existing/invalid tag" echo "Existing/invalid tag"
exit -1 exit -1
fi fi

View File

@ -38,7 +38,7 @@ echo https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist
echo. echo.
echo See %INSTRUCTIONS% for more details. echo See %INSTRUCTIONS% for more details.
echo. echo.
echo FOR THE BEST USER EXPERIENCE WE SUGGEST MAXIMIZING THIS WINDOW NOW. echo "For the best user experience we suggest enlarging or maximizing this window now."
pause pause
@rem ---------------------------- check Python version --------------- @rem ---------------------------- check Python version ---------------

View File

@ -9,17 +9,13 @@ cd $scriptdir
function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; } function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; }
MINIMUM_PYTHON_VERSION=3.9.0 MINIMUM_PYTHON_VERSION=3.9.0
MAXIMUM_PYTHON_VERSION=3.11.100 MAXIMUM_PYTHON_VERSION=3.11.0
PYTHON="" PYTHON=""
for candidate in python3.11 python3.10 python3.9 python3 python ; do for candidate in python3.10 python3.9 python3 python ; do
if ppath=`which $candidate`; then if ppath=`which $candidate`; then
# when using `pyenv`, the executable for an inactive Python version will exist but will not be operational
# we check that this found executable can actually run
if [ $($candidate --version &>/dev/null; echo ${PIPESTATUS}) -gt 0 ]; then continue; fi
python_version=$($ppath -V | awk '{ print $2 }') python_version=$($ppath -V | awk '{ print $2 }')
if [ $(version $python_version) -ge $(version "$MINIMUM_PYTHON_VERSION") ]; then if [ $(version $python_version) -ge $(version "$MINIMUM_PYTHON_VERSION") ]; then
if [ $(version $python_version) -le $(version "$MAXIMUM_PYTHON_VERSION") ]; then if [ $(version $python_version) -lt $(version "$MAXIMUM_PYTHON_VERSION") ]; then
PYTHON=$ppath PYTHON=$ppath
break break
fi fi

View File

@ -13,7 +13,7 @@ from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import Union from typing import Union
SUPPORTED_PYTHON = ">=3.9.0,<=3.11.100" SUPPORTED_PYTHON = ">=3.9.0,<3.11"
INSTALLER_REQS = ["rich", "semver", "requests", "plumbum", "prompt-toolkit"] INSTALLER_REQS = ["rich", "semver", "requests", "plumbum", "prompt-toolkit"]
BOOTSTRAP_VENV_PREFIX = "invokeai-installer-tmp" BOOTSTRAP_VENV_PREFIX = "invokeai-installer-tmp"
@ -141,16 +141,15 @@ class Installer:
# upgrade pip in Python 3.9 environments # upgrade pip in Python 3.9 environments
if int(platform.python_version_tuple()[1]) == 9: if int(platform.python_version_tuple()[1]) == 9:
from plumbum import FG, local from plumbum import FG, local
pip = local[get_pip_from_venv(venv_dir)] pip = local[get_pip_from_venv(venv_dir)]
pip["install", "--upgrade", "pip"] & FG pip[ "install", "--upgrade", "pip"] & FG
return venv_dir return venv_dir
def install( def install(self, root: str = "~/invokeai-3", version: str = "latest", yes_to_all=False, find_links: Path = None) -> None:
self, root: str = "~/invokeai", version: str = "latest", yes_to_all=False, find_links: Path = None
) -> None:
""" """
Install the InvokeAI application into the given runtime path Install the InvokeAI application into the given runtime path
@ -168,8 +167,7 @@ class Installer:
messages.welcome() messages.welcome()
default_path = os.environ.get("INVOKEAI_ROOT") or Path(root).expanduser().resolve() self.dest = Path(root).expanduser().resolve() if yes_to_all else messages.dest_path(root)
self.dest = default_path if yes_to_all else messages.dest_path(root)
# create the venv for the app # create the venv for the app
self.venv = self.app_venv() self.venv = self.app_venv()
@ -177,7 +175,7 @@ class Installer:
self.instance = InvokeAiInstance(runtime=self.dest, venv=self.venv, version=version) self.instance = InvokeAiInstance(runtime=self.dest, venv=self.venv, version=version)
# install dependencies and the InvokeAI application # install dependencies and the InvokeAI application
(extra_index_url, optional_modules) = get_torch_source() if not yes_to_all else (None, None) (extra_index_url,optional_modules) = get_torch_source() if not yes_to_all else (None,None)
self.instance.install( self.instance.install(
extra_index_url, extra_index_url,
optional_modules, optional_modules,
@ -190,7 +188,6 @@ class Installer:
# run through the configuration flow # run through the configuration flow
self.instance.configure() self.instance.configure()
class InvokeAiInstance: class InvokeAiInstance:
""" """
Manages an installed instance of InvokeAI, comprising a virtual environment and a runtime directory. Manages an installed instance of InvokeAI, comprising a virtual environment and a runtime directory.
@ -199,6 +196,7 @@ class InvokeAiInstance:
""" """
def __init__(self, runtime: Path, venv: Path, version: str) -> None: def __init__(self, runtime: Path, venv: Path, version: str) -> None:
self.runtime = runtime self.runtime = runtime
self.venv = venv self.venv = venv
self.pip = get_pip_from_venv(venv) self.pip = get_pip_from_venv(venv)
@ -249,9 +247,6 @@ class InvokeAiInstance:
pip[ pip[
"install", "install",
"--require-virtualenv", "--require-virtualenv",
"numpy~=1.24.0", # choose versions that won't be uninstalled during phase 2
"urllib3~=1.26.0",
"requests~=2.28.0",
"torch~=2.0.0", "torch~=2.0.0",
"torchmetrics==0.11.4", "torchmetrics==0.11.4",
"torchvision>=0.14.1", "torchvision>=0.14.1",
@ -317,7 +312,7 @@ class InvokeAiInstance:
"install", "install",
"--require-virtualenv", "--require-virtualenv",
"--use-pep517", "--use-pep517",
str(src) + (optional_modules if optional_modules else ""), str(src)+(optional_modules if optional_modules else ''),
"--find-links" if find_links is not None else None, "--find-links" if find_links is not None else None,
find_links, find_links,
"--extra-index-url" if extra_index_url is not None else None, "--extra-index-url" if extra_index_url is not None else None,
@ -334,12 +329,12 @@ class InvokeAiInstance:
# set sys.argv to a consistent state # set sys.argv to a consistent state
new_argv = [sys.argv[0]] new_argv = [sys.argv[0]]
for i in range(1, len(sys.argv)): for i in range(1,len(sys.argv)):
el = sys.argv[i] el = sys.argv[i]
if el in ["-r", "--root"]: if el in ['-r','--root']:
new_argv.append(el) new_argv.append(el)
new_argv.append(sys.argv[i + 1]) new_argv.append(sys.argv[i+1])
elif el in ["-y", "--yes", "--yes-to-all"]: elif el in ['-y','--yes','--yes-to-all']:
new_argv.append(el) new_argv.append(el)
sys.argv = new_argv sys.argv = new_argv
@ -358,16 +353,16 @@ class InvokeAiInstance:
invokeai_configure() invokeai_configure()
succeeded = True succeeded = True
except requests.exceptions.ConnectionError as e: except requests.exceptions.ConnectionError as e:
print(f"\nA network error was encountered during configuration and download: {str(e)}") print(f'\nA network error was encountered during configuration and download: {str(e)}')
except OSError as e: except OSError as e:
print(f"\nAn OS error was encountered during configuration and download: {str(e)}") print(f'\nAn OS error was encountered during configuration and download: {str(e)}')
except Exception as e: except Exception as e:
print(f"\nA problem was encountered during the configuration and download steps: {str(e)}") print(f'\nA problem was encountered during the configuration and download steps: {str(e)}')
finally: finally:
if not succeeded: if not succeeded:
print('To try again, find the "invokeai" directory, run the script "invoke.sh" or "invoke.bat"') print('To try again, find the "invokeai" directory, run the script "invoke.sh" or "invoke.bat"')
print("and choose option 7 to fix a broken install, optionally followed by option 5 to install models.") print('and choose option 7 to fix a broken install, optionally followed by option 5 to install models.')
print("Alternatively you can relaunch the installer.") print('Alternatively you can relaunch the installer.')
def install_user_scripts(self): def install_user_scripts(self):
""" """
@ -376,11 +371,11 @@ class InvokeAiInstance:
ext = "bat" if OS == "Windows" else "sh" ext = "bat" if OS == "Windows" else "sh"
# scripts = ['invoke', 'update'] #scripts = ['invoke', 'update']
scripts = ["invoke"] scripts = ['invoke']
for script in scripts: for script in scripts:
src = Path(__file__).parent / ".." / "templates" / f"{script}.{ext}.in" src = Path(__file__).parent / '..' / "templates" / f"{script}.{ext}.in"
dest = self.runtime / f"{script}.{ext}" dest = self.runtime / f"{script}.{ext}"
shutil.copy(src, dest) shutil.copy(src, dest)
os.chmod(dest, 0o0755) os.chmod(dest, 0o0755)
@ -425,7 +420,11 @@ def set_sys_path(venv_path: Path) -> None:
# filter out any paths in sys.path that may be system- or user-wide # filter out any paths in sys.path that may be system- or user-wide
# but leave the temporary bootstrap virtualenv as it contains packages we # but leave the temporary bootstrap virtualenv as it contains packages we
# temporarily need at install time # temporarily need at install time
sys.path = list(filter(lambda p: not p.endswith("-packages") or p.find(BOOTSTRAP_VENV_PREFIX) != -1, sys.path)) sys.path = list(filter(
lambda p: not p.endswith("-packages")
or p.find(BOOTSTRAP_VENV_PREFIX) != -1,
sys.path
))
# determine site-packages/lib directory location for the venv # determine site-packages/lib directory location for the venv
lib = "Lib" if OS == "Windows" else f"lib/python{sys.version_info.major}.{sys.version_info.minor}" lib = "Lib" if OS == "Windows" else f"lib/python{sys.version_info.major}.{sys.version_info.minor}"
@ -434,7 +433,7 @@ def set_sys_path(venv_path: Path) -> None:
sys.path.append(str(Path(venv_path, lib, "site-packages").expanduser().resolve())) sys.path.append(str(Path(venv_path, lib, "site-packages").expanduser().resolve()))
def get_torch_source() -> (Union[str, None], str): def get_torch_source() -> (Union[str, None],str):
""" """
Determine the extra index URL for pip to use for torch installation. Determine the extra index URL for pip to use for torch installation.
This depends on the OS and the graphics accelerator in use. This depends on the OS and the graphics accelerator in use.
@ -455,19 +454,16 @@ def get_torch_source() -> (Union[str, None], str):
device = graphical_accelerator() device = graphical_accelerator()
url = None url = None
optional_modules = "[onnx]" optional_modules = None
if OS == "Linux": if OS == "Linux":
if device == "rocm": if device == "rocm":
url = "https://download.pytorch.org/whl/rocm5.4.2" url = "https://download.pytorch.org/whl/rocm5.4.2"
elif device == "cpu": elif device == "cpu":
url = "https://download.pytorch.org/whl/cpu" url = "https://download.pytorch.org/whl/cpu"
if device == "cuda": if device == 'cuda':
url = "https://download.pytorch.org/whl/cu117" url = 'https://download.pytorch.org/whl/cu117'
optional_modules = "[xformers,onnx-cuda]" optional_modules = '[xformers]'
if device == "cuda_and_dml":
url = "https://download.pytorch.org/whl/cu117"
optional_modules = "[xformers,onnx-directml]"
# in all other cases, Torch wheels should be coming from PyPi as of Torch 1.13 # in all other cases, Torch wheels should be coming from PyPi as of Torch 1.13

View File

@ -3,7 +3,6 @@ InvokeAI Installer
""" """
import argparse import argparse
import os
from pathlib import Path from pathlib import Path
from installer import Installer from installer import Installer
@ -16,7 +15,7 @@ if __name__ == "__main__":
dest="root", dest="root",
type=str, type=str,
help="Destination path for installation", help="Destination path for installation",
default=os.environ.get("INVOKEAI_ROOT") or "~/invokeai", default="~/invokeai",
) )
parser.add_argument( parser.add_argument(
"-y", "-y",

View File

@ -36,15 +36,13 @@ else:
def welcome(): def welcome():
@group() @group()
def text(): def text():
if (platform_specific := _platform_specific_help()) != "": if (platform_specific := _platform_specific_help()) != "":
yield platform_specific yield platform_specific
yield "" yield ""
yield Text.from_markup( yield Text.from_markup("Some of the installation steps take a long time to run. Please be patient. If the script appears to hang for more than 10 minutes, please interrupt with [i]Control-C[/] and retry.", justify="center")
"Some of the installation steps take a long time to run. Please be patient. If the script appears to hang for more than 10 minutes, please interrupt with [i]Control-C[/] and retry.",
justify="center",
)
console.rule() console.rule()
print( print(
@ -60,7 +58,6 @@ def welcome():
) )
console.line() console.line()
def confirm_install(dest: Path) -> bool: def confirm_install(dest: Path) -> bool:
if dest.exists(): if dest.exists():
print(f":exclamation: Directory {dest} already exists :exclamation:") print(f":exclamation: Directory {dest} already exists :exclamation:")
@ -95,6 +92,7 @@ def dest_path(dest=None) -> Path:
dest_confirmed = confirm_install(dest) dest_confirmed = confirm_install(dest)
while not dest_confirmed: while not dest_confirmed:
# if the given destination already exists, the starting point for browsing is its parent directory. # if the given destination already exists, the starting point for browsing is its parent directory.
# the user may have made a typo, or otherwise wants to place the root dir next to an existing one. # the user may have made a typo, or otherwise wants to place the root dir next to an existing one.
# if the destination dir does NOT exist, then the user must have changed their mind about the selection. # if the destination dir does NOT exist, then the user must have changed their mind about the selection.
@ -167,10 +165,6 @@ def graphical_accelerator():
"an [gold1 b]NVIDIA[/] GPU (using CUDA™)", "an [gold1 b]NVIDIA[/] GPU (using CUDA™)",
"cuda", "cuda",
) )
nvidia_with_dml = (
"an [gold1 b]NVIDIA[/] GPU (using CUDA™, and DirectML™ for ONNX) -- ALPHA",
"cuda_and_dml",
)
amd = ( amd = (
"an [gold1 b]AMD[/] GPU (using ROCm™)", "an [gold1 b]AMD[/] GPU (using ROCm™)",
"rocm", "rocm",
@ -185,7 +179,7 @@ def graphical_accelerator():
) )
if OS == "Windows": if OS == "Windows":
options = [nvidia, nvidia_with_dml, cpu] options = [nvidia, cpu]
if OS == "Linux": if OS == "Linux":
options = [nvidia, amd, cpu] options = [nvidia, amd, cpu]
elif OS == "Darwin": elif OS == "Darwin":
@ -306,20 +300,15 @@ def introduction() -> None:
) )
console.line(2) console.line(2)
def _platform_specific_help()->str:
def _platform_specific_help() -> str:
if OS == "Darwin": if OS == "Darwin":
text = Text.from_markup( text = Text.from_markup("""[b wheat1]macOS Users![/]\n\nPlease be sure you have the [b wheat1]Xcode command-line tools[/] installed before continuing.\nIf not, cancel with [i]Control-C[/] and follow the Xcode install instructions at [deep_sky_blue1]https://www.freecodecamp.org/news/install-xcode-command-line-tools/[/].""")
"""[b wheat1]macOS Users![/]\n\nPlease be sure you have the [b wheat1]Xcode command-line tools[/] installed before continuing.\nIf not, cancel with [i]Control-C[/] and follow the Xcode install instructions at [deep_sky_blue1]https://www.freecodecamp.org/news/install-xcode-command-line-tools/[/]."""
)
elif OS == "Windows": elif OS == "Windows":
text = Text.from_markup( text = Text.from_markup("""[b wheat1]Windows Users![/]\n\nBefore you start, please do the following:
"""[b wheat1]Windows Users![/]\n\nBefore you start, please do the following:
1. Double-click on the file [b wheat1]WinLongPathsEnabled.reg[/] in order to 1. Double-click on the file [b wheat1]WinLongPathsEnabled.reg[/] in order to
enable long path support on your system. enable long path support on your system.
2. Make sure you have the [b wheat1]Visual C++ core libraries[/] installed. If not, install from 2. Make sure you have the [b wheat1]Visual C++ core libraries[/] installed. If not, install from
[deep_sky_blue1]https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170[/]""" [deep_sky_blue1]https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170[/]""")
)
else: else:
text = "" text = ""
return text return text

View File

@ -19,7 +19,7 @@ echo 8. Open the developer console
echo 9. Update InvokeAI echo 9. Update InvokeAI
echo 10. Command-line help echo 10. Command-line help
echo Q - Quit echo Q - Quit
set /P choice="Please enter 1-10, Q: [1] " set /P choice="Please enter 1-10, Q: [2] "
if not defined choice set choice=1 if not defined choice set choice=1
IF /I "%choice%" == "1" ( IF /I "%choice%" == "1" (
echo Starting the InvokeAI browser-based UI.. echo Starting the InvokeAI browser-based UI..
@ -41,7 +41,7 @@ IF /I "%choice%" == "1" (
python .venv\Scripts\invokeai-configure.exe --skip-sd-weight --skip-support-models python .venv\Scripts\invokeai-configure.exe --skip-sd-weight --skip-support-models
) ELSE IF /I "%choice%" == "7" ( ) ELSE IF /I "%choice%" == "7" (
echo Running invokeai-configure... echo Running invokeai-configure...
python .venv\Scripts\invokeai-configure.exe --yes --skip-sd-weight python .venv\Scripts\invokeai-configure.exe --yes --default_only
) ELSE IF /I "%choice%" == "8" ( ) ELSE IF /I "%choice%" == "8" (
echo Developer Console echo Developer Console
echo Python command is: echo Python command is:

View File

@ -82,7 +82,7 @@ do_choice() {
7) 7)
clear clear
printf "Re-run the configure script to fix a broken install or to complete a major upgrade\n" printf "Re-run the configure script to fix a broken install or to complete a major upgrade\n"
invokeai-configure --root ${INVOKEAI_ROOT} --yes --default_only --skip-sd-weights invokeai-configure --root ${INVOKEAI_ROOT} --yes --default_only
;; ;;
8) 8)
clear clear

View File

@ -1,6 +1,5 @@
# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654)
from typing import Optional
from logging import Logger from logging import Logger
import os import os
from invokeai.app.services.board_image_record_storage import ( from invokeai.app.services.board_image_record_storage import (
@ -55,12 +54,11 @@ logger = InvokeAILogger.getLogger()
class ApiDependencies: class ApiDependencies:
"""Contains and initializes all dependencies for the API""" """Contains and initializes all dependencies for the API"""
invoker: Optional[Invoker] = None invoker: Invoker = None
@staticmethod @staticmethod
def initialize(config: InvokeAIAppConfig, event_handler_id: int, logger: Logger = logger): def initialize(config: InvokeAIAppConfig, event_handler_id: int, logger: Logger = logger):
logger.info(f"InvokeAI version {__version__}") logger.debug(f"InvokeAI version {__version__}")
logger.info(f"Root directory = {str(config.root_path)}")
logger.debug(f"Internet connectivity is {config.internet_available}") logger.debug(f"Internet connectivity is {config.internet_available}")
events = FastAPIEventService(event_handler_id) events = FastAPIEventService(event_handler_id)
@ -79,7 +77,9 @@ class ApiDependencies:
image_record_storage = SqliteImageRecordStorage(db_location) image_record_storage = SqliteImageRecordStorage(db_location)
image_file_storage = DiskImageFileStorage(f"{output_folder}/images") image_file_storage = DiskImageFileStorage(f"{output_folder}/images")
names = SimpleNameService() names = SimpleNameService()
latents = ForwardCacheLatentsStorage(DiskLatentsStorage(f"{output_folder}/latents")) latents = ForwardCacheLatentsStorage(
DiskLatentsStorage(f"{output_folder}/latents")
)
board_record_storage = SqliteBoardRecordStorage(db_location) board_record_storage = SqliteBoardRecordStorage(db_location)
board_image_record_storage = SqliteBoardImageRecordStorage(db_location) board_image_record_storage = SqliteBoardImageRecordStorage(db_location)
@ -124,7 +124,9 @@ class ApiDependencies:
boards=boards, boards=boards,
board_images=board_images, board_images=board_images,
queue=MemoryInvocationQueue(), queue=MemoryInvocationQueue(),
graph_library=SqliteItemStorage[LibraryGraph](filename=db_location, table_name="graphs"), graph_library=SqliteItemStorage[LibraryGraph](
filename=db_location, table_name="graphs"
),
graph_execution_manager=graph_execution_manager, graph_execution_manager=graph_execution_manager,
processor=DefaultInvocationProcessor(), processor=DefaultInvocationProcessor(),
configuration=config, configuration=config,

View File

@ -1,35 +1,9 @@
import typing
from enum import Enum
from fastapi import Body
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from pathlib import Path
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from invokeai.backend.image_util.patchmatch import PatchMatch from invokeai.backend.image_util.patchmatch import PatchMatch
from invokeai.backend.image_util.safety_checker import SafetyChecker
from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark
from invokeai.app.invocations.upscale import ESRGAN_MODELS
from invokeai.version import __version__ from invokeai.version import __version__
from ..dependencies import ApiDependencies
from invokeai.backend.util.logging import logging
class LogLevel(int, Enum):
NotSet = logging.NOTSET
Debug = logging.DEBUG
Info = logging.INFO
Warning = logging.WARNING
Error = logging.ERROR
Critical = logging.CRITICAL
class Upscaler(BaseModel):
upscaling_method: str = Field(description="Name of upscaling method")
upscaling_models: list[str] = Field(description="List of upscaling models for this method")
app_router = APIRouter(prefix="/v1/app", tags=["app"]) app_router = APIRouter(prefix="/v1/app", tags=["app"])
@ -43,63 +17,20 @@ class AppConfig(BaseModel):
"""App Config Response""" """App Config Response"""
infill_methods: list[str] = Field(description="List of available infill methods") infill_methods: list[str] = Field(description="List of available infill methods")
upscaling_methods: list[Upscaler] = Field(description="List of upscaling methods")
nsfw_methods: list[str] = Field(description="List of NSFW checking methods")
watermarking_methods: list[str] = Field(description="List of invisible watermark methods")
@app_router.get("/version", operation_id="app_version", status_code=200, response_model=AppVersion) @app_router.get(
"/version", operation_id="app_version", status_code=200, response_model=AppVersion
)
async def get_version() -> AppVersion: async def get_version() -> AppVersion:
return AppVersion(version=__version__) return AppVersion(version=__version__)
@app_router.get("/config", operation_id="get_config", status_code=200, response_model=AppConfig)
async def get_config() -> AppConfig:
infill_methods = ["tile"]
if PatchMatch.patchmatch_available():
infill_methods.append("patchmatch")
upscaling_models = []
for model in typing.get_args(ESRGAN_MODELS):
upscaling_models.append(str(Path(model).stem))
upscaler = Upscaler(upscaling_method="esrgan", upscaling_models=upscaling_models)
nsfw_methods = []
if SafetyChecker.safety_checker_available():
nsfw_methods.append("nsfw_checker")
watermarking_methods = []
if InvisibleWatermark.invisible_watermark_available():
watermarking_methods.append("invisible_watermark")
return AppConfig(
infill_methods=infill_methods,
upscaling_methods=[upscaler],
nsfw_methods=nsfw_methods,
watermarking_methods=watermarking_methods,
)
@app_router.get( @app_router.get(
"/logging", "/config", operation_id="get_config", status_code=200, response_model=AppConfig
operation_id="get_log_level",
responses={200: {"description": "The operation was successful"}},
response_model=LogLevel,
) )
async def get_log_level() -> LogLevel: async def get_config() -> AppConfig:
"""Returns the log level""" infill_methods = ['tile']
return LogLevel(ApiDependencies.invoker.services.logger.level) if PatchMatch.patchmatch_available():
infill_methods.append('patchmatch')
return AppConfig(infill_methods=infill_methods)
@app_router.post(
"/logging",
operation_id="set_log_level",
responses={200: {"description": "The operation was successful"}},
response_model=LogLevel,
)
async def set_log_level(
level: LogLevel = Body(description="New log verbosity level"),
) -> LogLevel:
"""Sets the log verbosity level"""
ApiDependencies.invoker.services.logger.setLevel(level)
return LogLevel(ApiDependencies.invoker.services.logger.level)

View File

@ -24,14 +24,11 @@ async def create_board_image(
): ):
"""Creates a board_image""" """Creates a board_image"""
try: try:
result = ApiDependencies.invoker.services.board_images.add_image_to_board( result = ApiDependencies.invoker.services.board_images.add_image_to_board(board_id=board_id, image_name=image_name)
board_id=board_id, image_name=image_name
)
return result return result
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail="Failed to add to board") raise HTTPException(status_code=500, detail="Failed to add to board")
@board_images_router.delete( @board_images_router.delete(
"/", "/",
operation_id="remove_board_image", operation_id="remove_board_image",
@ -46,9 +43,27 @@ async def remove_board_image(
): ):
"""Deletes a board_image""" """Deletes a board_image"""
try: try:
result = ApiDependencies.invoker.services.board_images.remove_image_from_board( result = ApiDependencies.invoker.services.board_images.remove_image_from_board(board_id=board_id, image_name=image_name)
board_id=board_id, image_name=image_name
)
return result return result
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail="Failed to update board") raise HTTPException(status_code=500, detail="Failed to update board")
@board_images_router.get(
"/{board_id}",
operation_id="list_board_images",
response_model=OffsetPaginatedResults[ImageDTO],
)
async def list_board_images(
board_id: str = Path(description="The id of the board"),
offset: int = Query(default=0, description="The page offset"),
limit: int = Query(default=10, description="The number of boards per page"),
) -> OffsetPaginatedResults[ImageDTO]:
"""Gets a list of images for a board"""
results = ApiDependencies.invoker.services.board_images.get_images_for_board(
board_id,
)
return results

View File

@ -1,26 +1,16 @@
from typing import Optional, Union from typing import Optional, Union
from fastapi import Body, HTTPException, Path, Query from fastapi import Body, HTTPException, Path, Query
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from pydantic import BaseModel, Field
from invokeai.app.services.board_record_storage import BoardChanges from invokeai.app.services.board_record_storage import BoardChanges
from invokeai.app.services.image_record_storage import OffsetPaginatedResults from invokeai.app.services.image_record_storage import OffsetPaginatedResults
from invokeai.app.services.models.board_record import BoardDTO from invokeai.app.services.models.board_record import BoardDTO
from ..dependencies import ApiDependencies from ..dependencies import ApiDependencies
boards_router = APIRouter(prefix="/v1/boards", tags=["boards"]) boards_router = APIRouter(prefix="/v1/boards", tags=["boards"])
class DeleteBoardResult(BaseModel):
board_id: str = Field(description="The id of the board that was deleted.")
deleted_board_images: list[str] = Field(
description="The image names of the board-images relationships that were deleted."
)
deleted_images: list[str] = Field(description="The names of the images that were deleted.")
@boards_router.post( @boards_router.post(
"/", "/",
operation_id="create_board", operation_id="create_board",
@ -71,42 +61,33 @@ async def update_board(
) -> BoardDTO: ) -> BoardDTO:
"""Updates a board""" """Updates a board"""
try: try:
result = ApiDependencies.invoker.services.boards.update(board_id=board_id, changes=changes) result = ApiDependencies.invoker.services.boards.update(
board_id=board_id, changes=changes
)
return result return result
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail="Failed to update board") raise HTTPException(status_code=500, detail="Failed to update board")
@boards_router.delete("/{board_id}", operation_id="delete_board", response_model=DeleteBoardResult) @boards_router.delete("/{board_id}", operation_id="delete_board")
async def delete_board( async def delete_board(
board_id: str = Path(description="The id of board to delete"), board_id: str = Path(description="The id of board to delete"),
include_images: Optional[bool] = Query(description="Permanently delete all images on the board", default=False), include_images: Optional[bool] = Query(
) -> DeleteBoardResult: description="Permanently delete all images on the board", default=False
),
) -> None:
"""Deletes a board""" """Deletes a board"""
try: try:
if include_images is True: if include_images is True:
deleted_images = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board( ApiDependencies.invoker.services.images.delete_images_on_board(
board_id=board_id board_id=board_id
) )
ApiDependencies.invoker.services.images.delete_images_on_board(board_id=board_id)
ApiDependencies.invoker.services.boards.delete(board_id=board_id) ApiDependencies.invoker.services.boards.delete(board_id=board_id)
return DeleteBoardResult(
board_id=board_id,
deleted_board_images=[],
deleted_images=deleted_images,
)
else: else:
deleted_board_images = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
board_id=board_id
)
ApiDependencies.invoker.services.boards.delete(board_id=board_id) ApiDependencies.invoker.services.boards.delete(board_id=board_id)
return DeleteBoardResult(
board_id=board_id,
deleted_board_images=deleted_board_images,
deleted_images=[],
)
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail="Failed to delete board") # TODO: Does this need any exception handling at all?
pass
@boards_router.get( @boards_router.get(
@ -117,7 +98,9 @@ async def delete_board(
async def list_boards( async def list_boards(
all: Optional[bool] = Query(default=None, description="Whether to list all boards"), all: Optional[bool] = Query(default=None, description="Whether to list all boards"),
offset: Optional[int] = Query(default=None, description="The page offset"), offset: Optional[int] = Query(default=None, description="The page offset"),
limit: Optional[int] = Query(default=None, description="The number of boards per page"), limit: Optional[int] = Query(
default=None, description="The number of boards per page"
),
) -> Union[OffsetPaginatedResults[BoardDTO], list[BoardDTO]]: ) -> Union[OffsetPaginatedResults[BoardDTO], list[BoardDTO]]:
"""Gets a list of boards""" """Gets a list of boards"""
if all: if all:
@ -132,19 +115,3 @@ async def list_boards(
status_code=400, status_code=400,
detail="Invalid request: Must provide either 'all' or both 'offset' and 'limit'", detail="Invalid request: Must provide either 'all' or both 'offset' and 'limit'",
) )
@boards_router.get(
"/{board_id}/image_names",
operation_id="list_all_board_image_names",
response_model=list[str],
)
async def list_all_board_image_names(
board_id: str = Path(description="The id of the board"),
) -> list[str]:
"""Gets a list of images for a board"""
image_names = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
board_id,
)
return image_names

View File

@ -1,7 +1,8 @@
import io import io
from typing import Optional from typing import Optional
from fastapi import Body, HTTPException, Path, Query, Request, Response, UploadFile from fastapi import (Body, HTTPException, Path, Query, Request, Response,
UploadFile)
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from PIL import Image from PIL import Image
@ -10,11 +11,9 @@ from invokeai.app.invocations.metadata import ImageMetadata
from invokeai.app.models.image import ImageCategory, ResourceOrigin from invokeai.app.models.image import ImageCategory, ResourceOrigin
from invokeai.app.services.image_record_storage import OffsetPaginatedResults from invokeai.app.services.image_record_storage import OffsetPaginatedResults
from invokeai.app.services.item_storage import PaginatedResults from invokeai.app.services.item_storage import PaginatedResults
from invokeai.app.services.models.image_record import ( from invokeai.app.services.models.image_record import (ImageDTO,
ImageDTO,
ImageRecordChanges, ImageRecordChanges,
ImageUrlsDTO, ImageUrlsDTO)
)
from ..dependencies import ApiDependencies from ..dependencies import ApiDependencies
@ -40,9 +39,9 @@ async def upload_image(
response: Response, response: Response,
image_category: ImageCategory = Query(description="The category of the image"), image_category: ImageCategory = Query(description="The category of the image"),
is_intermediate: bool = Query(description="Whether this is an intermediate image"), is_intermediate: bool = Query(description="Whether this is an intermediate image"),
board_id: Optional[str] = Query(default=None, description="The board to add this image to, if any"), session_id: Optional[str] = Query(
session_id: Optional[str] = Query(default=None, description="The session ID associated with this upload, if any"), default=None, description="The session ID associated with this upload, if any"
crop_visible: Optional[bool] = Query(default=False, description="Whether to crop the image"), ),
) -> ImageDTO: ) -> ImageDTO:
"""Uploads an image""" """Uploads an image"""
if not file.content_type.startswith("image"): if not file.content_type.startswith("image"):
@ -52,9 +51,6 @@ async def upload_image(
try: try:
pil_image = Image.open(io.BytesIO(contents)) pil_image = Image.open(io.BytesIO(contents))
if crop_visible:
bbox = pil_image.getbbox()
pil_image = pil_image.crop(bbox)
except: except:
# Error opening the image # Error opening the image
raise HTTPException(status_code=415, detail="Failed to read image") raise HTTPException(status_code=415, detail="Failed to read image")
@ -65,7 +61,6 @@ async def upload_image(
image_origin=ResourceOrigin.EXTERNAL, image_origin=ResourceOrigin.EXTERNAL,
image_category=image_category, image_category=image_category,
session_id=session_id, session_id=session_id,
board_id=board_id,
is_intermediate=is_intermediate, is_intermediate=is_intermediate,
) )
@ -90,18 +85,6 @@ async def delete_image(
pass pass
@images_router.post("/clear-intermediates", operation_id="clear_intermediates")
async def clear_intermediates() -> int:
"""Clears all intermediates"""
try:
count_deleted = ApiDependencies.invoker.services.images.delete_intermediates()
return count_deleted
except Exception as e:
raise HTTPException(status_code=500, detail="Failed to clear intermediates")
pass
@images_router.patch( @images_router.patch(
"/{image_name}", "/{image_name}",
operation_id="update_image", operation_id="update_image",
@ -109,7 +92,9 @@ async def clear_intermediates() -> int:
) )
async def update_image( async def update_image(
image_name: str = Path(description="The name of the image to update"), image_name: str = Path(description="The name of the image to update"),
image_changes: ImageRecordChanges = Body(description="The changes to apply to the image"), image_changes: ImageRecordChanges = Body(
description="The changes to apply to the image"
),
) -> ImageDTO: ) -> ImageDTO:
"""Updates an image""" """Updates an image"""
@ -134,7 +119,6 @@ async def get_image_dto(
except Exception as e: except Exception as e:
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
@images_router.get( @images_router.get(
"/{image_name}/metadata", "/{image_name}/metadata",
operation_id="get_image_metadata", operation_id="get_image_metadata",
@ -204,11 +188,15 @@ async def get_image_thumbnail(
"""Gets a thumbnail image file""" """Gets a thumbnail image file"""
try: try:
path = ApiDependencies.invoker.services.images.get_path(image_name, thumbnail=True) path = ApiDependencies.invoker.services.images.get_path(
image_name, thumbnail=True
)
if not ApiDependencies.invoker.services.images.validate_path(path): if not ApiDependencies.invoker.services.images.validate_path(path):
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
response = FileResponse(path, media_type="image/webp", content_disposition_type="inline") response = FileResponse(
path, media_type="image/webp", content_disposition_type="inline"
)
response.headers["Cache-Control"] = f"max-age={IMAGE_MAX_AGE}" response.headers["Cache-Control"] = f"max-age={IMAGE_MAX_AGE}"
return response return response
except Exception as e: except Exception as e:
@ -227,7 +215,9 @@ async def get_image_urls(
try: try:
image_url = ApiDependencies.invoker.services.images.get_url(image_name) image_url = ApiDependencies.invoker.services.images.get_url(image_name)
thumbnail_url = ApiDependencies.invoker.services.images.get_url(image_name, thumbnail=True) thumbnail_url = ApiDependencies.invoker.services.images.get_url(
image_name, thumbnail=True
)
return ImageUrlsDTO( return ImageUrlsDTO(
image_name=image_name, image_name=image_name,
image_url=image_url, image_url=image_url,
@ -243,12 +233,17 @@ async def get_image_urls(
response_model=OffsetPaginatedResults[ImageDTO], response_model=OffsetPaginatedResults[ImageDTO],
) )
async def list_image_dtos( async def list_image_dtos(
image_origin: Optional[ResourceOrigin] = Query(default=None, description="The origin of images to list."), image_origin: Optional[ResourceOrigin] = Query(
categories: Optional[list[ImageCategory]] = Query(default=None, description="The categories of image to include."), default=None, description="The origin of images to list"
is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate images."), ),
categories: Optional[list[ImageCategory]] = Query(
default=None, description="The categories of image to include"
),
is_intermediate: Optional[bool] = Query(
default=None, description="Whether to list intermediate images"
),
board_id: Optional[str] = Query( board_id: Optional[str] = Query(
default=None, default=None, description="The board id to filter by"
description="The board id to filter by. Use 'none' to find images without a board.",
), ),
offset: int = Query(default=0, description="The page offset"), offset: int = Query(default=0, description="The page offset"),
limit: int = Query(default=10, description="The number of images per page"), limit: int = Query(default=10, description="The number of images per page"),

View File

@ -14,7 +14,6 @@ from invokeai.backend.model_management.models import (
OPENAPI_MODEL_CONFIGS, OPENAPI_MODEL_CONFIGS,
SchedulerPredictionType, SchedulerPredictionType,
ModelNotFoundException, ModelNotFoundException,
InvalidModelException,
) )
from invokeai.backend.model_management import MergeInterpolationMethod from invokeai.backend.model_management import MergeInterpolationMethod
@ -28,42 +27,33 @@ ConvertModelResponse = Union[tuple(OPENAPI_MODEL_CONFIGS)]
MergeModelResponse = Union[tuple(OPENAPI_MODEL_CONFIGS)] MergeModelResponse = Union[tuple(OPENAPI_MODEL_CONFIGS)]
ImportModelAttributes = Union[tuple(OPENAPI_MODEL_CONFIGS)] ImportModelAttributes = Union[tuple(OPENAPI_MODEL_CONFIGS)]
class ModelsList(BaseModel): class ModelsList(BaseModel):
models: list[Union[tuple(OPENAPI_MODEL_CONFIGS)]] models: list[Union[tuple(OPENAPI_MODEL_CONFIGS)]]
@models_router.get( @models_router.get(
"/", "/",
operation_id="list_models", operation_id="list_models",
responses={200: {"model": ModelsList}}, responses={200: {"model": ModelsList }},
) )
async def list_models( async def list_models(
base_models: Optional[List[BaseModelType]] = Query(default=None, description="Base models to include"), base_model: Optional[BaseModelType] = Query(default=None, description="Base model"),
model_type: Optional[ModelType] = Query(default=None, description="The type of model to get"), model_type: Optional[ModelType] = Query(default=None, description="The type of model to get"),
) -> ModelsList: ) -> ModelsList:
"""Gets a list of models""" """Gets a list of models"""
if base_models and len(base_models) > 0: models_raw = ApiDependencies.invoker.services.model_manager.list_models(base_model, model_type)
models_raw = list() models = parse_obj_as(ModelsList, { "models": models_raw })
for base_model in base_models:
models_raw.extend(ApiDependencies.invoker.services.model_manager.list_models(base_model, model_type))
else:
models_raw = ApiDependencies.invoker.services.model_manager.list_models(None, model_type)
models = parse_obj_as(ModelsList, {"models": models_raw})
return models return models
@models_router.patch( @models_router.patch(
"/{base_model}/{model_type}/{model_name}", "/{base_model}/{model_type}/{model_name}",
operation_id="update_model", operation_id="update_model",
responses={ responses={200: {"description" : "The model was updated successfully"},
200: {"description": "The model was updated successfully"}, 400: {"description" : "Bad request"},
400: {"description": "Bad request"}, 404: {"description" : "The model could not be found"},
404: {"description": "The model could not be found"}, 409: {"description" : "There is already a model corresponding to the new name"},
409: {"description": "There is already a model corresponding to the new name"},
}, },
status_code=200, status_code = 200,
response_model=UpdateModelResponse, response_model = UpdateModelResponse,
) )
async def update_model( async def update_model(
base_model: BaseModelType = Path(description="Base model"), base_model: BaseModelType = Path(description="Base model"),
@ -71,41 +61,28 @@ async def update_model(
model_name: str = Path(description="model name"), model_name: str = Path(description="model name"),
info: Union[tuple(OPENAPI_MODEL_CONFIGS)] = Body(description="Model configuration"), info: Union[tuple(OPENAPI_MODEL_CONFIGS)] = Body(description="Model configuration"),
) -> UpdateModelResponse: ) -> UpdateModelResponse:
"""Update model contents with a new config. If the model name or base fields are changed, then the model is renamed.""" """ Update model contents with a new config. If the model name or base fields are changed, then the model is renamed. """
logger = ApiDependencies.invoker.services.logger logger = ApiDependencies.invoker.services.logger
try: try:
previous_info = ApiDependencies.invoker.services.model_manager.list_model(
model_name=model_name,
base_model=base_model,
model_type=model_type,
)
# rename operation requested # rename operation requested
if info.model_name != model_name or info.base_model != base_model: if info.model_name != model_name or info.base_model != base_model:
ApiDependencies.invoker.services.model_manager.rename_model( result = ApiDependencies.invoker.services.model_manager.rename_model(
base_model=base_model, base_model = base_model,
model_type=model_type, model_type = model_type,
model_name=model_name, model_name = model_name,
new_name=info.model_name, new_name = info.model_name,
new_base=info.base_model, new_base = info.base_model,
) )
logger.info(f"Successfully renamed {base_model.value}/{model_name}=>{info.base_model}/{info.model_name}") logger.debug(f'renaming result = {result}')
# update information to support an update of attributes logger.info(f'Successfully renamed {base_model}/{model_name}=>{info.base_model}/{info.model_name}')
model_name = info.model_name model_name = info.model_name
base_model = info.base_model base_model = info.base_model
new_info = ApiDependencies.invoker.services.model_manager.list_model(
ApiDependencies.invoker.services.model_manager.update_model(
model_name=model_name, model_name=model_name,
base_model=base_model, base_model=base_model,
model_type=model_type, model_type=model_type,
) model_attributes=info.dict()
if new_info.get("path") != previous_info.get(
"path"
): # model manager moved model path during rename - don't overwrite it
info.path = new_info.get("path")
ApiDependencies.invoker.services.model_manager.update_model(
model_name=model_name, base_model=base_model, model_type=model_type, model_attributes=info.dict()
) )
model_raw = ApiDependencies.invoker.services.model_manager.list_model( model_raw = ApiDependencies.invoker.services.model_manager.list_model(
@ -125,85 +102,86 @@ async def update_model(
return model_response return model_response
@models_router.post( @models_router.post(
"/import", "/import",
operation_id="import_model", operation_id="import_model",
responses={ responses= {
201: {"description": "The model imported successfully"}, 201: {"description" : "The model imported successfully"},
404: {"description": "The model could not be found"}, 404: {"description" : "The model could not be found"},
415: {"description": "Unrecognized file/folder format"}, 424: {"description" : "The model appeared to import successfully, but could not be found in the model manager"},
424: {"description": "The model appeared to import successfully, but could not be found in the model manager"}, 409: {"description" : "There is already a model corresponding to this path or repo_id"},
409: {"description": "There is already a model corresponding to this path or repo_id"},
}, },
status_code=201, status_code=201,
response_model=ImportModelResponse, response_model=ImportModelResponse
) )
async def import_model( async def import_model(
location: str = Body(description="A model path, repo_id or URL to import"), location: str = Body(description="A model path, repo_id or URL to import"),
prediction_type: Optional[Literal["v_prediction", "epsilon", "sample"]] = Body( prediction_type: Optional[Literal['v_prediction','epsilon','sample']] = \
description="Prediction type for SDv2 checkpoint files", default="v_prediction" Body(description='Prediction type for SDv2 checkpoint files', default="v_prediction"),
),
) -> ImportModelResponse: ) -> ImportModelResponse:
"""Add a model using its local path, repo_id, or remote URL. Model characteristics will be probed and configured automatically""" """ Add a model using its local path, repo_id, or remote URL. Model characteristics will be probed and configured automatically """
items_to_import = {location} items_to_import = {location}
prediction_types = {x.value: x for x in SchedulerPredictionType} prediction_types = { x.value: x for x in SchedulerPredictionType }
logger = ApiDependencies.invoker.services.logger logger = ApiDependencies.invoker.services.logger
try: try:
installed_models = ApiDependencies.invoker.services.model_manager.heuristic_import( installed_models = ApiDependencies.invoker.services.model_manager.heuristic_import(
items_to_import=items_to_import, prediction_type_helper=lambda x: prediction_types.get(prediction_type) items_to_import = items_to_import,
prediction_type_helper = lambda x: prediction_types.get(prediction_type)
) )
info = installed_models.get(location) info = installed_models.get(location)
if not info: if not info:
logger.error("Import failed") logger.error("Import failed")
raise HTTPException(status_code=415) raise HTTPException(status_code=424)
logger.info(f"Successfully imported {location}, got {info}") logger.info(f'Successfully imported {location}, got {info}')
model_raw = ApiDependencies.invoker.services.model_manager.list_model( model_raw = ApiDependencies.invoker.services.model_manager.list_model(
model_name=info.name, base_model=info.base_model, model_type=info.model_type model_name=info.name,
base_model=info.base_model,
model_type=info.model_type
) )
return parse_obj_as(ImportModelResponse, model_raw) return parse_obj_as(ImportModelResponse, model_raw)
except ModelNotFoundException as e: except ModelNotFoundException as e:
logger.error(str(e)) logger.error(str(e))
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except InvalidModelException as e:
logger.error(str(e))
raise HTTPException(status_code=415)
except ValueError as e: except ValueError as e:
logger.error(str(e)) logger.error(str(e))
raise HTTPException(status_code=409, detail=str(e)) raise HTTPException(status_code=409, detail=str(e))
@models_router.post( @models_router.post(
"/add", "/add",
operation_id="add_model", operation_id="add_model",
responses={ responses= {
201: {"description": "The model added successfully"}, 201: {"description" : "The model added successfully"},
404: {"description": "The model could not be found"}, 404: {"description" : "The model could not be found"},
424: {"description": "The model appeared to add successfully, but could not be found in the model manager"}, 424: {"description" : "The model appeared to add successfully, but could not be found in the model manager"},
409: {"description": "There is already a model corresponding to this path or repo_id"}, 409: {"description" : "There is already a model corresponding to this path or repo_id"},
}, },
status_code=201, status_code=201,
response_model=ImportModelResponse, response_model=ImportModelResponse
) )
async def add_model( async def add_model(
info: Union[tuple(OPENAPI_MODEL_CONFIGS)] = Body(description="Model configuration"), info: Union[tuple(OPENAPI_MODEL_CONFIGS)] = Body(description="Model configuration"),
) -> ImportModelResponse: ) -> ImportModelResponse:
"""Add a model using the configuration information appropriate for its type. Only local models can be added by path""" """ Add a model using the configuration information appropriate for its type. Only local models can be added by path"""
logger = ApiDependencies.invoker.services.logger logger = ApiDependencies.invoker.services.logger
try: try:
ApiDependencies.invoker.services.model_manager.add_model( ApiDependencies.invoker.services.model_manager.add_model(
info.model_name, info.base_model, info.model_type, model_attributes=info.dict() info.model_name,
info.base_model,
info.model_type,
model_attributes = info.dict()
) )
logger.info(f"Successfully added {info.model_name}") logger.info(f'Successfully added {info.model_name}')
model_raw = ApiDependencies.invoker.services.model_manager.list_model( model_raw = ApiDependencies.invoker.services.model_manager.list_model(
model_name=info.model_name, base_model=info.base_model, model_type=info.model_type model_name=info.model_name,
base_model=info.base_model,
model_type=info.model_type
) )
return parse_obj_as(ImportModelResponse, model_raw) return parse_obj_as(ImportModelResponse, model_raw)
except ModelNotFoundException as e: except ModelNotFoundException as e:
@ -217,9 +195,12 @@ async def add_model(
@models_router.delete( @models_router.delete(
"/{base_model}/{model_type}/{model_name}", "/{base_model}/{model_type}/{model_name}",
operation_id="del_model", operation_id="del_model",
responses={204: {"description": "Model deleted successfully"}, 404: {"description": "Model not found"}}, responses={
status_code=204, 204: { "description": "Model deleted successfully" },
response_model=None, 404: { "description": "Model not found" }
},
status_code = 204,
response_model = None,
) )
async def delete_model( async def delete_model(
base_model: BaseModelType = Path(description="Base model"), base_model: BaseModelType = Path(description="Base model"),
@ -230,8 +211,9 @@ async def delete_model(
logger = ApiDependencies.invoker.services.logger logger = ApiDependencies.invoker.services.logger
try: try:
ApiDependencies.invoker.services.model_manager.del_model( ApiDependencies.invoker.services.model_manager.del_model(model_name,
model_name, base_model=base_model, model_type=model_type base_model = base_model,
model_type = model_type
) )
logger.info(f"Deleted model: {model_name}") logger.info(f"Deleted model: {model_name}")
return Response(status_code=204) return Response(status_code=204)
@ -239,40 +221,36 @@ async def delete_model(
logger.error(str(e)) logger.error(str(e))
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
@models_router.put( @models_router.put(
"/convert/{base_model}/{model_type}/{model_name}", "/convert/{base_model}/{model_type}/{model_name}",
operation_id="convert_model", operation_id="convert_model",
responses={ responses={
200: {"description": "Model converted successfully"}, 200: { "description": "Model converted successfully" },
400: {"description": "Bad request"}, 400: {"description" : "Bad request" },
404: {"description": "Model not found"}, 404: { "description": "Model not found" },
}, },
status_code=200, status_code = 200,
response_model=ConvertModelResponse, response_model = ConvertModelResponse,
) )
async def convert_model( async def convert_model(
base_model: BaseModelType = Path(description="Base model"), base_model: BaseModelType = Path(description="Base model"),
model_type: ModelType = Path(description="The type of model"), model_type: ModelType = Path(description="The type of model"),
model_name: str = Path(description="model name"), model_name: str = Path(description="model name"),
convert_dest_directory: Optional[str] = Query( convert_dest_directory: Optional[str] = Query(default=None, description="Save the converted model to the designated directory"),
default=None, description="Save the converted model to the designated directory"
),
) -> ConvertModelResponse: ) -> ConvertModelResponse:
"""Convert a checkpoint model into a diffusers model, optionally saving to the indicated destination directory, or `models` if none.""" """Convert a checkpoint model into a diffusers model, optionally saving to the indicated destination directory, or `models` if none."""
logger = ApiDependencies.invoker.services.logger logger = ApiDependencies.invoker.services.logger
try: try:
logger.info(f"Converting model: {model_name}") logger.info(f"Converting model: {model_name}")
dest = pathlib.Path(convert_dest_directory) if convert_dest_directory else None dest = pathlib.Path(convert_dest_directory) if convert_dest_directory else None
ApiDependencies.invoker.services.model_manager.convert_model( ApiDependencies.invoker.services.model_manager.convert_model(model_name,
model_name, base_model = base_model,
base_model=base_model, model_type = model_type,
model_type=model_type, convert_dest_directory = dest,
convert_dest_directory=dest,
)
model_raw = ApiDependencies.invoker.services.model_manager.list_model(
model_name, base_model=base_model, model_type=model_type
) )
model_raw = ApiDependencies.invoker.services.model_manager.list_model(model_name,
base_model = base_model,
model_type = model_type)
response = parse_obj_as(ConvertModelResponse, model_raw) response = parse_obj_as(ConvertModelResponse, model_raw)
except ModelNotFoundException as e: except ModelNotFoundException as e:
raise HTTPException(status_code=404, detail=f"Model '{model_name}' not found: {str(e)}") raise HTTPException(status_code=404, detail=f"Model '{model_name}' not found: {str(e)}")
@ -280,67 +258,63 @@ async def convert_model(
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
return response return response
@models_router.get( @models_router.get(
"/search", "/search",
operation_id="search_for_models", operation_id="search_for_models",
responses={ responses={
200: {"description": "Directory searched successfully"}, 200: { "description": "Directory searched successfully" },
404: {"description": "Invalid directory path"}, 404: { "description": "Invalid directory path" },
}, },
status_code=200, status_code = 200,
response_model=List[pathlib.Path], response_model = List[pathlib.Path]
) )
async def search_for_models( async def search_for_models(
search_path: pathlib.Path = Query(description="Directory path to search for models"), search_path: pathlib.Path = Query(description="Directory path to search for models")
) -> List[pathlib.Path]: )->List[pathlib.Path]:
if not search_path.is_dir(): if not search_path.is_dir():
raise HTTPException( raise HTTPException(status_code=404, detail=f"The search path '{search_path}' does not exist or is not directory")
status_code=404, detail=f"The search path '{search_path}' does not exist or is not directory" return ApiDependencies.invoker.services.model_manager.search_for_models([search_path])
)
return ApiDependencies.invoker.services.model_manager.search_for_models(search_path)
@models_router.get( @models_router.get(
"/ckpt_confs", "/ckpt_confs",
operation_id="list_ckpt_configs", operation_id="list_ckpt_configs",
responses={ responses={
200: {"description": "paths retrieved successfully"}, 200: { "description" : "paths retrieved successfully" },
}, },
status_code=200, status_code = 200,
response_model=List[pathlib.Path], response_model = List[pathlib.Path]
) )
async def list_ckpt_configs() -> List[pathlib.Path]: async def list_ckpt_configs(
)->List[pathlib.Path]:
"""Return a list of the legacy checkpoint configuration files stored in `ROOT/configs/stable-diffusion`, relative to ROOT.""" """Return a list of the legacy checkpoint configuration files stored in `ROOT/configs/stable-diffusion`, relative to ROOT."""
return ApiDependencies.invoker.services.model_manager.list_checkpoint_configs() return ApiDependencies.invoker.services.model_manager.list_checkpoint_configs()
@models_router.post( @models_router.get(
"/sync", "/sync",
operation_id="sync_to_config", operation_id="sync_to_config",
responses={ responses={
201: {"description": "synchronization successful"}, 201: { "description": "synchronization successful" },
}, },
status_code=201, status_code = 201,
response_model=bool, response_model = None
) )
async def sync_to_config() -> bool: async def sync_to_config(
)->None:
"""Call after making changes to models.yaml, autoimport directories or models directory to synchronize """Call after making changes to models.yaml, autoimport directories or models directory to synchronize
in-memory data structures with disk data structures.""" in-memory data structures with disk data structures."""
ApiDependencies.invoker.services.model_manager.sync_to_config() return ApiDependencies.invoker.services.model_manager.sync_to_config()
return True
@models_router.put( @models_router.put(
"/merge/{base_model}", "/merge/{base_model}",
operation_id="merge_models", operation_id="merge_models",
responses={ responses={
200: {"description": "Model converted successfully"}, 200: { "description": "Model converted successfully" },
400: {"description": "Incompatible models"}, 400: { "description": "Incompatible models" },
404: {"description": "One or more models not found"}, 404: { "description": "One or more models not found" },
}, },
status_code=200, status_code = 200,
response_model=MergeModelResponse, response_model = MergeModelResponse,
) )
async def merge_models( async def merge_models(
base_model: BaseModelType = Path(description="Base model"), base_model: BaseModelType = Path(description="Base model"),
@ -348,32 +322,25 @@ async def merge_models(
merged_model_name: Optional[str] = Body(description="Name of destination model"), merged_model_name: Optional[str] = Body(description="Name of destination model"),
alpha: Optional[float] = Body(description="Alpha weighting strength to apply to 2d and 3d models", default=0.5), alpha: Optional[float] = Body(description="Alpha weighting strength to apply to 2d and 3d models", default=0.5),
interp: Optional[MergeInterpolationMethod] = Body(description="Interpolation method"), interp: Optional[MergeInterpolationMethod] = Body(description="Interpolation method"),
force: Optional[bool] = Body( force: Optional[bool] = Body(description="Force merging of models created with different versions of diffusers", default=False),
description="Force merging of models created with different versions of diffusers", default=False merge_dest_directory: Optional[str] = Body(description="Save the merged model to the designated directory (with 'merged_model_name' appended)", default=None)
),
merge_dest_directory: Optional[str] = Body(
description="Save the merged model to the designated directory (with 'merged_model_name' appended)",
default=None,
),
) -> MergeModelResponse: ) -> MergeModelResponse:
"""Convert a checkpoint model into a diffusers model""" """Convert a checkpoint model into a diffusers model"""
logger = ApiDependencies.invoker.services.logger logger = ApiDependencies.invoker.services.logger
try: try:
logger.info(f"Merging models: {model_names} into {merge_dest_directory or '<MODELS>'}/{merged_model_name}") logger.info(f"Merging models: {model_names} into {merge_dest_directory or '<MODELS>'}/{merged_model_name}")
dest = pathlib.Path(merge_dest_directory) if merge_dest_directory else None dest = pathlib.Path(merge_dest_directory) if merge_dest_directory else None
result = ApiDependencies.invoker.services.model_manager.merge_models( result = ApiDependencies.invoker.services.model_manager.merge_models(model_names,
model_names,
base_model, base_model,
merged_model_name=merged_model_name or "+".join(model_names), merged_model_name=merged_model_name or "+".join(model_names),
alpha=alpha, alpha=alpha,
interp=interp, interp=interp,
force=force, force=force,
merge_dest_directory=dest, merge_dest_directory = dest
) )
model_raw = ApiDependencies.invoker.services.model_manager.list_model( model_raw = ApiDependencies.invoker.services.model_manager.list_model(result.name,
result.name, base_model = base_model,
base_model=base_model, model_type = ModelType.Main,
model_type=ModelType.Main,
) )
response = parse_obj_as(ConvertModelResponse, model_raw) response = parse_obj_as(ConvertModelResponse, model_raw)
except ModelNotFoundException: except ModelNotFoundException:
@ -381,3 +348,50 @@ async def merge_models(
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
return response return response
# The rename operation is now supported by update_model and no longer needs to be
# a standalone route.
# @models_router.post(
# "/rename/{base_model}/{model_type}/{model_name}",
# operation_id="rename_model",
# responses= {
# 201: {"description" : "The model was renamed successfully"},
# 404: {"description" : "The model could not be found"},
# 409: {"description" : "There is already a model corresponding to the new name"},
# },
# status_code=201,
# response_model=ImportModelResponse
# )
# async def rename_model(
# base_model: BaseModelType = Path(description="Base model"),
# model_type: ModelType = Path(description="The type of model"),
# model_name: str = Path(description="current model name"),
# new_name: Optional[str] = Query(description="new model name", default=None),
# new_base: Optional[BaseModelType] = Query(description="new model base", default=None),
# ) -> ImportModelResponse:
# """ Rename a model"""
# logger = ApiDependencies.invoker.services.logger
# try:
# result = ApiDependencies.invoker.services.model_manager.rename_model(
# base_model = base_model,
# model_type = model_type,
# model_name = model_name,
# new_name = new_name,
# new_base = new_base,
# )
# logger.debug(result)
# logger.info(f'Successfully renamed {model_name}=>{new_name}')
# model_raw = ApiDependencies.invoker.services.model_manager.list_model(
# model_name=new_name or model_name,
# base_model=new_base or base_model,
# model_type=model_type
# )
# return parse_obj_as(ImportModelResponse, model_raw)
# except ModelNotFoundException as e:
# logger.error(str(e))
# raise HTTPException(status_code=404, detail=str(e))
# except ValueError as e:
# logger.error(str(e))
# raise HTTPException(status_code=409, detail=str(e))

View File

@ -30,7 +30,9 @@ session_router = APIRouter(prefix="/v1/sessions", tags=["sessions"])
}, },
) )
async def create_session( async def create_session(
graph: Optional[Graph] = Body(default=None, description="The graph to initialize the session with") graph: Optional[Graph] = Body(
default=None, description="The graph to initialize the session with"
)
) -> GraphExecutionState: ) -> GraphExecutionState:
"""Creates a new session, optionally initializing it with an invocation graph""" """Creates a new session, optionally initializing it with an invocation graph"""
session = ApiDependencies.invoker.create_execution_state(graph) session = ApiDependencies.invoker.create_execution_state(graph)
@ -49,9 +51,13 @@ async def list_sessions(
) -> PaginatedResults[GraphExecutionState]: ) -> PaginatedResults[GraphExecutionState]:
"""Gets a list of sessions, optionally searching""" """Gets a list of sessions, optionally searching"""
if query == "": if query == "":
result = ApiDependencies.invoker.services.graph_execution_manager.list(page, per_page) result = ApiDependencies.invoker.services.graph_execution_manager.list(
page, per_page
)
else: else:
result = ApiDependencies.invoker.services.graph_execution_manager.search(query, page, per_page) result = ApiDependencies.invoker.services.graph_execution_manager.search(
query, page, per_page
)
return result return result
@ -85,9 +91,9 @@ async def get_session(
) )
async def add_node( async def add_node(
session_id: str = Path(description="The id of the session"), session_id: str = Path(description="The id of the session"),
node: Annotated[Union[BaseInvocation.get_invocations()], Field(discriminator="type")] = Body( # type: ignore node: Annotated[
description="The node to add" Union[BaseInvocation.get_invocations()], Field(discriminator="type") # type: ignore
), ] = Body(description="The node to add"),
) -> str: ) -> str:
"""Adds a node to the graph""" """Adds a node to the graph"""
session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id) session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id)
@ -118,9 +124,9 @@ async def add_node(
async def update_node( async def update_node(
session_id: str = Path(description="The id of the session"), session_id: str = Path(description="The id of the session"),
node_path: str = Path(description="The path to the node in the graph"), node_path: str = Path(description="The path to the node in the graph"),
node: Annotated[Union[BaseInvocation.get_invocations()], Field(discriminator="type")] = Body( # type: ignore node: Annotated[
description="The new node" Union[BaseInvocation.get_invocations()], Field(discriminator="type") # type: ignore
), ] = Body(description="The new node"),
) -> GraphExecutionState: ) -> GraphExecutionState:
"""Updates a node in the graph and removes all linked edges""" """Updates a node in the graph and removes all linked edges"""
session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id) session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id)
@ -224,7 +230,7 @@ async def delete_edge(
try: try:
edge = Edge( edge = Edge(
source=EdgeConnection(node_id=from_node_id, field=from_field), source=EdgeConnection(node_id=from_node_id, field=from_field),
destination=EdgeConnection(node_id=to_node_id, field=to_field), destination=EdgeConnection(node_id=to_node_id, field=to_field)
) )
session.delete_edge(edge) session.delete_edge(edge)
ApiDependencies.invoker.services.graph_execution_manager.set( ApiDependencies.invoker.services.graph_execution_manager.set(
@ -249,7 +255,9 @@ async def delete_edge(
) )
async def invoke_session( async def invoke_session(
session_id: str = Path(description="The id of the session to invoke"), session_id: str = Path(description="The id of the session to invoke"),
all: bool = Query(default=False, description="Whether or not to invoke all remaining invocations"), all: bool = Query(
default=False, description="Whether or not to invoke all remaining invocations"
),
) -> Response: ) -> Response:
"""Invokes a session""" """Invokes a session"""
session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id) session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id)
@ -266,7 +274,9 @@ async def invoke_session(
@session_router.delete( @session_router.delete(
"/{session_id}/invoke", "/{session_id}/invoke",
operation_id="cancel_session_invoke", operation_id="cancel_session_invoke",
responses={202: {"description": "The invocation is canceled"}}, responses={
202: {"description": "The invocation is canceled"}
},
) )
async def cancel_session_invoke( async def cancel_session_invoke(
session_id: str = Path(description="The id of the session to cancel"), session_id: str = Path(description="The id of the session to cancel"),

View File

@ -16,7 +16,9 @@ class SocketIO:
self.__sio.on("subscribe", handler=self._handle_sub) self.__sio.on("subscribe", handler=self._handle_sub)
self.__sio.on("unsubscribe", handler=self._handle_unsub) self.__sio.on("unsubscribe", handler=self._handle_unsub)
local_handler.register(event_name=EventServiceBase.session_event, _func=self._handle_session_event) local_handler.register(
event_name=EventServiceBase.session_event, _func=self._handle_session_event
)
async def _handle_session_event(self, event: Event): async def _handle_session_event(self, event: Event):
await self.__sio.emit( await self.__sio.emit(

View File

@ -3,9 +3,7 @@ import asyncio
import sys import sys
from inspect import signature from inspect import signature
import logging
import uvicorn import uvicorn
import socket
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@ -17,10 +15,9 @@ from fastapi_events.middleware import EventHandlerASGIMiddleware
from pathlib import Path from pathlib import Path
from pydantic.schema import schema from pydantic.schema import schema
# This should come early so that modules can log their initialization properly #This should come early so that modules can log their initialization properly
from .services.config import InvokeAIAppConfig from .services.config import InvokeAIAppConfig
from ..backend.util.logging import InvokeAILogger from ..backend.util.logging import InvokeAILogger
app_config = InvokeAIAppConfig.get_config() app_config = InvokeAIAppConfig.get_config()
app_config.parse_args() app_config.parse_args()
logger = InvokeAILogger.getLogger(config=app_config) logger = InvokeAILogger.getLogger(config=app_config)
@ -29,7 +26,7 @@ from invokeai.version.invokeai_version import __version__
# we call this early so that the message appears before # we call this early so that the message appears before
# other invokeai initialization messages # other invokeai initialization messages
if app_config.version: if app_config.version:
print(f"InvokeAI version {__version__}") print(f'InvokeAI version {__version__}')
sys.exit(0) sys.exit(0)
import invokeai.frontend.web as web_dir import invokeai.frontend.web as web_dir
@ -43,14 +40,13 @@ from .invocations.baseinvocation import BaseInvocation
import torch import torch
import invokeai.backend.util.hotfixes import invokeai.backend.util.hotfixes
if torch.backends.mps.is_available(): if torch.backends.mps.is_available():
import invokeai.backend.util.mps_fixes import invokeai.backend.util.mps_fixes
# fix for windows mimetypes registry entries being borked # fix for windows mimetypes registry entries being borked
# see https://github.com/invoke-ai/InvokeAI/discussions/3684#discussioncomment-6391352 # see https://github.com/invoke-ai/InvokeAI/discussions/3684#discussioncomment-6391352
mimetypes.add_type("application/javascript", ".js") mimetypes.add_type('application/javascript', '.js')
mimetypes.add_type("text/css", ".css") mimetypes.add_type('text/css', '.css')
# Create the app # Create the app
# TODO: create this all in a method so configuration/etc. can be passed in? # TODO: create this all in a method so configuration/etc. can be passed in?
@ -60,13 +56,14 @@ app = FastAPI(title="Invoke AI", docs_url=None, redoc_url=None)
event_handler_id: int = id(app) event_handler_id: int = id(app)
app.add_middleware( app.add_middleware(
EventHandlerASGIMiddleware, EventHandlerASGIMiddleware,
handlers=[local_handler], # TODO: consider doing this in services to support different configurations handlers=[
local_handler
], # TODO: consider doing this in services to support different configurations
middleware_id=event_handler_id, middleware_id=event_handler_id,
) )
socket_io = SocketIO(app) socket_io = SocketIO(app)
# Add startup event to load dependencies # Add startup event to load dependencies
@app.on_event("startup") @app.on_event("startup")
async def startup_event(): async def startup_event():
@ -78,7 +75,9 @@ async def startup_event():
allow_headers=app_config.allow_headers, allow_headers=app_config.allow_headers,
) )
ApiDependencies.initialize(config=app_config, event_handler_id=event_handler_id, logger=logger) ApiDependencies.initialize(
config=app_config, event_handler_id=event_handler_id, logger=logger
)
# Shut down threads # Shut down threads
@ -103,8 +102,7 @@ app.include_router(boards.boards_router, prefix="/api")
app.include_router(board_images.board_images_router, prefix="/api") app.include_router(board_images.board_images_router, prefix="/api")
app.include_router(app_info.app_router, prefix="/api") app.include_router(app_info.app_router, prefix='/api')
# Build a custom OpenAPI to include all outputs # Build a custom OpenAPI to include all outputs
# TODO: can outputs be included on metadata of invocation schemas somehow? # TODO: can outputs be included on metadata of invocation schemas somehow?
@ -145,7 +143,6 @@ def custom_openapi():
invoker_schema["output"] = outputs_ref invoker_schema["output"] = outputs_ref
from invokeai.backend.model_management.models import get_model_config_enums from invokeai.backend.model_management.models import get_model_config_enums
for model_config_format_enum in set(get_model_config_enums()): for model_config_format_enum in set(get_model_config_enums()):
name = model_config_format_enum.__qualname__ name = model_config_format_enum.__qualname__
@ -168,8 +165,7 @@ def custom_openapi():
app.openapi = custom_openapi app.openapi = custom_openapi
# Override API doc favicons # Override API doc favicons
app.mount("/static", StaticFiles(directory=Path(web_dir.__path__[0], "static/dream_web")), name="static") app.mount("/static", StaticFiles(directory=Path(web_dir.__path__[0], 'static/dream_web')), name="static")
@app.get("/docs", include_in_schema=False) @app.get("/docs", include_in_schema=False)
def overridden_swagger(): def overridden_swagger():
@ -190,48 +186,19 @@ def overridden_redoc():
# Must mount *after* the other routes else it borks em # Must mount *after* the other routes else it borks em
app.mount("/", StaticFiles(directory=Path(web_dir.__path__[0], "dist"), html=True), name="ui") app.mount("/",
StaticFiles(directory=Path(web_dir.__path__[0],"dist"),
html=True
), name="ui"
)
def invoke_api(): def invoke_api():
def find_port(port: int):
"""Find a port not in use starting at given port"""
# Taken from https://waylonwalker.com/python-find-available-port/, thanks Waylon!
# https://github.com/WaylonWalker
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
if s.connect_ex(("localhost", port)) == 0:
return find_port(port=port + 1)
else:
return port
from invokeai.backend.install.check_root import check_invokeai_root
check_invokeai_root(app_config) # note, may exit with an exception if root not set up
port = find_port(app_config.port)
if port != app_config.port:
logger.warn(f"Port {app_config.port} in use, using port {port}")
# Start our own event loop for eventing usage # Start our own event loop for eventing usage
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
config = uvicorn.Config( config = uvicorn.Config(app=app, host=app_config.host, port=app_config.port, loop=loop)
app=app, # Use access_log to turn off logging
host=app_config.host,
port=port,
loop=loop,
log_level=app_config.log_level,
)
server = uvicorn.Server(config) server = uvicorn.Server(config)
# replace uvicorn's loggers with InvokeAI's for consistent appearance
for logname in ["uvicorn.access", "uvicorn"]:
l = logging.getLogger(logname)
l.handlers.clear()
for ch in logger.handlers:
l.addHandler(ch)
loop.run_until_complete(server.serve()) loop.run_until_complete(server.serve())
if __name__ == "__main__": if __name__ == "__main__":
invoke_api() invoke_api()

View File

@ -14,14 +14,8 @@ from ..services.graph import GraphExecutionState, LibraryGraph, Edge
from ..services.invoker import Invoker from ..services.invoker import Invoker
def add_field_argument(command_parser, name: str, field, default_override=None): def add_field_argument(command_parser, name: str, field, default_override = None):
default = ( default = default_override if default_override is not None else field.default if field.default_factory is None else field.default_factory()
default_override
if default_override is not None
else field.default
if field.default_factory is None
else field.default_factory()
)
if get_origin(field.type_) == Literal: if get_origin(field.type_) == Literal:
allowed_values = get_args(field.type_) allowed_values = get_args(field.type_)
allowed_types = set() allowed_types = set()
@ -53,8 +47,8 @@ def add_parsers(
commands: list[type], commands: list[type],
command_field: str = "type", command_field: str = "type",
exclude_fields: list[str] = ["id", "type"], exclude_fields: list[str] = ["id", "type"],
add_arguments: Union[Callable[[argparse.ArgumentParser], None], None] = None, add_arguments: Union[Callable[[argparse.ArgumentParser], None],None] = None
): ):
"""Adds parsers for each command to the subparsers""" """Adds parsers for each command to the subparsers"""
# Create subparsers for each command # Create subparsers for each command
@ -76,7 +70,9 @@ def add_parsers(
def add_graph_parsers( def add_graph_parsers(
subparsers, graphs: list[LibraryGraph], add_arguments: Union[Callable[[argparse.ArgumentParser], None], None] = None subparsers,
graphs: list[LibraryGraph],
add_arguments: Union[Callable[[argparse.ArgumentParser], None], None] = None
): ):
for graph in graphs: for graph in graphs:
command_parser = subparsers.add_parser(graph.name, help=graph.description) command_parser = subparsers.add_parser(graph.name, help=graph.description)
@ -132,7 +128,6 @@ class CliContext:
class ExitCli(Exception): class ExitCli(Exception):
"""Exception to exit the CLI""" """Exception to exit the CLI"""
pass pass
@ -160,7 +155,7 @@ class BaseCommand(ABC, BaseModel):
@classmethod @classmethod
def get_commands_map(cls): def get_commands_map(cls):
# Get the type strings out of the literals and into a dictionary # Get the type strings out of the literals and into a dictionary
return dict(map(lambda t: (get_args(get_type_hints(t)["type"])[0], t), BaseCommand.get_all_subclasses())) return dict(map(lambda t: (get_args(get_type_hints(t)['type'])[0], t),BaseCommand.get_all_subclasses()))
@abstractmethod @abstractmethod
def run(self, context: CliContext) -> None: def run(self, context: CliContext) -> None:
@ -170,8 +165,7 @@ class BaseCommand(ABC, BaseModel):
class ExitCommand(BaseCommand): class ExitCommand(BaseCommand):
"""Exits the CLI""" """Exits the CLI"""
type: Literal['exit'] = 'exit'
type: Literal["exit"] = "exit"
def run(self, context: CliContext) -> None: def run(self, context: CliContext) -> None:
raise ExitCli() raise ExitCli()
@ -179,8 +173,7 @@ class ExitCommand(BaseCommand):
class HelpCommand(BaseCommand): class HelpCommand(BaseCommand):
"""Shows help""" """Shows help"""
type: Literal['help'] = 'help'
type: Literal["help"] = "help"
def run(self, context: CliContext) -> None: def run(self, context: CliContext) -> None:
context.parser.print_help() context.parser.print_help()
@ -190,7 +183,11 @@ def get_graph_execution_history(
graph_execution_state: GraphExecutionState, graph_execution_state: GraphExecutionState,
) -> Iterable[str]: ) -> Iterable[str]:
"""Gets the history of fully-executed invocations for a graph execution""" """Gets the history of fully-executed invocations for a graph execution"""
return (n for n in reversed(graph_execution_state.executed_history) if n in graph_execution_state.graph.nodes) return (
n
for n in reversed(graph_execution_state.executed_history)
if n in graph_execution_state.graph.nodes
)
def get_invocation_command(invocation) -> str: def get_invocation_command(invocation) -> str:
@ -221,8 +218,7 @@ def get_invocation_command(invocation) -> str:
class HistoryCommand(BaseCommand): class HistoryCommand(BaseCommand):
"""Shows the invocation history""" """Shows the invocation history"""
type: Literal['history'] = 'history'
type: Literal["history"] = "history"
# Inputs # Inputs
# fmt: off # fmt: off
@ -239,8 +235,7 @@ class HistoryCommand(BaseCommand):
class SetDefaultCommand(BaseCommand): class SetDefaultCommand(BaseCommand):
"""Sets a default value for a field""" """Sets a default value for a field"""
type: Literal['default'] = 'default'
type: Literal["default"] = "default"
# Inputs # Inputs
# fmt: off # fmt: off
@ -258,8 +253,7 @@ class SetDefaultCommand(BaseCommand):
class DrawGraphCommand(BaseCommand): class DrawGraphCommand(BaseCommand):
"""Debugs a graph""" """Debugs a graph"""
type: Literal['draw_graph'] = 'draw_graph'
type: Literal["draw_graph"] = "draw_graph"
def run(self, context: CliContext) -> None: def run(self, context: CliContext) -> None:
session: GraphExecutionState = context.invoker.services.graph_execution_manager.get(context.session.id) session: GraphExecutionState = context.invoker.services.graph_execution_manager.get(context.session.id)
@ -277,8 +271,7 @@ class DrawGraphCommand(BaseCommand):
class DrawExecutionGraphCommand(BaseCommand): class DrawExecutionGraphCommand(BaseCommand):
"""Debugs an execution graph""" """Debugs an execution graph"""
type: Literal['draw_xgraph'] = 'draw_xgraph'
type: Literal["draw_xgraph"] = "draw_xgraph"
def run(self, context: CliContext) -> None: def run(self, context: CliContext) -> None:
session: GraphExecutionState = context.invoker.services.graph_execution_manager.get(context.session.id) session: GraphExecutionState = context.invoker.services.graph_execution_manager.get(context.session.id)
@ -293,7 +286,6 @@ class DrawExecutionGraphCommand(BaseCommand):
plt.axis("off") plt.axis("off")
plt.show() plt.show()
class SortedHelpFormatter(argparse.HelpFormatter): class SortedHelpFormatter(argparse.HelpFormatter):
def _iter_indented_subactions(self, action): def _iter_indented_subactions(self, action):
try: try:

View File

@ -19,8 +19,8 @@ from ..services.invocation_services import InvocationServices
# singleton object, class variable # singleton object, class variable
completer = None completer = None
class Completer(object): class Completer(object):
def __init__(self, model_manager: ModelManager): def __init__(self, model_manager: ModelManager):
self.commands = self.get_commands() self.commands = self.get_commands()
self.matches = None self.matches = None
@ -56,17 +56,17 @@ class Completer(object):
return match return match
@classmethod @classmethod
def get_commands(self) -> List[object]: def get_commands(self)->List[object]:
""" """
Return a list of all the client commands and invocations. Return a list of all the client commands and invocations.
""" """
return BaseCommand.get_commands() + BaseInvocation.get_invocations() return BaseCommand.get_commands() + BaseInvocation.get_invocations()
def get_current_command(self, buffer: str) -> tuple[str, str]: def get_current_command(self, buffer: str)->tuple[str, str]:
""" """
Parse the readline buffer to find the most recent command and its switch. Parse the readline buffer to find the most recent command and its switch.
""" """
if len(buffer) == 0: if len(buffer)==0:
return None, None return None, None
tokens = shlex.split(buffer) tokens = shlex.split(buffer)
command = None command = None
@ -78,11 +78,11 @@ class Completer(object):
else: else:
switch = t switch = t
# don't try to autocomplete switches that are already complete # don't try to autocomplete switches that are already complete
if switch and buffer.endswith(" "): if switch and buffer.endswith(' '):
switch = None switch=None
return command or "", switch or "" return command or '', switch or ''
def parse_commands(self) -> Dict[str, List[str]]: def parse_commands(self)->Dict[str, List[str]]:
""" """
Return a dict in which the keys are the command name Return a dict in which the keys are the command name
and the values are the parameters the command takes. and the values are the parameters the command takes.
@ -90,11 +90,11 @@ class Completer(object):
result = dict() result = dict()
for command in self.commands: for command in self.commands:
hints = get_type_hints(command) hints = get_type_hints(command)
name = get_args(hints["type"])[0] name = get_args(hints['type'])[0]
result.update({name: hints}) result.update({name:hints})
return result return result
def get_command_options(self, command: str, switch: str) -> List[str]: def get_command_options(self, command: str, switch: str)->List[str]:
""" """
Return all the parameters that can be passed to the command as Return all the parameters that can be passed to the command as
command-line switches. Returns None if the command is unrecognized. command-line switches. Returns None if the command is unrecognized.
@ -105,28 +105,25 @@ class Completer(object):
# handle switches in the format "-foo=bar" # handle switches in the format "-foo=bar"
argument = None argument = None
if switch and "=" in switch: if switch and '=' in switch:
switch, argument = switch.split("=") switch, argument = switch.split('=')
parameter = switch.strip("-") parameter = switch.strip('-')
if parameter in parsed_commands[command]: if parameter in parsed_commands[command]:
if argument is None: if argument is None:
return self.get_parameter_options(parameter, parsed_commands[command][parameter]) return self.get_parameter_options(parameter, parsed_commands[command][parameter])
else: else:
return [ return [f"--{parameter}={x}" for x in self.get_parameter_options(parameter, parsed_commands[command][parameter])]
f"--{parameter}={x}"
for x in self.get_parameter_options(parameter, parsed_commands[command][parameter])
]
else: else:
return [f"--{x}" for x in parsed_commands[command].keys()] return [f"--{x}" for x in parsed_commands[command].keys()]
def get_parameter_options(self, parameter: str, typehint) -> List[str]: def get_parameter_options(self, parameter: str, typehint)->List[str]:
""" """
Given a parameter type (such as Literal), offers autocompletions. Given a parameter type (such as Literal), offers autocompletions.
""" """
if get_origin(typehint) == Literal: if get_origin(typehint) == Literal:
return get_args(typehint) return get_args(typehint)
if parameter == "model": if parameter == 'model':
return self.manager.model_names() return self.manager.model_names()
def _pre_input_hook(self): def _pre_input_hook(self):
@ -135,7 +132,6 @@ class Completer(object):
readline.redisplay() readline.redisplay()
self.linebuffer = None self.linebuffer = None
def set_autocompleter(services: InvocationServices) -> Completer: def set_autocompleter(services: InvocationServices) -> Completer:
global completer global completer
@ -166,6 +162,8 @@ def set_autocompleter(services: InvocationServices) -> Completer:
pass pass
except OSError: # file likely corrupted except OSError: # file likely corrupted
newname = f"{histfile}.old" newname = f"{histfile}.old"
logger.error(f"Your history file {histfile} couldn't be loaded and may be corrupted. Renaming it to {newname}") logger.error(
f"Your history file {histfile} couldn't be loaded and may be corrupted. Renaming it to {newname}"
)
histfile.replace(Path(newname)) histfile.replace(Path(newname))
atexit.register(readline.write_history_file, histfile) atexit.register(readline.write_history_file, histfile)

View File

@ -13,7 +13,6 @@ from pydantic.fields import Field
# This should come early so that the logger can pick up its configuration options # This should come early so that the logger can pick up its configuration options
from .services.config import InvokeAIAppConfig from .services.config import InvokeAIAppConfig
from invokeai.backend.util.logging import InvokeAILogger from invokeai.backend.util.logging import InvokeAILogger
config = InvokeAIAppConfig.get_config() config = InvokeAIAppConfig.get_config()
config.parse_args() config.parse_args()
logger = InvokeAILogger().getLogger(config=config) logger = InvokeAILogger().getLogger(config=config)
@ -21,7 +20,7 @@ from invokeai.version.invokeai_version import __version__
# we call this early so that the message appears before other invokeai initialization messages # we call this early so that the message appears before other invokeai initialization messages
if config.version: if config.version:
print(f"InvokeAI version {__version__}") print(f'InvokeAI version {__version__}')
sys.exit(0) sys.exit(0)
from invokeai.app.services.board_image_record_storage import ( from invokeai.app.services.board_image_record_storage import (
@ -37,21 +36,18 @@ from invokeai.app.services.image_record_storage import SqliteImageRecordStorage
from invokeai.app.services.images import ImageService, ImageServiceDependencies from invokeai.app.services.images import ImageService, ImageServiceDependencies
from invokeai.app.services.resource_name import SimpleNameService from invokeai.app.services.resource_name import SimpleNameService
from invokeai.app.services.urls import LocalUrlService from invokeai.app.services.urls import LocalUrlService
from .services.default_graphs import default_text_to_image_graph_id, create_system_graphs from .services.default_graphs import (default_text_to_image_graph_id,
create_system_graphs)
from .services.latent_storage import DiskLatentsStorage, ForwardCacheLatentsStorage from .services.latent_storage import DiskLatentsStorage, ForwardCacheLatentsStorage
from .cli.commands import BaseCommand, CliContext, ExitCli, SortedHelpFormatter, add_graph_parsers, add_parsers from .cli.commands import (BaseCommand, CliContext, ExitCli,
SortedHelpFormatter, add_graph_parsers, add_parsers)
from .cli.completer import set_autocompleter from .cli.completer import set_autocompleter
from .invocations.baseinvocation import BaseInvocation from .invocations.baseinvocation import BaseInvocation
from .services.events import EventServiceBase from .services.events import EventServiceBase
from .services.graph import ( from .services.graph import (Edge, EdgeConnection, GraphExecutionState,
Edge, GraphInvocation, LibraryGraph,
EdgeConnection, are_connection_types_compatible)
GraphExecutionState,
GraphInvocation,
LibraryGraph,
are_connection_types_compatible,
)
from .services.image_file_storage import DiskImageFileStorage from .services.image_file_storage import DiskImageFileStorage
from .services.invocation_queue import MemoryInvocationQueue from .services.invocation_queue import MemoryInvocationQueue
from .services.invocation_services import InvocationServices from .services.invocation_services import InvocationServices
@ -62,7 +58,6 @@ from .services.sqlite import SqliteItemStorage
import torch import torch
import invokeai.backend.util.hotfixes import invokeai.backend.util.hotfixes
if torch.backends.mps.is_available(): if torch.backends.mps.is_available():
import invokeai.backend.util.mps_fixes import invokeai.backend.util.mps_fixes
@ -74,7 +69,6 @@ class CliCommand(BaseModel):
class InvalidArgs(Exception): class InvalidArgs(Exception):
pass pass
def add_invocation_args(command_parser): def add_invocation_args(command_parser):
# Add linking capability # Add linking capability
command_parser.add_argument( command_parser.add_argument(
@ -119,7 +113,7 @@ def get_command_parser(services: InvocationServices) -> argparse.ArgumentParser:
return parser return parser
class NodeField: class NodeField():
alias: str alias: str
node_path: str node_path: str
field: str field: str
@ -132,20 +126,15 @@ class NodeField:
self.field_type = field_type self.field_type = field_type
def fields_from_type_hints(hints: dict[str, type], node_path: str) -> dict[str, NodeField]: def fields_from_type_hints(hints: dict[str, type], node_path: str) -> dict[str,NodeField]:
return {k: NodeField(alias=k, node_path=node_path, field=k, field_type=v) for k, v in hints.items()} return {k:NodeField(alias=k, node_path=node_path, field=k, field_type=v) for k, v in hints.items()}
def get_node_input_field(graph: LibraryGraph, field_alias: str, node_id: str) -> NodeField: def get_node_input_field(graph: LibraryGraph, field_alias: str, node_id: str) -> NodeField:
"""Gets the node field for the specified field alias""" """Gets the node field for the specified field alias"""
exposed_input = next(e for e in graph.exposed_inputs if e.alias == field_alias) exposed_input = next(e for e in graph.exposed_inputs if e.alias == field_alias)
node_type = type(graph.graph.get_node(exposed_input.node_path)) node_type = type(graph.graph.get_node(exposed_input.node_path))
return NodeField( return NodeField(alias=exposed_input.alias, node_path=f'{node_id}.{exposed_input.node_path}', field=exposed_input.field, field_type=get_type_hints(node_type)[exposed_input.field])
alias=exposed_input.alias,
node_path=f"{node_id}.{exposed_input.node_path}",
field=exposed_input.field,
field_type=get_type_hints(node_type)[exposed_input.field],
)
def get_node_output_field(graph: LibraryGraph, field_alias: str, node_id: str) -> NodeField: def get_node_output_field(graph: LibraryGraph, field_alias: str, node_id: str) -> NodeField:
@ -153,12 +142,7 @@ def get_node_output_field(graph: LibraryGraph, field_alias: str, node_id: str) -
exposed_output = next(e for e in graph.exposed_outputs if e.alias == field_alias) exposed_output = next(e for e in graph.exposed_outputs if e.alias == field_alias)
node_type = type(graph.graph.get_node(exposed_output.node_path)) node_type = type(graph.graph.get_node(exposed_output.node_path))
node_output_type = node_type.get_output_type() node_output_type = node_type.get_output_type()
return NodeField( return NodeField(alias=exposed_output.alias, node_path=f'{node_id}.{exposed_output.node_path}', field=exposed_output.field, field_type=get_type_hints(node_output_type)[exposed_output.field])
alias=exposed_output.alias,
node_path=f"{node_id}.{exposed_output.node_path}",
field=exposed_output.field,
field_type=get_type_hints(node_output_type)[exposed_output.field],
)
def get_node_inputs(invocation: BaseInvocation, context: CliContext) -> dict[str, NodeField]: def get_node_inputs(invocation: BaseInvocation, context: CliContext) -> dict[str, NodeField]:
@ -181,7 +165,9 @@ def get_node_outputs(invocation: BaseInvocation, context: CliContext) -> dict[st
return {e.alias: get_node_output_field(graph, e.alias, invocation.id) for e in graph.exposed_outputs} return {e.alias: get_node_output_field(graph, e.alias, invocation.id) for e in graph.exposed_outputs}
def generate_matching_edges(a: BaseInvocation, b: BaseInvocation, context: CliContext) -> list[Edge]: def generate_matching_edges(
a: BaseInvocation, b: BaseInvocation, context: CliContext
) -> list[Edge]:
"""Generates all possible edges between two invocations""" """Generates all possible edges between two invocations"""
afields = get_node_outputs(a, context) afields = get_node_outputs(a, context)
bfields = get_node_inputs(b, context) bfields = get_node_inputs(b, context)
@ -193,14 +179,12 @@ def generate_matching_edges(a: BaseInvocation, b: BaseInvocation, context: CliCo
matching_fields = matching_fields.difference(invalid_fields) matching_fields = matching_fields.difference(invalid_fields)
# Validate types # Validate types
matching_fields = [ matching_fields = [f for f in matching_fields if are_connection_types_compatible(afields[f].field_type, bfields[f].field_type)]
f for f in matching_fields if are_connection_types_compatible(afields[f].field_type, bfields[f].field_type)
]
edges = [ edges = [
Edge( Edge(
source=EdgeConnection(node_id=afields[alias].node_path, field=afields[alias].field), source=EdgeConnection(node_id=afields[alias].node_path, field=afields[alias].field),
destination=EdgeConnection(node_id=bfields[alias].node_path, field=bfields[alias].field), destination=EdgeConnection(node_id=bfields[alias].node_path, field=bfields[alias].field)
) )
for alias in matching_fields for alias in matching_fields
] ]
@ -209,7 +193,6 @@ def generate_matching_edges(a: BaseInvocation, b: BaseInvocation, context: CliCo
class SessionError(Exception): class SessionError(Exception):
"""Raised when a session error has occurred""" """Raised when a session error has occurred"""
pass pass
@ -229,20 +212,19 @@ def invoke_all(context: CliContext):
raise SessionError() raise SessionError()
def invoke_cli(): def invoke_cli():
logger.info(f"InvokeAI version {__version__}") logger.info(f'InvokeAI version {__version__}')
# get the optional list of invocations to execute on the command line # get the optional list of invocations to execute on the command line
parser = config.get_parser() parser = config.get_parser()
parser.add_argument("commands", nargs="*") parser.add_argument('commands',nargs='*')
invocation_commands = parser.parse_args().commands invocation_commands = parser.parse_args().commands
# get the optional file to read commands from. # get the optional file to read commands from.
# Simplest is to use it for STDIN # Simplest is to use it for STDIN
if infile := config.from_file: if infile := config.from_file:
sys.stdin = open(infile, "r") sys.stdin = open(infile,"r")
model_manager = ModelManagerService(config, logger) model_manager = ModelManagerService(config,logger)
events = EventServiceBase() events = EventServiceBase()
output_folder = config.output_path output_folder = config.output_path
@ -252,7 +234,7 @@ def invoke_cli():
db_location = ":memory:" db_location = ":memory:"
else: else:
db_location = config.db_path db_location = config.db_path
db_location.parent.mkdir(parents=True, exist_ok=True) db_location.parent.mkdir(parents=True,exist_ok=True)
logger.info(f'InvokeAI database location is "{db_location}"') logger.info(f'InvokeAI database location is "{db_location}"')
@ -303,18 +285,21 @@ def invoke_cli():
services = InvocationServices( services = InvocationServices(
model_manager=model_manager, model_manager=model_manager,
events=events, events=events,
latents=ForwardCacheLatentsStorage(DiskLatentsStorage(f"{output_folder}/latents")), latents = ForwardCacheLatentsStorage(DiskLatentsStorage(f'{output_folder}/latents')),
images=images, images=images,
boards=boards, boards=boards,
board_images=board_images, board_images=board_images,
queue=MemoryInvocationQueue(), queue=MemoryInvocationQueue(),
graph_library=SqliteItemStorage[LibraryGraph](filename=db_location, table_name="graphs"), graph_library=SqliteItemStorage[LibraryGraph](
filename=db_location, table_name="graphs"
),
graph_execution_manager=graph_execution_manager, graph_execution_manager=graph_execution_manager,
processor=DefaultInvocationProcessor(), processor=DefaultInvocationProcessor(),
logger=logger, logger=logger,
configuration=config, configuration=config,
) )
system_graphs = create_system_graphs(services.graph_library) system_graphs = create_system_graphs(services.graph_library)
system_graph_names = set([g.name for g in system_graphs]) system_graph_names = set([g.name for g in system_graphs])
set_autocompleter(services) set_autocompleter(services)
@ -323,7 +308,7 @@ def invoke_cli():
session: GraphExecutionState = invoker.create_execution_state() session: GraphExecutionState = invoker.create_execution_state()
parser = get_command_parser(services) parser = get_command_parser(services)
re_negid = re.compile("^-[0-9]+$") re_negid = re.compile('^-[0-9]+$')
# Uncomment to print out previous sessions at startup # Uncomment to print out previous sessions at startup
# print(services.session_manager.list()) # print(services.session_manager.list())
@ -347,7 +332,7 @@ def invoke_cli():
try: try:
# Refresh the state of the session # Refresh the state of the session
# history = list(get_graph_execution_history(context.session)) #history = list(get_graph_execution_history(context.session))
history = list(reversed(context.nodes_added)) history = list(reversed(context.nodes_added))
# Split the command for piping # Split the command for piping
@ -370,15 +355,15 @@ def invoke_cli():
# Parse invocation # Parse invocation
command: CliCommand = None # type:ignore command: CliCommand = None # type:ignore
system_graph: Optional[LibraryGraph] = None system_graph: Optional[LibraryGraph] = None
if args["type"] in system_graph_names: if args['type'] in system_graph_names:
system_graph = next(filter(lambda g: g.name == args["type"], system_graphs)) system_graph = next(filter(lambda g: g.name == args['type'], system_graphs))
invocation = GraphInvocation(graph=system_graph.graph, id=str(current_id)) invocation = GraphInvocation(graph=system_graph.graph, id=str(current_id))
for exposed_input in system_graph.exposed_inputs: for exposed_input in system_graph.exposed_inputs:
if exposed_input.alias in args: if exposed_input.alias in args:
node = invocation.graph.get_node(exposed_input.node_path) node = invocation.graph.get_node(exposed_input.node_path)
field = exposed_input.field field = exposed_input.field
setattr(node, field, args[exposed_input.alias]) setattr(node, field, args[exposed_input.alias])
command = CliCommand(command=invocation) command = CliCommand(command = invocation)
context.graph_nodes[invocation.id] = system_graph.id context.graph_nodes[invocation.id] = system_graph.id
else: else:
args["id"] = current_id args["id"] = current_id
@ -400,13 +385,17 @@ def invoke_cli():
# Pipe previous command output (if there was a previous command) # Pipe previous command output (if there was a previous command)
edges: list[Edge] = list() edges: list[Edge] = list()
if len(history) > 0 or current_id != start_id: if len(history) > 0 or current_id != start_id:
from_id = history[0] if current_id == start_id else str(current_id - 1) from_id = (
history[0] if current_id == start_id else str(current_id - 1)
)
from_node = ( from_node = (
next(filter(lambda n: n[0].id == from_id, new_invocations))[0] next(filter(lambda n: n[0].id == from_id, new_invocations))[0]
if current_id != start_id if current_id != start_id
else context.session.graph.get_node(from_id) else context.session.graph.get_node(from_id)
) )
matching_edges = generate_matching_edges(from_node, command.command, context) matching_edges = generate_matching_edges(
from_node, command.command, context
)
edges.extend(matching_edges) edges.extend(matching_edges)
# Parse provided links # Parse provided links
@ -417,18 +406,16 @@ def invoke_cli():
node_id = str(current_id + int(node_id)) node_id = str(current_id + int(node_id))
link_node = context.session.graph.get_node(node_id) link_node = context.session.graph.get_node(node_id)
matching_edges = generate_matching_edges(link_node, command.command, context) matching_edges = generate_matching_edges(
link_node, command.command, context
)
matching_destinations = [e.destination for e in matching_edges] matching_destinations = [e.destination for e in matching_edges]
edges = [e for e in edges if e.destination not in matching_destinations] edges = [e for e in edges if e.destination not in matching_destinations]
edges.extend(matching_edges) edges.extend(matching_edges)
if "link" in args and args["link"]: if "link" in args and args["link"]:
for link in args["link"]: for link in args["link"]:
edges = [ edges = [e for e in edges if e.destination.node_id != command.command.id or e.destination.field != link[2]]
e
for e in edges
if e.destination.node_id != command.command.id or e.destination.field != link[2]
]
node_id = link[0] node_id = link[0]
if re_negid.match(node_id): if re_negid.match(node_id):
@ -441,7 +428,7 @@ def invoke_cli():
edges.append( edges.append(
Edge( Edge(
source=EdgeConnection(node_id=node_output.node_path, field=node_output.field), source=EdgeConnection(node_id=node_output.node_path, field=node_output.field),
destination=EdgeConnection(node_id=node_input.node_path, field=node_input.field), destination=EdgeConnection(node_id=node_input.node_path, field=node_input.field)
) )
) )

View File

@ -4,5 +4,9 @@ __all__ = []
dirname = os.path.dirname(os.path.abspath(__file__)) dirname = os.path.dirname(os.path.abspath(__file__))
for f in os.listdir(dirname): for f in os.listdir(dirname):
if f != "__init__.py" and os.path.isfile("%s/%s" % (dirname, f)) and f[-3:] == ".py": if (
f != "__init__.py"
and os.path.isfile("%s/%s" % (dirname, f))
and f[-3:] == ".py"
):
__all__.append(f[:-3]) __all__.append(f[:-3])

View File

@ -4,7 +4,8 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from inspect import signature from inspect import signature
from typing import TYPE_CHECKING, Dict, List, Literal, TypedDict, get_args, get_type_hints from typing import (TYPE_CHECKING, Dict, List, Literal, TypedDict, get_args,
get_type_hints)
from pydantic import BaseConfig, BaseModel, Field from pydantic import BaseConfig, BaseModel, Field

View File

@ -4,11 +4,17 @@ from typing import Literal
import numpy as np import numpy as np
from pydantic import Field, validator from pydantic import Field, validator
from invokeai.app.models.image import ImageField from invokeai.app.models.image import ImageField
from invokeai.app.util.misc import SEED_MAX, get_random_seed from invokeai.app.util.misc import SEED_MAX, get_random_seed
from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationConfig, InvocationContext, UIConfig from .baseinvocation import (
BaseInvocation,
InvocationConfig,
InvocationContext,
BaseInvocationOutput,
UIConfig,
)
class IntCollectionOutput(BaseInvocationOutput): class IntCollectionOutput(BaseInvocationOutput):
@ -51,11 +57,6 @@ class RangeInvocation(BaseInvocation):
stop: int = Field(default=10, description="The stop of the range") stop: int = Field(default=10, description="The stop of the range")
step: int = Field(default=1, description="The step of the range") step: int = Field(default=1, description="The step of the range")
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Range", "tags": ["range", "integer", "collection"]},
}
@validator("stop") @validator("stop")
def stop_gt_start(cls, v, values): def stop_gt_start(cls, v, values):
if "start" in values and v <= values["start"]: if "start" in values and v <= values["start"]:
@ -63,7 +64,9 @@ class RangeInvocation(BaseInvocation):
return v return v
def invoke(self, context: InvocationContext) -> IntCollectionOutput: def invoke(self, context: InvocationContext) -> IntCollectionOutput:
return IntCollectionOutput(collection=list(range(self.start, self.stop, self.step))) return IntCollectionOutput(
collection=list(range(self.start, self.stop, self.step))
)
class RangeOfSizeInvocation(BaseInvocation): class RangeOfSizeInvocation(BaseInvocation):
@ -76,13 +79,10 @@ class RangeOfSizeInvocation(BaseInvocation):
size: int = Field(default=1, description="The number of values") size: int = Field(default=1, description="The number of values")
step: int = Field(default=1, description="The step of the range") step: int = Field(default=1, description="The step of the range")
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Sized Range", "tags": ["range", "integer", "size", "collection"]},
}
def invoke(self, context: InvocationContext) -> IntCollectionOutput: def invoke(self, context: InvocationContext) -> IntCollectionOutput:
return IntCollectionOutput(collection=list(range(self.start, self.start + self.size, self.step))) return IntCollectionOutput(
collection=list(range(self.start, self.start + self.size, self.step))
)
class RandomRangeInvocation(BaseInvocation): class RandomRangeInvocation(BaseInvocation):
@ -92,7 +92,9 @@ class RandomRangeInvocation(BaseInvocation):
# Inputs # Inputs
low: int = Field(default=0, description="The inclusive low value") low: int = Field(default=0, description="The inclusive low value")
high: int = Field(default=np.iinfo(np.int32).max, description="The exclusive high value") high: int = Field(
default=np.iinfo(np.int32).max, description="The exclusive high value"
)
size: int = Field(default=1, description="The number of values to generate") size: int = Field(default=1, description="The number of values to generate")
seed: int = Field( seed: int = Field(
ge=0, ge=0,
@ -101,14 +103,11 @@ class RandomRangeInvocation(BaseInvocation):
default_factory=get_random_seed, default_factory=get_random_seed,
) )
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Random Range", "tags": ["range", "integer", "random", "collection"]},
}
def invoke(self, context: InvocationContext) -> IntCollectionOutput: def invoke(self, context: InvocationContext) -> IntCollectionOutput:
rng = np.random.default_rng(self.seed) rng = np.random.default_rng(self.seed)
return IntCollectionOutput(collection=list(rng.integers(low=self.low, high=self.high, size=self.size))) return IntCollectionOutput(
collection=list(rng.integers(low=self.low, high=self.high, size=self.size))
)
class ImageCollectionInvocation(BaseInvocation): class ImageCollectionInvocation(BaseInvocation):
@ -122,7 +121,6 @@ class ImageCollectionInvocation(BaseInvocation):
default=[], description="The image collection to load" default=[], description="The image collection to load"
) )
# fmt: on # fmt: on
def invoke(self, context: InvocationContext) -> ImageCollectionOutput: def invoke(self, context: InvocationContext) -> ImageCollectionOutput:
return ImageCollectionOutput(collection=self.images) return ImageCollectionOutput(collection=self.images)
@ -130,7 +128,6 @@ class ImageCollectionInvocation(BaseInvocation):
schema_extra = { schema_extra = {
"ui": { "ui": {
"type_hints": { "type_hints": {
"title": "Image Collection",
"images": "image_collection", "images": "image_collection",
} }
}, },

View File

@ -1,73 +1,37 @@
from typing import Literal, Optional, Union, List, Annotated from typing import Literal, Optional, Union, List
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
import re import re
from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationContext, InvocationConfig
from .model import ClipField
from ...backend.util.devices import torch_dtype
from ...backend.stable_diffusion.diffusion import InvokeAIDiffuserComponent
from ...backend.model_management import BaseModelType, ModelType, SubModelType, ModelPatcher
import torch import torch
from compel import Compel, ReturnedEmbeddingsType from compel import Compel
from compel.prompt_parser import Blend, Conjunction, CrossAttentionControlSubstitute, FlattenedPrompt, Fragment from compel.prompt_parser import (Blend, Conjunction,
CrossAttentionControlSubstitute,
FlattenedPrompt, Fragment)
from ...backend.util.devices import torch_dtype from ...backend.util.devices import torch_dtype
from ...backend.model_management import ModelType from ...backend.model_management import ModelType
from ...backend.model_management.models import ModelNotFoundException from ...backend.model_management.models import ModelNotFoundException
from ...backend.model_management.lora import ModelPatcher from ...backend.model_management.lora import ModelPatcher
from ...backend.stable_diffusion.diffusion import InvokeAIDiffuserComponent from ...backend.stable_diffusion.diffusion import InvokeAIDiffuserComponent
from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationConfig, InvocationContext from .baseinvocation import (BaseInvocation, BaseInvocationOutput,
InvocationConfig, InvocationContext)
from .model import ClipField from .model import ClipField
from dataclasses import dataclass
class ConditioningField(BaseModel): class ConditioningField(BaseModel):
conditioning_name: Optional[str] = Field(default=None, description="The name of conditioning data") conditioning_name: Optional[str] = Field(
default=None, description="The name of conditioning data")
class Config: class Config:
schema_extra = {"required": ["conditioning_name"]} schema_extra = {"required": ["conditioning_name"]}
@dataclass
class BasicConditioningInfo:
# type: Literal["basic_conditioning"] = "basic_conditioning"
embeds: torch.Tensor
extra_conditioning: Optional[InvokeAIDiffuserComponent.ExtraConditioningInfo]
# weight: float
# mode: ConditioningAlgo
@dataclass
class SDXLConditioningInfo(BasicConditioningInfo):
# type: Literal["sdxl_conditioning"] = "sdxl_conditioning"
pooled_embeds: torch.Tensor
add_time_ids: torch.Tensor
ConditioningInfoType = Annotated[Union[BasicConditioningInfo, SDXLConditioningInfo], Field(discriminator="type")]
@dataclass
class ConditioningFieldData:
conditionings: List[Union[BasicConditioningInfo, SDXLConditioningInfo]]
# unconditioned: Optional[torch.Tensor]
# class ConditioningAlgo(str, Enum):
# Compose = "compose"
# ComposeEx = "compose_ex"
# PerpNeg = "perp_neg"
class CompelOutput(BaseInvocationOutput): class CompelOutput(BaseInvocationOutput):
"""Compel parser output""" """Compel parser output"""
# fmt: off #fmt: off
type: Literal["compel_output"] = "compel_output" type: Literal["compel_output"] = "compel_output"
conditioning: ConditioningField = Field(default=None, description="Conditioning") conditioning: ConditioningField = Field(default=None, description="Conditioning")
# fmt: on #fmt: on
class CompelInvocation(BaseInvocation): class CompelInvocation(BaseInvocation):
@ -81,28 +45,33 @@ class CompelInvocation(BaseInvocation):
# Schema customisation # Schema customisation
class Config(InvocationConfig): class Config(InvocationConfig):
schema_extra = { schema_extra = {
"ui": {"title": "Prompt (Compel)", "tags": ["prompt", "compel"], "type_hints": {"model": "model"}}, "ui": {
"title": "Prompt (Compel)",
"tags": ["prompt", "compel"],
"type_hints": {
"model": "model"
}
},
} }
@torch.no_grad() @torch.no_grad()
def invoke(self, context: InvocationContext) -> CompelOutput: def invoke(self, context: InvocationContext) -> CompelOutput:
tokenizer_info = context.services.model_manager.get_model( tokenizer_info = context.services.model_manager.get_model(
**self.clip.tokenizer.dict(), **self.clip.tokenizer.dict(),
context=context,
) )
text_encoder_info = context.services.model_manager.get_model( text_encoder_info = context.services.model_manager.get_model(
**self.clip.text_encoder.dict(), **self.clip.text_encoder.dict(),
context=context,
) )
def _lora_loader(): def _lora_loader():
for lora in self.clip.loras: for lora in self.clip.loras:
lora_info = context.services.model_manager.get_model(**lora.dict(exclude={"weight"}), context=context) lora_info = context.services.model_manager.get_model(
**lora.dict(exclude={"weight"}))
yield (lora_info.context.model, lora.weight) yield (lora_info.context.model, lora.weight)
del lora_info del lora_info
return return
# loras = [(context.services.model_manager.get_model(**lora.dict(exclude={"weight"})).context.model, lora.weight) for lora in self.clip.loras] #loras = [(context.services.model_manager.get_model(**lora.dict(exclude={"weight"})).context.model, lora.weight) for lora in self.clip.loras]
ti_list = [] ti_list = []
for trigger in re.findall(r"<[a-zA-Z0-9., _-]+>", self.prompt): for trigger in re.findall(r"<[a-zA-Z0-9., _-]+>", self.prompt):
@ -113,29 +82,25 @@ class CompelInvocation(BaseInvocation):
model_name=name, model_name=name,
base_model=self.clip.text_encoder.base_model, base_model=self.clip.text_encoder.base_model,
model_type=ModelType.TextualInversion, model_type=ModelType.TextualInversion,
context=context,
).context.model ).context.model
) )
except ModelNotFoundException: except ModelNotFoundException:
# print(e) # print(e)
# import traceback #import traceback
# print(traceback.format_exc()) #print(traceback.format_exc())
print(f'Warn: trigger: "{trigger}" not found') print(f"Warn: trigger: \"{trigger}\" not found")
with ModelPatcher.apply_lora_text_encoder(text_encoder_info.context.model, _lora_loader()),\
ModelPatcher.apply_ti(tokenizer_info.context.model, text_encoder_info.context.model, ti_list) as (tokenizer, ti_manager),\
ModelPatcher.apply_clip_skip(text_encoder_info.context.model, self.clip.skipped_layers),\
text_encoder_info as text_encoder:
with ModelPatcher.apply_lora_text_encoder(
text_encoder_info.context.model, _lora_loader()
), ModelPatcher.apply_ti(tokenizer_info.context.model, text_encoder_info.context.model, ti_list) as (
tokenizer,
ti_manager,
), ModelPatcher.apply_clip_skip(
text_encoder_info.context.model, self.clip.skipped_layers
), text_encoder_info as text_encoder:
compel = Compel( compel = Compel(
tokenizer=tokenizer, tokenizer=tokenizer,
text_encoder=text_encoder, text_encoder=text_encoder,
textual_inversion_manager=ti_manager, textual_inversion_manager=ti_manager,
dtype_for_device_getter=torch_dtype, dtype_for_device_getter=torch_dtype,
truncate_long_prompts=True, truncate_long_prompts=False,
) )
conjunction = Compel.parse_prompt_string(self.prompt) conjunction = Compel.parse_prompt_string(self.prompt)
@ -144,26 +109,19 @@ class CompelInvocation(BaseInvocation):
if context.services.configuration.log_tokenization: if context.services.configuration.log_tokenization:
log_tokenization_for_prompt_object(prompt, tokenizer) log_tokenization_for_prompt_object(prompt, tokenizer)
c, options = compel.build_conditioning_tensor_for_prompt_object(prompt) c, options = compel.build_conditioning_tensor_for_prompt_object(
prompt)
ec = InvokeAIDiffuserComponent.ExtraConditioningInfo( ec = InvokeAIDiffuserComponent.ExtraConditioningInfo(
tokens_count_including_eos_bos=get_max_token_count(tokenizer, conjunction), tokens_count_including_eos_bos=get_max_token_count(
cross_attention_control_args=options.get("cross_attention_control", None), tokenizer, conjunction),
) cross_attention_control_args=options.get(
"cross_attention_control", None),)
c = c.detach().to("cpu")
conditioning_data = ConditioningFieldData(
conditionings=[
BasicConditioningInfo(
embeds=c,
extra_conditioning=ec,
)
]
)
conditioning_name = f"{context.graph_execution_state_id}_{self.id}_conditioning" conditioning_name = f"{context.graph_execution_state_id}_{self.id}_conditioning"
context.services.latents.save(conditioning_name, conditioning_data)
# TODO: hacky but works ;D maybe rename latents somehow?
context.services.latents.save(conditioning_name, (c, ec))
return CompelOutput( return CompelOutput(
conditioning=ConditioningField( conditioning=ConditioningField(
@ -171,408 +129,18 @@ class CompelInvocation(BaseInvocation):
), ),
) )
class SDXLPromptInvocationBase:
def run_clip_raw(self, context, clip_field, prompt, get_pooled):
tokenizer_info = context.services.model_manager.get_model(
**clip_field.tokenizer.dict(),
context=context,
)
text_encoder_info = context.services.model_manager.get_model(
**clip_field.text_encoder.dict(),
context=context,
)
def _lora_loader():
for lora in clip_field.loras:
lora_info = context.services.model_manager.get_model(**lora.dict(exclude={"weight"}), context=context)
yield (lora_info.context.model, lora.weight)
del lora_info
return
# loras = [(context.services.model_manager.get_model(**lora.dict(exclude={"weight"})).context.model, lora.weight) for lora in self.clip.loras]
ti_list = []
for trigger in re.findall(r"<[a-zA-Z0-9., _-]+>", prompt):
name = trigger[1:-1]
try:
ti_list.append(
context.services.model_manager.get_model(
model_name=name,
base_model=clip_field.text_encoder.base_model,
model_type=ModelType.TextualInversion,
context=context,
).context.model
)
except ModelNotFoundException:
# print(e)
# import traceback
# print(traceback.format_exc())
print(f'Warn: trigger: "{trigger}" not found')
with ModelPatcher.apply_lora_text_encoder(
text_encoder_info.context.model, _lora_loader()
), ModelPatcher.apply_ti(tokenizer_info.context.model, text_encoder_info.context.model, ti_list) as (
tokenizer,
ti_manager,
), ModelPatcher.apply_clip_skip(
text_encoder_info.context.model, clip_field.skipped_layers
), text_encoder_info as text_encoder:
text_inputs = tokenizer(
prompt,
padding="max_length",
max_length=tokenizer.model_max_length,
truncation=True,
return_tensors="pt",
)
text_input_ids = text_inputs.input_ids
prompt_embeds = text_encoder(
text_input_ids.to(text_encoder.device),
output_hidden_states=True,
)
if get_pooled:
c_pooled = prompt_embeds[0]
else:
c_pooled = None
c = prompt_embeds.hidden_states[-2]
del tokenizer
del text_encoder
del tokenizer_info
del text_encoder_info
c = c.detach().to("cpu")
if c_pooled is not None:
c_pooled = c_pooled.detach().to("cpu")
return c, c_pooled, None
def run_clip_compel(self, context, clip_field, prompt, get_pooled):
tokenizer_info = context.services.model_manager.get_model(
**clip_field.tokenizer.dict(),
context=context,
)
text_encoder_info = context.services.model_manager.get_model(
**clip_field.text_encoder.dict(),
context=context,
)
def _lora_loader():
for lora in clip_field.loras:
lora_info = context.services.model_manager.get_model(**lora.dict(exclude={"weight"}), context=context)
yield (lora_info.context.model, lora.weight)
del lora_info
return
# loras = [(context.services.model_manager.get_model(**lora.dict(exclude={"weight"})).context.model, lora.weight) for lora in self.clip.loras]
ti_list = []
for trigger in re.findall(r"<[a-zA-Z0-9., _-]+>", prompt):
name = trigger[1:-1]
try:
ti_list.append(
context.services.model_manager.get_model(
model_name=name,
base_model=clip_field.text_encoder.base_model,
model_type=ModelType.TextualInversion,
context=context,
).context.model
)
except ModelNotFoundException:
# print(e)
# import traceback
# print(traceback.format_exc())
print(f'Warn: trigger: "{trigger}" not found')
with ModelPatcher.apply_lora_text_encoder(
text_encoder_info.context.model, _lora_loader()
), ModelPatcher.apply_ti(tokenizer_info.context.model, text_encoder_info.context.model, ti_list) as (
tokenizer,
ti_manager,
), ModelPatcher.apply_clip_skip(
text_encoder_info.context.model, clip_field.skipped_layers
), text_encoder_info as text_encoder:
compel = Compel(
tokenizer=tokenizer,
text_encoder=text_encoder,
textual_inversion_manager=ti_manager,
dtype_for_device_getter=torch_dtype,
truncate_long_prompts=True, # TODO:
returned_embeddings_type=ReturnedEmbeddingsType.PENULTIMATE_HIDDEN_STATES_NON_NORMALIZED, # TODO: clip skip
requires_pooled=True,
)
conjunction = Compel.parse_prompt_string(prompt)
if context.services.configuration.log_tokenization:
# TODO: better logging for and syntax
for prompt_obj in conjunction.prompts:
log_tokenization_for_prompt_object(prompt_obj, tokenizer)
# TODO: ask for optimizations? to not run text_encoder twice
c, options = compel.build_conditioning_tensor_for_conjunction(conjunction)
if get_pooled:
c_pooled = compel.conditioning_provider.get_pooled_embeddings([prompt])
else:
c_pooled = None
ec = InvokeAIDiffuserComponent.ExtraConditioningInfo(
tokens_count_including_eos_bos=get_max_token_count(tokenizer, conjunction),
cross_attention_control_args=options.get("cross_attention_control", None),
)
del tokenizer
del text_encoder
del tokenizer_info
del text_encoder_info
c = c.detach().to("cpu")
if c_pooled is not None:
c_pooled = c_pooled.detach().to("cpu")
return c, c_pooled, ec
class SDXLCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase):
"""Parse prompt using compel package to conditioning."""
type: Literal["sdxl_compel_prompt"] = "sdxl_compel_prompt"
prompt: str = Field(default="", description="Prompt")
style: str = Field(default="", description="Style prompt")
original_width: int = Field(1024, description="")
original_height: int = Field(1024, description="")
crop_top: int = Field(0, description="")
crop_left: int = Field(0, description="")
target_width: int = Field(1024, description="")
target_height: int = Field(1024, description="")
clip: ClipField = Field(None, description="Clip to use")
clip2: ClipField = Field(None, description="Clip2 to use")
# Schema customisation
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "SDXL Prompt (Compel)", "tags": ["prompt", "compel"], "type_hints": {"model": "model"}},
}
@torch.no_grad()
def invoke(self, context: InvocationContext) -> CompelOutput:
c1, c1_pooled, ec1 = self.run_clip_compel(context, self.clip, self.prompt, False)
if self.style.strip() == "":
c2, c2_pooled, ec2 = self.run_clip_compel(context, self.clip2, self.prompt, True)
else:
c2, c2_pooled, ec2 = self.run_clip_compel(context, self.clip2, self.style, True)
original_size = (self.original_height, self.original_width)
crop_coords = (self.crop_top, self.crop_left)
target_size = (self.target_height, self.target_width)
add_time_ids = torch.tensor([original_size + crop_coords + target_size])
conditioning_data = ConditioningFieldData(
conditionings=[
SDXLConditioningInfo(
embeds=torch.cat([c1, c2], dim=-1),
pooled_embeds=c2_pooled,
add_time_ids=add_time_ids,
extra_conditioning=ec1,
)
]
)
conditioning_name = f"{context.graph_execution_state_id}_{self.id}_conditioning"
context.services.latents.save(conditioning_name, conditioning_data)
return CompelOutput(
conditioning=ConditioningField(
conditioning_name=conditioning_name,
),
)
class SDXLRefinerCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase):
"""Parse prompt using compel package to conditioning."""
type: Literal["sdxl_refiner_compel_prompt"] = "sdxl_refiner_compel_prompt"
style: str = Field(default="", description="Style prompt") # TODO: ?
original_width: int = Field(1024, description="")
original_height: int = Field(1024, description="")
crop_top: int = Field(0, description="")
crop_left: int = Field(0, description="")
aesthetic_score: float = Field(6.0, description="")
clip2: ClipField = Field(None, description="Clip to use")
# Schema customisation
class Config(InvocationConfig):
schema_extra = {
"ui": {
"title": "SDXL Refiner Prompt (Compel)",
"tags": ["prompt", "compel"],
"type_hints": {"model": "model"},
},
}
@torch.no_grad()
def invoke(self, context: InvocationContext) -> CompelOutput:
c2, c2_pooled, ec2 = self.run_clip_compel(context, self.clip2, self.style, True)
original_size = (self.original_height, self.original_width)
crop_coords = (self.crop_top, self.crop_left)
add_time_ids = torch.tensor([original_size + crop_coords + (self.aesthetic_score,)])
conditioning_data = ConditioningFieldData(
conditionings=[
SDXLConditioningInfo(
embeds=c2,
pooled_embeds=c2_pooled,
add_time_ids=add_time_ids,
extra_conditioning=ec2, # or None
)
]
)
conditioning_name = f"{context.graph_execution_state_id}_{self.id}_conditioning"
context.services.latents.save(conditioning_name, conditioning_data)
return CompelOutput(
conditioning=ConditioningField(
conditioning_name=conditioning_name,
),
)
class SDXLRawPromptInvocation(BaseInvocation, SDXLPromptInvocationBase):
"""Pass unmodified prompt to conditioning without compel processing."""
type: Literal["sdxl_raw_prompt"] = "sdxl_raw_prompt"
prompt: str = Field(default="", description="Prompt")
style: str = Field(default="", description="Style prompt")
original_width: int = Field(1024, description="")
original_height: int = Field(1024, description="")
crop_top: int = Field(0, description="")
crop_left: int = Field(0, description="")
target_width: int = Field(1024, description="")
target_height: int = Field(1024, description="")
clip: ClipField = Field(None, description="Clip to use")
clip2: ClipField = Field(None, description="Clip2 to use")
# Schema customisation
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "SDXL Prompt (Raw)", "tags": ["prompt", "compel"], "type_hints": {"model": "model"}},
}
@torch.no_grad()
def invoke(self, context: InvocationContext) -> CompelOutput:
c1, c1_pooled, ec1 = self.run_clip_raw(context, self.clip, self.prompt, False)
if self.style.strip() == "":
c2, c2_pooled, ec2 = self.run_clip_raw(context, self.clip2, self.prompt, True)
else:
c2, c2_pooled, ec2 = self.run_clip_raw(context, self.clip2, self.style, True)
original_size = (self.original_height, self.original_width)
crop_coords = (self.crop_top, self.crop_left)
target_size = (self.target_height, self.target_width)
add_time_ids = torch.tensor([original_size + crop_coords + target_size])
conditioning_data = ConditioningFieldData(
conditionings=[
SDXLConditioningInfo(
embeds=torch.cat([c1, c2], dim=-1),
pooled_embeds=c2_pooled,
add_time_ids=add_time_ids,
extra_conditioning=ec1,
)
]
)
conditioning_name = f"{context.graph_execution_state_id}_{self.id}_conditioning"
context.services.latents.save(conditioning_name, conditioning_data)
return CompelOutput(
conditioning=ConditioningField(
conditioning_name=conditioning_name,
),
)
class SDXLRefinerRawPromptInvocation(BaseInvocation, SDXLPromptInvocationBase):
"""Parse prompt using compel package to conditioning."""
type: Literal["sdxl_refiner_raw_prompt"] = "sdxl_refiner_raw_prompt"
style: str = Field(default="", description="Style prompt") # TODO: ?
original_width: int = Field(1024, description="")
original_height: int = Field(1024, description="")
crop_top: int = Field(0, description="")
crop_left: int = Field(0, description="")
aesthetic_score: float = Field(6.0, description="")
clip2: ClipField = Field(None, description="Clip to use")
# Schema customisation
class Config(InvocationConfig):
schema_extra = {
"ui": {
"title": "SDXL Refiner Prompt (Raw)",
"tags": ["prompt", "compel"],
"type_hints": {"model": "model"},
},
}
@torch.no_grad()
def invoke(self, context: InvocationContext) -> CompelOutput:
c2, c2_pooled, ec2 = self.run_clip_raw(context, self.clip2, self.style, True)
original_size = (self.original_height, self.original_width)
crop_coords = (self.crop_top, self.crop_left)
add_time_ids = torch.tensor([original_size + crop_coords + (self.aesthetic_score,)])
conditioning_data = ConditioningFieldData(
conditionings=[
SDXLConditioningInfo(
embeds=c2,
pooled_embeds=c2_pooled,
add_time_ids=add_time_ids,
extra_conditioning=ec2, # or None
)
]
)
conditioning_name = f"{context.graph_execution_state_id}_{self.id}_conditioning"
context.services.latents.save(conditioning_name, conditioning_data)
return CompelOutput(
conditioning=ConditioningField(
conditioning_name=conditioning_name,
),
)
class ClipSkipInvocationOutput(BaseInvocationOutput): class ClipSkipInvocationOutput(BaseInvocationOutput):
"""Clip skip node output""" """Clip skip node output"""
type: Literal["clip_skip_output"] = "clip_skip_output" type: Literal["clip_skip_output"] = "clip_skip_output"
clip: ClipField = Field(None, description="Clip with skipped layers") clip: ClipField = Field(None, description="Clip with skipped layers")
class ClipSkipInvocation(BaseInvocation): class ClipSkipInvocation(BaseInvocation):
"""Skip layers in clip text_encoder model.""" """Skip layers in clip text_encoder model."""
type: Literal["clip_skip"] = "clip_skip" type: Literal["clip_skip"] = "clip_skip"
clip: ClipField = Field(None, description="Clip to use") clip: ClipField = Field(None, description="Clip to use")
skipped_layers: int = Field(0, description="Number of layers to skip in text_encoder") skipped_layers: int = Field(0, description="Number of layers to skip in text_encoder")
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "CLIP Skip", "tags": ["clip", "skip"]},
}
def invoke(self, context: InvocationContext) -> ClipSkipInvocationOutput: def invoke(self, context: InvocationContext) -> ClipSkipInvocationOutput:
self.clip.skipped_layers += self.skipped_layers self.clip.skipped_layers += self.skipped_layers
return ClipSkipInvocationOutput( return ClipSkipInvocationOutput(
@ -581,26 +149,46 @@ class ClipSkipInvocation(BaseInvocation):
def get_max_token_count( def get_max_token_count(
tokenizer, prompt: Union[FlattenedPrompt, Blend, Conjunction], truncate_if_too_long=False tokenizer, prompt: Union[FlattenedPrompt, Blend, Conjunction],
) -> int: truncate_if_too_long=False) -> int:
if type(prompt) is Blend: if type(prompt) is Blend:
blend: Blend = prompt blend: Blend = prompt
return max([get_max_token_count(tokenizer, p, truncate_if_too_long) for p in blend.prompts]) return max(
[
get_max_token_count(tokenizer, p, truncate_if_too_long)
for p in blend.prompts
]
)
elif type(prompt) is Conjunction: elif type(prompt) is Conjunction:
conjunction: Conjunction = prompt conjunction: Conjunction = prompt
return sum([get_max_token_count(tokenizer, p, truncate_if_too_long) for p in conjunction.prompts]) return sum(
[
get_max_token_count(tokenizer, p, truncate_if_too_long)
for p in conjunction.prompts
]
)
else: else:
return len(get_tokens_for_prompt_object(tokenizer, prompt, truncate_if_too_long)) return len(
get_tokens_for_prompt_object(
tokenizer, prompt, truncate_if_too_long))
def get_tokens_for_prompt_object(tokenizer, parsed_prompt: FlattenedPrompt, truncate_if_too_long=True) -> List[str]: def get_tokens_for_prompt_object(
tokenizer, parsed_prompt: FlattenedPrompt, truncate_if_too_long=True
) -> List[str]:
if type(parsed_prompt) is Blend: if type(parsed_prompt) is Blend:
raise ValueError("Blend is not supported here - you need to get tokens for each of its .children") raise ValueError(
"Blend is not supported here - you need to get tokens for each of its .children"
)
text_fragments = [ text_fragments = [
x.text x.text
if type(x) is Fragment if type(x) is Fragment
else (" ".join([f.text for f in x.original]) if type(x) is CrossAttentionControlSubstitute else str(x)) else (
" ".join([f.text for f in x.original])
if type(x) is CrossAttentionControlSubstitute
else str(x)
)
for x in parsed_prompt.children for x in parsed_prompt.children
] ]
text = " ".join(text_fragments) text = " ".join(text_fragments)
@ -611,17 +199,25 @@ def get_tokens_for_prompt_object(tokenizer, parsed_prompt: FlattenedPrompt, trun
return tokens return tokens
def log_tokenization_for_conjunction(c: Conjunction, tokenizer, display_label_prefix=None): def log_tokenization_for_conjunction(
c: Conjunction, tokenizer, display_label_prefix=None
):
display_label_prefix = display_label_prefix or "" display_label_prefix = display_label_prefix or ""
for i, p in enumerate(c.prompts): for i, p in enumerate(c.prompts):
if len(c.prompts) > 1: if len(c.prompts) > 1:
this_display_label_prefix = f"{display_label_prefix}(conjunction part {i + 1}, weight={c.weights[i]})" this_display_label_prefix = f"{display_label_prefix}(conjunction part {i + 1}, weight={c.weights[i]})"
else: else:
this_display_label_prefix = display_label_prefix this_display_label_prefix = display_label_prefix
log_tokenization_for_prompt_object(p, tokenizer, display_label_prefix=this_display_label_prefix) log_tokenization_for_prompt_object(
p,
tokenizer,
display_label_prefix=this_display_label_prefix
)
def log_tokenization_for_prompt_object(p: Union[Blend, FlattenedPrompt], tokenizer, display_label_prefix=None): def log_tokenization_for_prompt_object(
p: Union[Blend, FlattenedPrompt], tokenizer, display_label_prefix=None
):
display_label_prefix = display_label_prefix or "" display_label_prefix = display_label_prefix or ""
if type(p) is Blend: if type(p) is Blend:
blend: Blend = p blend: Blend = p
@ -658,10 +254,13 @@ def log_tokenization_for_prompt_object(p: Union[Blend, FlattenedPrompt], tokeniz
) )
else: else:
text = " ".join([x.text for x in flattened_prompt.children]) text = " ".join([x.text for x in flattened_prompt.children])
log_tokenization_for_text(text, tokenizer, display_label=display_label_prefix) log_tokenization_for_text(
text, tokenizer, display_label=display_label_prefix
)
def log_tokenization_for_text(text, tokenizer, display_label=None, truncate_if_too_long=False): def log_tokenization_for_text(
text, tokenizer, display_label=None, truncate_if_too_long=False):
"""shows how the prompt is tokenized """shows how the prompt is tokenized
# usually tokens have '</w>' to indicate end-of-word, # usually tokens have '</w>' to indicate end-of-word,
# but for readability it has been replaced with ' ' # but for readability it has been replaced with ' '

View File

@ -1,35 +1,44 @@
# Invocations for ControlNet image preprocessors # Invocations for ControlNet image preprocessors
# initial implementation by Gregg Helt, 2023 # initial implementation by Gregg Helt, 2023
# heavily leverages controlnet_aux package: https://github.com/patrickvonplaten/controlnet_aux # heavily leverages controlnet_aux package: https://github.com/patrickvonplaten/controlnet_aux
from builtins import bool, float from builtins import float, bool
from typing import Dict, List, Literal, Optional, Union
import cv2 import cv2
import numpy as np import numpy as np
from typing import Literal, Optional, Union, List, Dict
from PIL import Image
from pydantic import BaseModel, Field, validator
from ...backend.model_management import BaseModelType, ModelType
from ..models.image import ImageField, ImageCategory, ResourceOrigin
from .baseinvocation import (
BaseInvocation,
BaseInvocationOutput,
InvocationContext,
InvocationConfig,
)
from controlnet_aux import ( from controlnet_aux import (
CannyDetector, CannyDetector,
ContentShuffleDetector,
HEDdetector, HEDdetector,
LeresDetector,
LineartAnimeDetector,
LineartDetector, LineartDetector,
MediapipeFaceDetector, LineartAnimeDetector,
MidasDetector, MidasDetector,
MLSDdetector, MLSDdetector,
NormalBaeDetector, NormalBaeDetector,
OpenposeDetector, OpenposeDetector,
PidiNetDetector, PidiNetDetector,
SamDetector, ContentShuffleDetector,
ZoeDetector, ZoeDetector,
MediapipeFaceDetector,
SamDetector,
LeresDetector,
) )
from controlnet_aux.util import HWC3, ade_palette
from PIL import Image
from pydantic import BaseModel, Field, validator
from ...backend.model_management import BaseModelType, ModelType from controlnet_aux.util import HWC3, ade_palette
from ..models.image import ImageCategory, ImageField, ResourceOrigin
from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationConfig, InvocationContext
from ..models.image import ImageOutput, PILInvocationConfig from .image import ImageOutput, PILInvocationConfig
CONTROLNET_DEFAULT_MODELS = [ CONTROLNET_DEFAULT_MODELS = [
########################################### ###########################################
@ -43,6 +52,7 @@ CONTROLNET_DEFAULT_MODELS = [
"lllyasviel/sd-controlnet-scribble", "lllyasviel/sd-controlnet-scribble",
"lllyasviel/sd-controlnet-normal", "lllyasviel/sd-controlnet-normal",
"lllyasviel/sd-controlnet-mlsd", "lllyasviel/sd-controlnet-mlsd",
############################################# #############################################
# lllyasviel sd v1.5, ControlNet v1.1 models # lllyasviel sd v1.5, ControlNet v1.1 models
############################################# #############################################
@ -64,6 +74,7 @@ CONTROLNET_DEFAULT_MODELS = [
"lllyasviel/control_v11e_sd15_shuffle", "lllyasviel/control_v11e_sd15_shuffle",
"lllyasviel/control_v11e_sd15_ip2p", "lllyasviel/control_v11e_sd15_ip2p",
"lllyasviel/control_v11f1e_sd15_tile", "lllyasviel/control_v11f1e_sd15_tile",
################################################# #################################################
# thibaud sd v2.1 models (ControlNet v1.0? or v1.1? # thibaud sd v2.1 models (ControlNet v1.0? or v1.1?
################################################## ##################################################
@ -78,6 +89,7 @@ CONTROLNET_DEFAULT_MODELS = [
"thibaud/controlnet-sd21-lineart-diffusers", "thibaud/controlnet-sd21-lineart-diffusers",
"thibaud/controlnet-sd21-normalbae-diffusers", "thibaud/controlnet-sd21-normalbae-diffusers",
"thibaud/controlnet-sd21-ade20k-diffusers", "thibaud/controlnet-sd21-ade20k-diffusers",
############################################## ##############################################
# ControlNetMediaPipeface, ControlNet v1.1 # ControlNetMediaPipeface, ControlNet v1.1
############################################## ##############################################
@ -90,16 +102,8 @@ CONTROLNET_DEFAULT_MODELS = [
CONTROLNET_NAME_VALUES = Literal[tuple(CONTROLNET_DEFAULT_MODELS)] CONTROLNET_NAME_VALUES = Literal[tuple(CONTROLNET_DEFAULT_MODELS)]
CONTROLNET_MODE_VALUES = Literal[tuple(["balanced", "more_prompt", "more_control", "unbalanced"])] CONTROLNET_MODE_VALUES = Literal[tuple(["balanced", "more_prompt", "more_control", "unbalanced"])]
CONTROLNET_RESIZE_VALUES = Literal[ # crop and fill options not ready yet
tuple( # CONTROLNET_RESIZE_VALUES = Literal[tuple(["just_resize", "crop_resize", "fill_resize"])]
[
"just_resize",
"crop_resize",
"fill_resize",
"just_resize_simple",
]
)
]
class ControlNetModelField(BaseModel): class ControlNetModelField(BaseModel):
@ -108,20 +112,17 @@ class ControlNetModelField(BaseModel):
model_name: str = Field(description="Name of the ControlNet model") model_name: str = Field(description="Name of the ControlNet model")
base_model: BaseModelType = Field(description="Base model") base_model: BaseModelType = Field(description="Base model")
class ControlField(BaseModel): class ControlField(BaseModel):
image: ImageField = Field(default=None, description="The control image") image: ImageField = Field(default=None, description="The control image")
control_model: Optional[ControlNetModelField] = Field(default=None, description="The ControlNet model to use") control_model: Optional[ControlNetModelField] = Field(default=None, description="The ControlNet model to use")
# control_weight: Optional[float] = Field(default=1, description="weight given to controlnet") # control_weight: Optional[float] = Field(default=1, description="weight given to controlnet")
control_weight: Union[float, List[float]] = Field(default=1, description="The weight given to the ControlNet") control_weight: Union[float, List[float]] = Field(default=1, description="The weight given to the ControlNet")
begin_step_percent: float = Field( begin_step_percent: float = Field(default=0, ge=0, le=1,
default=0, ge=0, le=1, description="When the ControlNet is first applied (% of total steps)" description="When the ControlNet is first applied (% of total steps)")
) end_step_percent: float = Field(default=1, ge=0, le=1,
end_step_percent: float = Field( description="When the ControlNet is last applied (% of total steps)")
default=1, ge=0, le=1, description="When the ControlNet is last applied (% of total steps)"
)
control_mode: CONTROLNET_MODE_VALUES = Field(default="balanced", description="The control mode to use") control_mode: CONTROLNET_MODE_VALUES = Field(default="balanced", description="The control mode to use")
resize_mode: CONTROLNET_RESIZE_VALUES = Field(default="just_resize", description="The resize mode to use") # resize_mode: CONTROLNET_RESIZE_VALUES = Field(default="just_resize", description="The resize mode to use")
@validator("control_weight") @validator("control_weight")
def validate_control_weight(cls, v): def validate_control_weight(cls, v):
@ -129,12 +130,11 @@ class ControlField(BaseModel):
if isinstance(v, list): if isinstance(v, list):
for i in v: for i in v:
if i < -1 or i > 2: if i < -1 or i > 2:
raise ValueError("Control weights must be within -1 to 2 range") raise ValueError('Control weights must be within -1 to 2 range')
else: else:
if v < -1 or v > 2: if v < -1 or v > 2:
raise ValueError("Control weights must be within -1 to 2 range") raise ValueError('Control weights must be within -1 to 2 range')
return v return v
class Config: class Config:
schema_extra = { schema_extra = {
"required": ["image", "control_model", "control_weight", "begin_step_percent", "end_step_percent"], "required": ["image", "control_model", "control_weight", "begin_step_percent", "end_step_percent"],
@ -144,13 +144,12 @@ class ControlField(BaseModel):
"control_model": "controlnet_model", "control_model": "controlnet_model",
# "control_weight": "number", # "control_weight": "number",
} }
}, }
} }
class ControlOutput(BaseInvocationOutput): class ControlOutput(BaseInvocationOutput):
"""node output for ControlNet info""" """node output for ControlNet info"""
# fmt: off # fmt: off
type: Literal["control_output"] = "control_output" type: Literal["control_output"] = "control_output"
control: ControlField = Field(default=None, description="The control info") control: ControlField = Field(default=None, description="The control info")
@ -159,7 +158,6 @@ class ControlOutput(BaseInvocationOutput):
class ControlNetInvocation(BaseInvocation): class ControlNetInvocation(BaseInvocation):
"""Collects ControlNet info to pass to other nodes""" """Collects ControlNet info to pass to other nodes"""
# fmt: off # fmt: off
type: Literal["controlnet"] = "controlnet" type: Literal["controlnet"] = "controlnet"
# Inputs # Inputs
@ -172,21 +170,19 @@ class ControlNetInvocation(BaseInvocation):
end_step_percent: float = Field(default=1, ge=0, le=1, end_step_percent: float = Field(default=1, ge=0, le=1,
description="When the ControlNet is last applied (% of total steps)") description="When the ControlNet is last applied (% of total steps)")
control_mode: CONTROLNET_MODE_VALUES = Field(default="balanced", description="The control mode used") control_mode: CONTROLNET_MODE_VALUES = Field(default="balanced", description="The control mode used")
resize_mode: CONTROLNET_RESIZE_VALUES = Field(default="just_resize", description="The resize mode used")
# fmt: on # fmt: on
class Config(InvocationConfig): class Config(InvocationConfig):
schema_extra = { schema_extra = {
"ui": { "ui": {
"title": "ControlNet", "tags": ["latents"],
"tags": ["controlnet", "latents"],
"type_hints": { "type_hints": {
"model": "model", "model": "model",
"control": "control", "control": "control",
# "cfg_scale": "float", # "cfg_scale": "float",
"cfg_scale": "number", "cfg_scale": "number",
"control_weight": "float", "control_weight": "float",
}, }
}, },
} }
@ -199,7 +195,6 @@ class ControlNetInvocation(BaseInvocation):
begin_step_percent=self.begin_step_percent, begin_step_percent=self.begin_step_percent,
end_step_percent=self.end_step_percent, end_step_percent=self.end_step_percent,
control_mode=self.control_mode, control_mode=self.control_mode,
resize_mode=self.resize_mode,
), ),
) )
@ -213,10 +208,6 @@ class ImageProcessorInvocation(BaseInvocation, PILInvocationConfig):
image: ImageField = Field(default=None, description="The image to process") image: ImageField = Field(default=None, description="The image to process")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Image Processor", "tags": ["image", "processor"]},
}
def run_processor(self, image): def run_processor(self, image):
# superclass just passes through image without processing # superclass just passes through image without processing
@ -240,7 +231,7 @@ class ImageProcessorInvocation(BaseInvocation, PILInvocationConfig):
image_category=ImageCategory.CONTROL, image_category=ImageCategory.CONTROL,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
node_id=self.id, node_id=self.id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate
) )
"""Builds an ImageOutput and its ImageField""" """Builds an ImageOutput and its ImageField"""
@ -248,16 +239,15 @@ class ImageProcessorInvocation(BaseInvocation, PILInvocationConfig):
return ImageOutput( return ImageOutput(
image=processed_image_field, image=processed_image_field,
# width=processed_image.width, # width=processed_image.width,
width=image_dto.width, width = image_dto.width,
# height=processed_image.height, # height=processed_image.height,
height=image_dto.height, height = image_dto.height,
# mode=processed_image.mode, # mode=processed_image.mode,
) )
class CannyImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig): class CannyImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig):
"""Canny edge detection for ControlNet""" """Canny edge detection for ControlNet"""
# fmt: off # fmt: off
type: Literal["canny_image_processor"] = "canny_image_processor" type: Literal["canny_image_processor"] = "canny_image_processor"
# Input # Input
@ -265,11 +255,6 @@ class CannyImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfi
high_threshold: int = Field(default=200, ge=0, le=255, description="The high threshold of the Canny pixel gradient (0-255)") high_threshold: int = Field(default=200, ge=0, le=255, description="The high threshold of the Canny pixel gradient (0-255)")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Canny Processor", "tags": ["controlnet", "canny", "image", "processor"]},
}
def run_processor(self, image): def run_processor(self, image):
canny_processor = CannyDetector() canny_processor = CannyDetector()
processed_image = canny_processor(image, self.low_threshold, self.high_threshold) processed_image = canny_processor(image, self.low_threshold, self.high_threshold)
@ -278,7 +263,6 @@ class CannyImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfi
class HedImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig): class HedImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig):
"""Applies HED edge detection to image""" """Applies HED edge detection to image"""
# fmt: off # fmt: off
type: Literal["hed_image_processor"] = "hed_image_processor" type: Literal["hed_image_processor"] = "hed_image_processor"
# Inputs # Inputs
@ -289,15 +273,9 @@ class HedImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig)
scribble: bool = Field(default=False, description="Whether to use scribble mode") scribble: bool = Field(default=False, description="Whether to use scribble mode")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Softedge(HED) Processor", "tags": ["controlnet", "softedge", "hed", "image", "processor"]},
}
def run_processor(self, image): def run_processor(self, image):
hed_processor = HEDdetector.from_pretrained("lllyasviel/Annotators") hed_processor = HEDdetector.from_pretrained("lllyasviel/Annotators")
processed_image = hed_processor( processed_image = hed_processor(image,
image,
detect_resolution=self.detect_resolution, detect_resolution=self.detect_resolution,
image_resolution=self.image_resolution, image_resolution=self.image_resolution,
# safe not supported in controlnet_aux v0.0.3 # safe not supported in controlnet_aux v0.0.3
@ -309,7 +287,6 @@ class HedImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig)
class LineartImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig): class LineartImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig):
"""Applies line art processing to image""" """Applies line art processing to image"""
# fmt: off # fmt: off
type: Literal["lineart_image_processor"] = "lineart_image_processor" type: Literal["lineart_image_processor"] = "lineart_image_processor"
# Inputs # Inputs
@ -318,22 +295,17 @@ class LineartImageProcessorInvocation(ImageProcessorInvocation, PILInvocationCon
coarse: bool = Field(default=False, description="Whether to use coarse mode") coarse: bool = Field(default=False, description="Whether to use coarse mode")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Lineart Processor", "tags": ["controlnet", "lineart", "image", "processor"]},
}
def run_processor(self, image): def run_processor(self, image):
lineart_processor = LineartDetector.from_pretrained("lllyasviel/Annotators") lineart_processor = LineartDetector.from_pretrained("lllyasviel/Annotators")
processed_image = lineart_processor( processed_image = lineart_processor(image,
image, detect_resolution=self.detect_resolution, image_resolution=self.image_resolution, coarse=self.coarse detect_resolution=self.detect_resolution,
) image_resolution=self.image_resolution,
coarse=self.coarse)
return processed_image return processed_image
class LineartAnimeImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig): class LineartAnimeImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig):
"""Applies line art anime processing to image""" """Applies line art anime processing to image"""
# fmt: off # fmt: off
type: Literal["lineart_anime_image_processor"] = "lineart_anime_image_processor" type: Literal["lineart_anime_image_processor"] = "lineart_anime_image_processor"
# Inputs # Inputs
@ -341,18 +313,9 @@ class LineartAnimeImageProcessorInvocation(ImageProcessorInvocation, PILInvocati
image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {
"title": "Lineart Anime Processor",
"tags": ["controlnet", "lineart", "anime", "image", "processor"],
},
}
def run_processor(self, image): def run_processor(self, image):
processor = LineartAnimeDetector.from_pretrained("lllyasviel/Annotators") processor = LineartAnimeDetector.from_pretrained("lllyasviel/Annotators")
processed_image = processor( processed_image = processor(image,
image,
detect_resolution=self.detect_resolution, detect_resolution=self.detect_resolution,
image_resolution=self.image_resolution, image_resolution=self.image_resolution,
) )
@ -361,7 +324,6 @@ class LineartAnimeImageProcessorInvocation(ImageProcessorInvocation, PILInvocati
class OpenposeImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig): class OpenposeImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig):
"""Applies Openpose processing to image""" """Applies Openpose processing to image"""
# fmt: off # fmt: off
type: Literal["openpose_image_processor"] = "openpose_image_processor" type: Literal["openpose_image_processor"] = "openpose_image_processor"
# Inputs # Inputs
@ -370,15 +332,9 @@ class OpenposeImageProcessorInvocation(ImageProcessorInvocation, PILInvocationCo
image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Openpose Processor", "tags": ["controlnet", "openpose", "image", "processor"]},
}
def run_processor(self, image): def run_processor(self, image):
openpose_processor = OpenposeDetector.from_pretrained("lllyasviel/Annotators") openpose_processor = OpenposeDetector.from_pretrained("lllyasviel/Annotators")
processed_image = openpose_processor( processed_image = openpose_processor(image,
image,
detect_resolution=self.detect_resolution, detect_resolution=self.detect_resolution,
image_resolution=self.image_resolution, image_resolution=self.image_resolution,
hand_and_face=self.hand_and_face, hand_and_face=self.hand_and_face,
@ -388,7 +344,6 @@ class OpenposeImageProcessorInvocation(ImageProcessorInvocation, PILInvocationCo
class MidasDepthImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig): class MidasDepthImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig):
"""Applies Midas depth processing to image""" """Applies Midas depth processing to image"""
# fmt: off # fmt: off
type: Literal["midas_depth_image_processor"] = "midas_depth_image_processor" type: Literal["midas_depth_image_processor"] = "midas_depth_image_processor"
# Inputs # Inputs
@ -398,15 +353,9 @@ class MidasDepthImageProcessorInvocation(ImageProcessorInvocation, PILInvocation
# depth_and_normal: bool = Field(default=False, description="whether to use depth and normal mode") # depth_and_normal: bool = Field(default=False, description="whether to use depth and normal mode")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Midas (Depth) Processor", "tags": ["controlnet", "midas", "depth", "image", "processor"]},
}
def run_processor(self, image): def run_processor(self, image):
midas_processor = MidasDetector.from_pretrained("lllyasviel/Annotators") midas_processor = MidasDetector.from_pretrained("lllyasviel/Annotators")
processed_image = midas_processor( processed_image = midas_processor(image,
image,
a=np.pi * self.a_mult, a=np.pi * self.a_mult,
bg_th=self.bg_th, bg_th=self.bg_th,
# dept_and_normal not supported in controlnet_aux v0.0.3 # dept_and_normal not supported in controlnet_aux v0.0.3
@ -417,7 +366,6 @@ class MidasDepthImageProcessorInvocation(ImageProcessorInvocation, PILInvocation
class NormalbaeImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig): class NormalbaeImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig):
"""Applies NormalBae processing to image""" """Applies NormalBae processing to image"""
# fmt: off # fmt: off
type: Literal["normalbae_image_processor"] = "normalbae_image_processor" type: Literal["normalbae_image_processor"] = "normalbae_image_processor"
# Inputs # Inputs
@ -425,22 +373,16 @@ class NormalbaeImageProcessorInvocation(ImageProcessorInvocation, PILInvocationC
image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Normal BAE Processor", "tags": ["controlnet", "normal", "bae", "image", "processor"]},
}
def run_processor(self, image): def run_processor(self, image):
normalbae_processor = NormalBaeDetector.from_pretrained("lllyasviel/Annotators") normalbae_processor = NormalBaeDetector.from_pretrained("lllyasviel/Annotators")
processed_image = normalbae_processor( processed_image = normalbae_processor(image,
image, detect_resolution=self.detect_resolution, image_resolution=self.image_resolution detect_resolution=self.detect_resolution,
) image_resolution=self.image_resolution)
return processed_image return processed_image
class MlsdImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig): class MlsdImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig):
"""Applies MLSD processing to image""" """Applies MLSD processing to image"""
# fmt: off # fmt: off
type: Literal["mlsd_image_processor"] = "mlsd_image_processor" type: Literal["mlsd_image_processor"] = "mlsd_image_processor"
# Inputs # Inputs
@ -450,26 +392,18 @@ class MlsdImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig
thr_d: float = Field(default=0.1, ge=0, description="MLSD parameter `thr_d`") thr_d: float = Field(default=0.1, ge=0, description="MLSD parameter `thr_d`")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "MLSD Processor", "tags": ["controlnet", "mlsd", "image", "processor"]},
}
def run_processor(self, image): def run_processor(self, image):
mlsd_processor = MLSDdetector.from_pretrained("lllyasviel/Annotators") mlsd_processor = MLSDdetector.from_pretrained("lllyasviel/Annotators")
processed_image = mlsd_processor( processed_image = mlsd_processor(image,
image,
detect_resolution=self.detect_resolution, detect_resolution=self.detect_resolution,
image_resolution=self.image_resolution, image_resolution=self.image_resolution,
thr_v=self.thr_v, thr_v=self.thr_v,
thr_d=self.thr_d, thr_d=self.thr_d)
)
return processed_image return processed_image
class PidiImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig): class PidiImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig):
"""Applies PIDI processing to image""" """Applies PIDI processing to image"""
# fmt: off # fmt: off
type: Literal["pidi_image_processor"] = "pidi_image_processor" type: Literal["pidi_image_processor"] = "pidi_image_processor"
# Inputs # Inputs
@ -479,26 +413,18 @@ class PidiImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig
scribble: bool = Field(default=False, description="Whether to use scribble mode") scribble: bool = Field(default=False, description="Whether to use scribble mode")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "PIDI Processor", "tags": ["controlnet", "pidi", "image", "processor"]},
}
def run_processor(self, image): def run_processor(self, image):
pidi_processor = PidiNetDetector.from_pretrained("lllyasviel/Annotators") pidi_processor = PidiNetDetector.from_pretrained("lllyasviel/Annotators")
processed_image = pidi_processor( processed_image = pidi_processor(image,
image,
detect_resolution=self.detect_resolution, detect_resolution=self.detect_resolution,
image_resolution=self.image_resolution, image_resolution=self.image_resolution,
safe=self.safe, safe=self.safe,
scribble=self.scribble, scribble=self.scribble)
)
return processed_image return processed_image
class ContentShuffleImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig): class ContentShuffleImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig):
"""Applies content shuffle processing to image""" """Applies content shuffle processing to image"""
# fmt: off # fmt: off
type: Literal["content_shuffle_image_processor"] = "content_shuffle_image_processor" type: Literal["content_shuffle_image_processor"] = "content_shuffle_image_processor"
# Inputs # Inputs
@ -509,23 +435,14 @@ class ContentShuffleImageProcessorInvocation(ImageProcessorInvocation, PILInvoca
f: Optional[int] = Field(default=256, ge=0, description="Content shuffle `f` parameter") f: Optional[int] = Field(default=256, ge=0, description="Content shuffle `f` parameter")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {
"title": "Content Shuffle Processor",
"tags": ["controlnet", "contentshuffle", "image", "processor"],
},
}
def run_processor(self, image): def run_processor(self, image):
content_shuffle_processor = ContentShuffleDetector() content_shuffle_processor = ContentShuffleDetector()
processed_image = content_shuffle_processor( processed_image = content_shuffle_processor(image,
image,
detect_resolution=self.detect_resolution, detect_resolution=self.detect_resolution,
image_resolution=self.image_resolution, image_resolution=self.image_resolution,
h=self.h, h=self.h,
w=self.w, w=self.w,
f=self.f, f=self.f
) )
return processed_image return processed_image
@ -533,16 +450,10 @@ class ContentShuffleImageProcessorInvocation(ImageProcessorInvocation, PILInvoca
# should work with controlnet_aux >= 0.0.4 and timm <= 0.6.13 # should work with controlnet_aux >= 0.0.4 and timm <= 0.6.13
class ZoeDepthImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig): class ZoeDepthImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig):
"""Applies Zoe depth processing to image""" """Applies Zoe depth processing to image"""
# fmt: off # fmt: off
type: Literal["zoe_depth_image_processor"] = "zoe_depth_image_processor" type: Literal["zoe_depth_image_processor"] = "zoe_depth_image_processor"
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Zoe (Depth) Processor", "tags": ["controlnet", "zoe", "depth", "image", "processor"]},
}
def run_processor(self, image): def run_processor(self, image):
zoe_depth_processor = ZoeDetector.from_pretrained("lllyasviel/Annotators") zoe_depth_processor = ZoeDetector.from_pretrained("lllyasviel/Annotators")
processed_image = zoe_depth_processor(image) processed_image = zoe_depth_processor(image)
@ -551,7 +462,6 @@ class ZoeDepthImageProcessorInvocation(ImageProcessorInvocation, PILInvocationCo
class MediapipeFaceProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig): class MediapipeFaceProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig):
"""Applies mediapipe face processing to image""" """Applies mediapipe face processing to image"""
# fmt: off # fmt: off
type: Literal["mediapipe_face_processor"] = "mediapipe_face_processor" type: Literal["mediapipe_face_processor"] = "mediapipe_face_processor"
# Inputs # Inputs
@ -559,24 +469,17 @@ class MediapipeFaceProcessorInvocation(ImageProcessorInvocation, PILInvocationCo
min_confidence: float = Field(default=0.5, ge=0, le=1, description="Minimum confidence for face detection") min_confidence: float = Field(default=0.5, ge=0, le=1, description="Minimum confidence for face detection")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Mediapipe Processor", "tags": ["controlnet", "mediapipe", "image", "processor"]},
}
def run_processor(self, image): def run_processor(self, image):
# MediaPipeFaceDetector throws an error if image has alpha channel # MediaPipeFaceDetector throws an error if image has alpha channel
# so convert to RGB if needed # so convert to RGB if needed
if image.mode == "RGBA": if image.mode == 'RGBA':
image = image.convert("RGB") image = image.convert('RGB')
mediapipe_face_processor = MediapipeFaceDetector() mediapipe_face_processor = MediapipeFaceDetector()
processed_image = mediapipe_face_processor(image, max_faces=self.max_faces, min_confidence=self.min_confidence) processed_image = mediapipe_face_processor(image, max_faces=self.max_faces, min_confidence=self.min_confidence)
return processed_image return processed_image
class LeresImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig): class LeresImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig):
"""Applies leres processing to image""" """Applies leres processing to image"""
# fmt: off # fmt: off
type: Literal["leres_image_processor"] = "leres_image_processor" type: Literal["leres_image_processor"] = "leres_image_processor"
# Inputs # Inputs
@ -587,25 +490,19 @@ class LeresImageProcessorInvocation(ImageProcessorInvocation, PILInvocationConfi
image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image") image_resolution: int = Field(default=512, ge=0, description="The pixel resolution for the output image")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Leres (Depth) Processor", "tags": ["controlnet", "leres", "depth", "image", "processor"]},
}
def run_processor(self, image): def run_processor(self, image):
leres_processor = LeresDetector.from_pretrained("lllyasviel/Annotators") leres_processor = LeresDetector.from_pretrained("lllyasviel/Annotators")
processed_image = leres_processor( processed_image = leres_processor(image,
image,
thr_a=self.thr_a, thr_a=self.thr_a,
thr_b=self.thr_b, thr_b=self.thr_b,
boost=self.boost, boost=self.boost,
detect_resolution=self.detect_resolution, detect_resolution=self.detect_resolution,
image_resolution=self.image_resolution, image_resolution=self.image_resolution)
)
return processed_image return processed_image
class TileResamplerProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig): class TileResamplerProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig):
# fmt: off # fmt: off
type: Literal["tile_image_processor"] = "tile_image_processor" type: Literal["tile_image_processor"] = "tile_image_processor"
# Inputs # Inputs
@ -613,17 +510,8 @@ class TileResamplerProcessorInvocation(ImageProcessorInvocation, PILInvocationCo
down_sampling_rate: float = Field(default=1.0, ge=1.0, le=8.0, description="Down sampling rate") down_sampling_rate: float = Field(default=1.0, ge=1.0, le=8.0, description="Down sampling rate")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {
"title": "Tile Resample Processor",
"tags": ["controlnet", "tile", "resample", "image", "processor"],
},
}
# tile_resample copied from sd-webui-controlnet/scripts/processor.py # tile_resample copied from sd-webui-controlnet/scripts/processor.py
def tile_resample( def tile_resample(self,
self,
np_img: np.ndarray, np_img: np.ndarray,
res=512, # never used? res=512, # never used?
down_sampling_rate=1.0, down_sampling_rate=1.0,
@ -639,41 +527,31 @@ class TileResamplerProcessorInvocation(ImageProcessorInvocation, PILInvocationCo
def run_processor(self, img): def run_processor(self, img):
np_img = np.array(img, dtype=np.uint8) np_img = np.array(img, dtype=np.uint8)
processed_np_image = self.tile_resample( processed_np_image = self.tile_resample(np_img,
np_img, #res=self.tile_size,
# res=self.tile_size, down_sampling_rate=self.down_sampling_rate
down_sampling_rate=self.down_sampling_rate,
) )
processed_image = Image.fromarray(processed_np_image) processed_image = Image.fromarray(processed_np_image)
return processed_image return processed_image
class SegmentAnythingProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig): class SegmentAnythingProcessorInvocation(ImageProcessorInvocation, PILInvocationConfig):
"""Applies segment anything processing to image""" """Applies segment anything processing to image"""
# fmt: off # fmt: off
type: Literal["segment_anything_processor"] = "segment_anything_processor" type: Literal["segment_anything_processor"] = "segment_anything_processor"
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {
"title": "Segment Anything Processor",
"tags": ["controlnet", "segment", "anything", "sam", "image", "processor"],
},
}
def run_processor(self, image): def run_processor(self, image):
# segment_anything_processor = SamDetector.from_pretrained("ybelkada/segment-anything", subfolder="checkpoints") # segment_anything_processor = SamDetector.from_pretrained("ybelkada/segment-anything", subfolder="checkpoints")
segment_anything_processor = SamDetectorReproducibleColors.from_pretrained( segment_anything_processor = SamDetectorReproducibleColors.from_pretrained("ybelkada/segment-anything", subfolder="checkpoints")
"ybelkada/segment-anything", subfolder="checkpoints"
)
np_img = np.array(image, dtype=np.uint8) np_img = np.array(image, dtype=np.uint8)
processed_image = segment_anything_processor(np_img) processed_image = segment_anything_processor(np_img)
return processed_image return processed_image
class SamDetectorReproducibleColors(SamDetector): class SamDetectorReproducibleColors(SamDetector):
# overriding SamDetector.show_anns() method to use reproducible colors for segmentation image # overriding SamDetector.show_anns() method to use reproducible colors for segmentation image
# base class show_anns() method randomizes colors, # base class show_anns() method randomizes colors,
# which seems to also lead to non-reproducible image generation # which seems to also lead to non-reproducible image generation
@ -681,12 +559,12 @@ class SamDetectorReproducibleColors(SamDetector):
def show_anns(self, anns: List[Dict]): def show_anns(self, anns: List[Dict]):
if len(anns) == 0: if len(anns) == 0:
return return
sorted_anns = sorted(anns, key=(lambda x: x["area"]), reverse=True) sorted_anns = sorted(anns, key=(lambda x: x['area']), reverse=True)
h, w = anns[0]["segmentation"].shape h, w = anns[0]['segmentation'].shape
final_img = Image.fromarray(np.zeros((h, w, 3), dtype=np.uint8), mode="RGB") final_img = Image.fromarray(np.zeros((h, w, 3), dtype=np.uint8), mode="RGB")
palette = ade_palette() palette = ade_palette()
for i, ann in enumerate(sorted_anns): for i, ann in enumerate(sorted_anns):
m = ann["segmentation"] m = ann['segmentation']
img = np.empty((m.shape[0], m.shape[1], 3), dtype=np.uint8) img = np.empty((m.shape[0], m.shape[1], 3), dtype=np.uint8)
# doing modulo just in case number of annotated regions exceeds number of colors in palette # doing modulo just in case number of annotated regions exceeds number of colors in palette
ann_color = palette[i % len(palette)] ann_color = palette[i % len(palette)]

View File

@ -35,11 +35,6 @@ class CvInpaintInvocation(BaseInvocation, CvInvocationConfig):
mask: ImageField = Field(default=None, description="The mask to use when inpainting") mask: ImageField = Field(default=None, description="The mask to use when inpainting")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "OpenCV Inpaint", "tags": ["opencv", "inpaint"]},
}
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.services.images.get_pil_image(self.image.image_name) image = context.services.images.get_pil_image(self.image.image_name)
mask = context.services.images.get_pil_image(self.mask.image_name) mask = context.services.images.get_pil_image(self.mask.image_name)

View File

@ -6,7 +6,8 @@ from typing import Literal, Optional, get_args
import torch import torch
from pydantic import Field from pydantic import Field
from invokeai.app.models.image import ColorField, ImageCategory, ImageField, ResourceOrigin from invokeai.app.models.image import (ColorField, ImageCategory, ImageField,
ResourceOrigin)
from invokeai.app.util.misc import SEED_MAX, get_random_seed from invokeai.app.util.misc import SEED_MAX, get_random_seed
from invokeai.backend.generator.inpaint import infill_methods from invokeai.backend.generator.inpaint import infill_methods
@ -24,12 +25,13 @@ from contextlib import contextmanager, ExitStack, ContextDecorator
SAMPLER_NAME_VALUES = Literal[tuple(InvokeAIGenerator.schedulers())] SAMPLER_NAME_VALUES = Literal[tuple(InvokeAIGenerator.schedulers())]
INFILL_METHODS = Literal[tuple(infill_methods())] INFILL_METHODS = Literal[tuple(infill_methods())]
DEFAULT_INFILL_METHOD = "patchmatch" if "patchmatch" in get_args(INFILL_METHODS) else "tile" DEFAULT_INFILL_METHOD = (
"patchmatch" if "patchmatch" in get_args(INFILL_METHODS) else "tile"
)
from .latent import get_scheduler from .latent import get_scheduler
class OldModelContext(ContextDecorator): class OldModelContext(ContextDecorator):
model: StableDiffusionGeneratorPipeline model: StableDiffusionGeneratorPipeline
@ -42,7 +44,6 @@ class OldModelContext(ContextDecorator):
def __exit__(self, *exc): def __exit__(self, *exc):
return False return False
class OldModelInfo: class OldModelInfo:
name: str name: str
hash: str hash: str
@ -63,34 +64,20 @@ class InpaintInvocation(BaseInvocation):
positive_conditioning: Optional[ConditioningField] = Field(description="Positive conditioning for generation") positive_conditioning: Optional[ConditioningField] = Field(description="Positive conditioning for generation")
negative_conditioning: Optional[ConditioningField] = Field(description="Negative conditioning for generation") negative_conditioning: Optional[ConditioningField] = Field(description="Negative conditioning for generation")
seed: int = Field( seed: int = Field(ge=0, le=SEED_MAX, description="The seed to use (omit for random)", default_factory=get_random_seed)
ge=0, le=SEED_MAX, description="The seed to use (omit for random)", default_factory=get_random_seed
)
steps: int = Field(default=30, gt=0, description="The number of steps to use to generate the image") steps: int = Field(default=30, gt=0, description="The number of steps to use to generate the image")
width: int = Field( width: int = Field(default=512, multiple_of=8, gt=0, description="The width of the resulting image", )
default=512, height: int = Field(default=512, multiple_of=8, gt=0, description="The height of the resulting image", )
multiple_of=8, cfg_scale: float = Field(default=7.5, ge=1, description="The Classifier-Free Guidance, higher values may result in a result closer to the prompt", )
gt=0, scheduler: SAMPLER_NAME_VALUES = Field(default="euler", description="The scheduler to use" )
description="The width of the resulting image",
)
height: int = Field(
default=512,
multiple_of=8,
gt=0,
description="The height of the resulting image",
)
cfg_scale: float = Field(
default=7.5,
ge=1,
description="The Classifier-Free Guidance, higher values may result in a result closer to the prompt",
)
scheduler: SAMPLER_NAME_VALUES = Field(default="euler", description="The scheduler to use")
unet: UNetField = Field(default=None, description="UNet model") unet: UNetField = Field(default=None, description="UNet model")
vae: VaeField = Field(default=None, description="Vae model") vae: VaeField = Field(default=None, description="Vae model")
# Inputs # Inputs
image: Optional[ImageField] = Field(description="The input image") image: Optional[ImageField] = Field(description="The input image")
strength: float = Field(default=0.75, gt=0, le=1, description="The strength of the original image") strength: float = Field(
default=0.75, gt=0, le=1, description="The strength of the original image"
)
fit: bool = Field( fit: bool = Field(
default=True, default=True,
description="Whether or not the result should be fit to the aspect ratio of the input image", description="Whether or not the result should be fit to the aspect ratio of the input image",
@ -99,10 +86,18 @@ class InpaintInvocation(BaseInvocation):
# Inputs # Inputs
mask: Optional[ImageField] = Field(description="The mask") mask: Optional[ImageField] = Field(description="The mask")
seam_size: int = Field(default=96, ge=1, description="The seam inpaint size (px)") seam_size: int = Field(default=96, ge=1, description="The seam inpaint size (px)")
seam_blur: int = Field(default=16, ge=0, description="The seam inpaint blur radius (px)") seam_blur: int = Field(
seam_strength: float = Field(default=0.75, gt=0, le=1, description="The seam inpaint strength") default=16, ge=0, description="The seam inpaint blur radius (px)"
seam_steps: int = Field(default=30, ge=1, description="The number of steps to use for seam inpaint") )
tile_size: int = Field(default=32, ge=1, description="The tile infill method size (px)") seam_strength: float = Field(
default=0.75, gt=0, le=1, description="The seam inpaint strength"
)
seam_steps: int = Field(
default=30, ge=1, description="The number of steps to use for seam inpaint"
)
tile_size: int = Field(
default=32, ge=1, description="The tile infill method size (px)"
)
infill_method: INFILL_METHODS = Field( infill_method: INFILL_METHODS = Field(
default=DEFAULT_INFILL_METHOD, default=DEFAULT_INFILL_METHOD,
description="The method used to infill empty regions (px)", description="The method used to infill empty regions (px)",
@ -133,7 +128,9 @@ class InpaintInvocation(BaseInvocation):
# Schema customisation # Schema customisation
class Config(InvocationConfig): class Config(InvocationConfig):
schema_extra = { schema_extra = {
"ui": {"tags": ["stable-diffusion", "image"], "title": "Inpaint"}, "ui": {
"tags": ["stable-diffusion", "image"],
},
} }
def dispatch_progress( def dispatch_progress(
@ -149,13 +146,9 @@ class InpaintInvocation(BaseInvocation):
source_node_id=source_node_id, source_node_id=source_node_id,
) )
def get_conditioning(self, context, unet): def get_conditioning(self, context):
positive_cond_data = context.services.latents.get(self.positive_conditioning.conditioning_name) c, extra_conditioning_info = context.services.latents.get(self.positive_conditioning.conditioning_name)
c = positive_cond_data.conditionings[0].embeds.to(device=unet.device, dtype=unet.dtype) uc, _ = context.services.latents.get(self.negative_conditioning.conditioning_name)
extra_conditioning_info = positive_cond_data.conditionings[0].extra_conditioning
negative_cond_data = context.services.latents.get(self.negative_conditioning.conditioning_name)
uc = negative_cond_data.conditionings[0].embeds.to(device=unet.device, dtype=unet.dtype)
return (uc, c, extra_conditioning_info) return (uc, c, extra_conditioning_info)
@ -164,23 +157,18 @@ class InpaintInvocation(BaseInvocation):
def _lora_loader(): def _lora_loader():
for lora in self.unet.loras: for lora in self.unet.loras:
lora_info = context.services.model_manager.get_model( lora_info = context.services.model_manager.get_model(
**lora.dict(exclude={"weight"}), **lora.dict(exclude={"weight"}))
context=context,
)
yield (lora_info.context.model, lora.weight) yield (lora_info.context.model, lora.weight)
del lora_info del lora_info
return return
unet_info = context.services.model_manager.get_model( unet_info = context.services.model_manager.get_model(**self.unet.unet.dict())
**self.unet.unet.dict(), vae_info = context.services.model_manager.get_model(**self.vae.vae.dict())
context=context,
) with vae_info as vae,\
vae_info = context.services.model_manager.get_model( ModelPatcher.apply_lora_unet(unet_info.context.model, _lora_loader()),\
**self.vae.vae.dict(), unet_info as unet:
context=context,
)
with vae_info as vae, ModelPatcher.apply_lora_unet(unet_info.context.model, _lora_loader()), unet_info as unet:
device = context.services.model_manager.mgr.cache.execution_device device = context.services.model_manager.mgr.cache.execution_device
dtype = context.services.model_manager.mgr.cache.precision dtype = context.services.model_manager.mgr.cache.precision
@ -204,13 +192,24 @@ class InpaintInvocation(BaseInvocation):
) )
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
image = None if self.image is None else context.services.images.get_pil_image(self.image.image_name) image = (
mask = None if self.mask is None else context.services.images.get_pil_image(self.mask.image_name) None
if self.image is None
else context.services.images.get_pil_image(self.image.image_name)
)
mask = (
None
if self.mask is None
else context.services.images.get_pil_image(self.mask.image_name)
)
# Get the source node id (we are invoking the prepared node) # Get the source node id (we are invoking the prepared node)
graph_execution_state = context.services.graph_execution_manager.get(context.graph_execution_state_id) graph_execution_state = context.services.graph_execution_manager.get(
context.graph_execution_state_id
)
source_node_id = graph_execution_state.prepared_source_mapping[self.id] source_node_id = graph_execution_state.prepared_source_mapping[self.id]
conditioning = self.get_conditioning(context)
scheduler = get_scheduler( scheduler = get_scheduler(
context=context, context=context,
scheduler_info=self.unet.scheduler, scheduler_info=self.unet.scheduler,
@ -218,8 +217,6 @@ class InpaintInvocation(BaseInvocation):
) )
with self.load_model_old_way(context, scheduler) as model: with self.load_model_old_way(context, scheduler) as model:
conditioning = self.get_conditioning(context, model.context.model.unet)
outputs = Inpaint(model).generate( outputs = Inpaint(model).generate(
conditioning=conditioning, conditioning=conditioning,
scheduler=scheduler, scheduler=scheduler,

View File

@ -4,25 +4,60 @@ from typing import Literal, Optional
import numpy import numpy
from PIL import Image, ImageFilter, ImageOps, ImageChops from PIL import Image, ImageFilter, ImageOps, ImageChops
from pydantic import Field from pydantic import BaseModel, Field
from pathlib import Path
from typing import Union from typing import Union
from invokeai.app.invocations.metadata import CoreMetadata
from ..models.image import ( from ..models.image import ImageCategory, ImageField, ResourceOrigin
ImageCategory,
ImageField,
ResourceOrigin,
PILInvocationConfig,
ImageOutput,
MaskOutput,
)
from .baseinvocation import ( from .baseinvocation import (
BaseInvocation, BaseInvocation,
BaseInvocationOutput,
InvocationContext, InvocationContext,
InvocationConfig, InvocationConfig,
) )
from invokeai.backend.image_util.safety_checker import SafetyChecker
from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark
class PILInvocationConfig(BaseModel):
"""Helper class to provide all PIL invocations with additional config"""
class Config(InvocationConfig):
schema_extra = {
"ui": {
"tags": ["PIL", "image"],
},
}
class ImageOutput(BaseInvocationOutput):
"""Base class for invocations that output an image"""
# fmt: off
type: Literal["image_output"] = "image_output"
image: ImageField = Field(default=None, description="The output image")
width: int = Field(description="The width of the image in pixels")
height: int = Field(description="The height of the image in pixels")
# fmt: on
class Config:
schema_extra = {"required": ["type", "image", "width", "height"]}
class MaskOutput(BaseInvocationOutput):
"""Base class for invocations that output a mask"""
# fmt: off
type: Literal["mask"] = "mask"
mask: ImageField = Field(default=None, description="The output mask")
width: int = Field(description="The width of the mask in pixels")
height: int = Field(description="The height of the mask in pixels")
# fmt: on
class Config:
schema_extra = {
"required": [
"type",
"mask",
]
}
class LoadImageInvocation(BaseInvocation): class LoadImageInvocation(BaseInvocation):
@ -36,12 +71,6 @@ class LoadImageInvocation(BaseInvocation):
default=None, description="The image to load" default=None, description="The image to load"
) )
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Load Image", "tags": ["image", "load"]},
}
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.services.images.get_pil_image(self.image.image_name) image = context.services.images.get_pil_image(self.image.image_name)
@ -58,12 +87,9 @@ class ShowImageInvocation(BaseInvocation):
type: Literal["show_image"] = "show_image" type: Literal["show_image"] = "show_image"
# Inputs # Inputs
image: Optional[ImageField] = Field(default=None, description="The image to show") image: Optional[ImageField] = Field(
default=None, description="The image to show"
class Config(InvocationConfig): )
schema_extra = {
"ui": {"title": "Show Image", "tags": ["image", "show"]},
}
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.services.images.get_pil_image(self.image.image_name) image = context.services.images.get_pil_image(self.image.image_name)
@ -93,15 +119,12 @@ class ImageCropInvocation(BaseInvocation, PILInvocationConfig):
height: int = Field(default=512, gt=0, description="The height of the crop rectangle") height: int = Field(default=512, gt=0, description="The height of the crop rectangle")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Crop Image", "tags": ["image", "crop"]},
}
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.services.images.get_pil_image(self.image.image_name) image = context.services.images.get_pil_image(self.image.image_name)
image_crop = Image.new(mode="RGBA", size=(self.width, self.height), color=(0, 0, 0, 0)) image_crop = Image.new(
mode="RGBA", size=(self.width, self.height), color=(0, 0, 0, 0)
)
image_crop.paste(image, (-self.x, -self.y)) image_crop.paste(image, (-self.x, -self.y))
image_dto = context.services.images.create( image_dto = context.services.images.create(
@ -134,16 +157,15 @@ class ImagePasteInvocation(BaseInvocation, PILInvocationConfig):
y: int = Field(default=0, description="The top y coordinate at which to paste the image") y: int = Field(default=0, description="The top y coordinate at which to paste the image")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Paste Image", "tags": ["image", "paste"]},
}
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
base_image = context.services.images.get_pil_image(self.base_image.image_name) base_image = context.services.images.get_pil_image(self.base_image.image_name)
image = context.services.images.get_pil_image(self.image.image_name) image = context.services.images.get_pil_image(self.image.image_name)
mask = ( mask = (
None if self.mask is None else ImageOps.invert(context.services.images.get_pil_image(self.mask.image_name)) None
if self.mask is None
else ImageOps.invert(
context.services.images.get_pil_image(self.mask.image_name)
)
) )
# TODO: probably shouldn't invert mask here... should user be required to do it? # TODO: probably shouldn't invert mask here... should user be required to do it?
@ -152,7 +174,9 @@ class ImagePasteInvocation(BaseInvocation, PILInvocationConfig):
max_x = max(base_image.width, image.width + self.x) max_x = max(base_image.width, image.width + self.x)
max_y = max(base_image.height, image.height + self.y) max_y = max(base_image.height, image.height + self.y)
new_image = Image.new(mode="RGBA", size=(max_x - min_x, max_y - min_y), color=(0, 0, 0, 0)) new_image = Image.new(
mode="RGBA", size=(max_x - min_x, max_y - min_y), color=(0, 0, 0, 0)
)
new_image.paste(base_image, (abs(min_x), abs(min_y))) new_image.paste(base_image, (abs(min_x), abs(min_y)))
new_image.paste(image, (max(0, self.x), max(0, self.y)), mask=mask) new_image.paste(image, (max(0, self.x), max(0, self.y)), mask=mask)
@ -183,11 +207,6 @@ class MaskFromAlphaInvocation(BaseInvocation, PILInvocationConfig):
invert: bool = Field(default=False, description="Whether or not to invert the mask") invert: bool = Field(default=False, description="Whether or not to invert the mask")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Mask From Alpha", "tags": ["image", "mask", "alpha"]},
}
def invoke(self, context: InvocationContext) -> MaskOutput: def invoke(self, context: InvocationContext) -> MaskOutput:
image = context.services.images.get_pil_image(self.image.image_name) image = context.services.images.get_pil_image(self.image.image_name)
@ -222,11 +241,6 @@ class ImageMultiplyInvocation(BaseInvocation, PILInvocationConfig):
image2: Optional[ImageField] = Field(default=None, description="The second image to multiply") image2: Optional[ImageField] = Field(default=None, description="The second image to multiply")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Multiply Images", "tags": ["image", "multiply"]},
}
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
image1 = context.services.images.get_pil_image(self.image1.image_name) image1 = context.services.images.get_pil_image(self.image1.image_name)
image2 = context.services.images.get_pil_image(self.image2.image_name) image2 = context.services.images.get_pil_image(self.image2.image_name)
@ -263,11 +277,6 @@ class ImageChannelInvocation(BaseInvocation, PILInvocationConfig):
channel: IMAGE_CHANNELS = Field(default="A", description="The channel to get") channel: IMAGE_CHANNELS = Field(default="A", description="The channel to get")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Image Channel", "tags": ["image", "channel"]},
}
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.services.images.get_pil_image(self.image.image_name) image = context.services.images.get_pil_image(self.image.image_name)
@ -303,11 +312,6 @@ class ImageConvertInvocation(BaseInvocation, PILInvocationConfig):
mode: IMAGE_MODES = Field(default="L", description="The mode to convert to") mode: IMAGE_MODES = Field(default="L", description="The mode to convert to")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Convert Image", "tags": ["image", "convert"]},
}
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.services.images.get_pil_image(self.image.image_name) image = context.services.images.get_pil_image(self.image.image_name)
@ -341,16 +345,13 @@ class ImageBlurInvocation(BaseInvocation, PILInvocationConfig):
blur_type: Literal["gaussian", "box"] = Field(default="gaussian", description="The type of blur") blur_type: Literal["gaussian", "box"] = Field(default="gaussian", description="The type of blur")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Blur Image", "tags": ["image", "blur"]},
}
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.services.images.get_pil_image(self.image.image_name) image = context.services.images.get_pil_image(self.image.image_name)
blur = ( blur = (
ImageFilter.GaussianBlur(self.radius) if self.blur_type == "gaussian" else ImageFilter.BoxBlur(self.radius) ImageFilter.GaussianBlur(self.radius)
if self.blur_type == "gaussian"
else ImageFilter.BoxBlur(self.radius)
) )
blur_image = image.filter(blur) blur_image = image.filter(blur)
@ -403,11 +404,6 @@ class ImageResizeInvocation(BaseInvocation, PILInvocationConfig):
resample_mode: PIL_RESAMPLING_MODES = Field(default="bicubic", description="The resampling mode") resample_mode: PIL_RESAMPLING_MODES = Field(default="bicubic", description="The resampling mode")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Resize Image", "tags": ["image", "resize"]},
}
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.services.images.get_pil_image(self.image.image_name) image = context.services.images.get_pil_image(self.image.image_name)
@ -442,15 +438,10 @@ class ImageScaleInvocation(BaseInvocation, PILInvocationConfig):
# Inputs # Inputs
image: Optional[ImageField] = Field(default=None, description="The image to scale") image: Optional[ImageField] = Field(default=None, description="The image to scale")
scale_factor: Optional[float] = Field(default=2.0, gt=0, description="The factor by which to scale the image") scale_factor: float = Field(gt=0, description="The factor by which to scale the image")
resample_mode: PIL_RESAMPLING_MODES = Field(default="bicubic", description="The resampling mode") resample_mode: PIL_RESAMPLING_MODES = Field(default="bicubic", description="The resampling mode")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Scale Image", "tags": ["image", "scale"]},
}
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.services.images.get_pil_image(self.image.image_name) image = context.services.images.get_pil_image(self.image.image_name)
@ -491,11 +482,6 @@ class ImageLerpInvocation(BaseInvocation, PILInvocationConfig):
max: int = Field(default=255, ge=0, le=255, description="The maximum output value") max: int = Field(default=255, ge=0, le=255, description="The maximum output value")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Image Linear Interpolation", "tags": ["image", "linear", "interpolation", "lerp"]},
}
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.services.images.get_pil_image(self.image.image_name) image = context.services.images.get_pil_image(self.image.image_name)
@ -532,19 +518,16 @@ class ImageInverseLerpInvocation(BaseInvocation, PILInvocationConfig):
max: int = Field(default=255, ge=0, le=255, description="The maximum input value") max: int = Field(default=255, ge=0, le=255, description="The maximum input value")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {
"title": "Image Inverse Linear Interpolation",
"tags": ["image", "linear", "interpolation", "inverse"],
},
}
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.services.images.get_pil_image(self.image.image_name) image = context.services.images.get_pil_image(self.image.image_name)
image_arr = numpy.asarray(image, dtype=numpy.float32) image_arr = numpy.asarray(image, dtype=numpy.float32)
image_arr = numpy.minimum(numpy.maximum(image_arr - self.min, 0) / float(self.max - self.min), 1) * 255 image_arr = (
numpy.minimum(
numpy.maximum(image_arr - self.min, 0) / float(self.max - self.min), 1
)
* 255
)
ilerp_image = Image.fromarray(numpy.uint8(image_arr)) ilerp_image = Image.fromarray(numpy.uint8(image_arr))
@ -562,91 +545,3 @@ class ImageInverseLerpInvocation(BaseInvocation, PILInvocationConfig):
width=image_dto.width, width=image_dto.width,
height=image_dto.height, height=image_dto.height,
) )
class ImageNSFWBlurInvocation(BaseInvocation, PILInvocationConfig):
"""Add blur to NSFW-flagged images"""
# fmt: off
type: Literal["img_nsfw"] = "img_nsfw"
# Inputs
image: Optional[ImageField] = Field(default=None, description="The image to check")
metadata: Optional[CoreMetadata] = Field(default=None, description="Optional core metadata to be written to the image")
# fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Blur NSFW Images", "tags": ["image", "nsfw", "checker"]},
}
def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.services.images.get_pil_image(self.image.image_name)
logger = context.services.logger
logger.debug("Running NSFW checker")
if SafetyChecker.has_nsfw_concept(image):
logger.info("A potentially NSFW image has been detected. Image will be blurred.")
blurry_image = image.filter(filter=ImageFilter.GaussianBlur(radius=32))
caution = self._get_caution_img()
blurry_image.paste(caution, (0, 0), caution)
image = blurry_image
image_dto = context.services.images.create(
image=image,
image_origin=ResourceOrigin.INTERNAL,
image_category=ImageCategory.GENERAL,
node_id=self.id,
session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate,
metadata=self.metadata.dict() if self.metadata else None,
)
return ImageOutput(
image=ImageField(image_name=image_dto.image_name),
width=image_dto.width,
height=image_dto.height,
)
def _get_caution_img(self) -> Image:
import invokeai.app.assets.images as image_assets
caution = Image.open(Path(image_assets.__path__[0]) / "caution.png")
return caution.resize((caution.width // 2, caution.height // 2))
class ImageWatermarkInvocation(BaseInvocation, PILInvocationConfig):
"""Add an invisible watermark to an image"""
# fmt: off
type: Literal["img_watermark"] = "img_watermark"
# Inputs
image: Optional[ImageField] = Field(default=None, description="The image to check")
text: str = Field(default='InvokeAI', description="Watermark text")
metadata: Optional[CoreMetadata] = Field(default=None, description="Optional core metadata to be written to the image")
# fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Add Invisible Watermark", "tags": ["image", "watermark", "invisible"]},
}
def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.services.images.get_pil_image(self.image.image_name)
new_image = InvisibleWatermark.add_watermark(image, self.text)
image_dto = context.services.images.create(
image=new_image,
image_origin=ResourceOrigin.INTERNAL,
image_category=ImageCategory.GENERAL,
node_id=self.id,
session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate,
metadata=self.metadata.dict() if self.metadata else None,
)
return ImageOutput(
image=ImageField(image_name=image_dto.image_name),
width=image_dto.width,
height=image_dto.height,
)

View File

@ -14,7 +14,6 @@ from invokeai.backend.image_util.patchmatch import PatchMatch
from ..models.image import ColorField, ImageCategory, ImageField, ResourceOrigin from ..models.image import ColorField, ImageCategory, ImageField, ResourceOrigin
from .baseinvocation import ( from .baseinvocation import (
BaseInvocation, BaseInvocation,
InvocationConfig,
InvocationContext, InvocationContext,
) )
@ -30,7 +29,9 @@ def infill_methods() -> list[str]:
INFILL_METHODS = Literal[tuple(infill_methods())] INFILL_METHODS = Literal[tuple(infill_methods())]
DEFAULT_INFILL_METHOD = "patchmatch" if "patchmatch" in get_args(INFILL_METHODS) else "tile" DEFAULT_INFILL_METHOD = (
"patchmatch" if "patchmatch" in get_args(INFILL_METHODS) else "tile"
)
def infill_patchmatch(im: Image.Image) -> Image.Image: def infill_patchmatch(im: Image.Image) -> Image.Image:
@ -42,7 +43,9 @@ def infill_patchmatch(im: Image.Image) -> Image.Image:
return im return im
# Patchmatch (note, we may want to expose patch_size? Increasing it significantly impacts performance though) # Patchmatch (note, we may want to expose patch_size? Increasing it significantly impacts performance though)
im_patched_np = PatchMatch.inpaint(im.convert("RGB"), ImageOps.invert(im.split()[-1]), patch_size=3) im_patched_np = PatchMatch.inpaint(
im.convert("RGB"), ImageOps.invert(im.split()[-1]), patch_size=3
)
im_patched = Image.fromarray(im_patched_np, mode="RGB") im_patched = Image.fromarray(im_patched_np, mode="RGB")
return im_patched return im_patched
@ -64,7 +67,9 @@ def get_tile_images(image: np.ndarray, width=8, height=8):
) )
def tile_fill_missing(im: Image.Image, tile_size: int = 16, seed: Optional[int] = None) -> Image.Image: def tile_fill_missing(
im: Image.Image, tile_size: int = 16, seed: Optional[int] = None
) -> Image.Image:
# Only fill if there's an alpha layer # Only fill if there's an alpha layer
if im.mode != "RGBA": if im.mode != "RGBA":
return im return im
@ -97,7 +102,9 @@ def tile_fill_missing(im: Image.Image, tile_size: int = 16, seed: Optional[int]
# Find all invalid tiles and replace with a random valid tile # Find all invalid tiles and replace with a random valid tile
replace_count = (tiles_mask == False).sum() replace_count = (tiles_mask == False).sum()
rng = np.random.default_rng(seed=seed) rng = np.random.default_rng(seed=seed)
tiles_all[np.logical_not(tiles_mask)] = filtered_tiles[rng.choice(filtered_tiles.shape[0], replace_count), :, :, :] tiles_all[np.logical_not(tiles_mask)] = filtered_tiles[
rng.choice(filtered_tiles.shape[0], replace_count), :, :, :
]
# Convert back to an image # Convert back to an image
tiles_all = tiles_all.reshape(tshape) tiles_all = tiles_all.reshape(tshape)
@ -118,17 +125,14 @@ class InfillColorInvocation(BaseInvocation):
"""Infills transparent areas of an image with a solid color""" """Infills transparent areas of an image with a solid color"""
type: Literal["infill_rgba"] = "infill_rgba" type: Literal["infill_rgba"] = "infill_rgba"
image: Optional[ImageField] = Field(default=None, description="The image to infill") image: Optional[ImageField] = Field(
default=None, description="The image to infill"
)
color: ColorField = Field( color: ColorField = Field(
default=ColorField(r=127, g=127, b=127, a=255), default=ColorField(r=127, g=127, b=127, a=255),
description="The color to use to infill", description="The color to use to infill",
) )
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Color Infill", "tags": ["image", "inpaint", "color", "infill"]},
}
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.services.images.get_pil_image(self.image.image_name) image = context.services.images.get_pil_image(self.image.image_name)
@ -158,7 +162,9 @@ class InfillTileInvocation(BaseInvocation):
type: Literal["infill_tile"] = "infill_tile" type: Literal["infill_tile"] = "infill_tile"
image: Optional[ImageField] = Field(default=None, description="The image to infill") image: Optional[ImageField] = Field(
default=None, description="The image to infill"
)
tile_size: int = Field(default=32, ge=1, description="The tile size (px)") tile_size: int = Field(default=32, ge=1, description="The tile size (px)")
seed: int = Field( seed: int = Field(
ge=0, ge=0,
@ -167,15 +173,12 @@ class InfillTileInvocation(BaseInvocation):
default_factory=get_random_seed, default_factory=get_random_seed,
) )
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Tile Infill", "tags": ["image", "inpaint", "tile", "infill"]},
}
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.services.images.get_pil_image(self.image.image_name) image = context.services.images.get_pil_image(self.image.image_name)
infilled = tile_fill_missing(image.copy(), seed=self.seed, tile_size=self.tile_size) infilled = tile_fill_missing(
image.copy(), seed=self.seed, tile_size=self.tile_size
)
infilled.paste(image, (0, 0), image.split()[-1]) infilled.paste(image, (0, 0), image.split()[-1])
image_dto = context.services.images.create( image_dto = context.services.images.create(
@ -199,12 +202,9 @@ class InfillPatchMatchInvocation(BaseInvocation):
type: Literal["infill_patchmatch"] = "infill_patchmatch" type: Literal["infill_patchmatch"] = "infill_patchmatch"
image: Optional[ImageField] = Field(default=None, description="The image to infill") image: Optional[ImageField] = Field(
default=None, description="The image to infill"
class Config(InvocationConfig): )
schema_extra = {
"ui": {"title": "Patch Match Infill", "tags": ["image", "inpaint", "patchmatch", "infill"]},
}
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.services.images.get_pil_image(self.image.image_name) image = context.services.images.get_pil_image(self.image.image_name)

View File

@ -12,43 +12,31 @@ from pydantic import BaseModel, Field, validator
from invokeai.app.invocations.metadata import CoreMetadata from invokeai.app.invocations.metadata import CoreMetadata
from invokeai.app.util.step_callback import stable_diffusion_step_callback from invokeai.app.util.step_callback import stable_diffusion_step_callback
from invokeai.backend.model_management.models import ModelType, SilenceWarnings from invokeai.backend.model_management.models.base import ModelType
from ...backend.model_management.lora import ModelPatcher from ...backend.model_management.lora import ModelPatcher
from ...backend.stable_diffusion import PipelineIntermediateState from ...backend.stable_diffusion import PipelineIntermediateState
from ...backend.stable_diffusion.diffusers_pipeline import ( from ...backend.stable_diffusion.diffusers_pipeline import (
ConditioningData, ConditioningData, ControlNetData, StableDiffusionGeneratorPipeline,
ControlNetData, image_resized_to_grid_as_tensor)
StableDiffusionGeneratorPipeline, from ...backend.stable_diffusion.diffusion.shared_invokeai_diffusion import \
image_resized_to_grid_as_tensor, PostprocessingSettings
)
from ...backend.stable_diffusion.diffusion.shared_invokeai_diffusion import PostprocessingSettings
from ...backend.stable_diffusion.schedulers import SCHEDULER_MAP from ...backend.stable_diffusion.schedulers import SCHEDULER_MAP
from ...backend.model_management import ModelPatcher from ...backend.util.devices import torch_dtype
from ...backend.util.devices import choose_torch_device, torch_dtype, choose_precision
from ..models.image import ImageCategory, ImageField, ResourceOrigin from ..models.image import ImageCategory, ImageField, ResourceOrigin
from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationConfig, InvocationContext from .baseinvocation import (BaseInvocation, BaseInvocationOutput,
InvocationConfig, InvocationContext)
from .compel import ConditioningField from .compel import ConditioningField
from .controlnet_image_processors import ControlField from .controlnet_image_processors import ControlField
from .image import ImageOutput from .image import ImageOutput
from .model import ModelInfo, UNetField, VaeField from .model import ModelInfo, UNetField, VaeField
from invokeai.app.util.controlnet_utils import prepare_control_image
from diffusers.models.attention_processor import (
AttnProcessor2_0,
LoRAAttnProcessor2_0,
LoRAXFormersAttnProcessor,
XFormersAttnProcessor,
)
DEFAULT_PRECISION = choose_precision(choose_torch_device())
class LatentsField(BaseModel): class LatentsField(BaseModel):
"""A latents field used for passing latents between invocations""" """A latents field used for passing latents between invocations"""
latents_name: Optional[str] = Field(default=None, description="The name of the latents") latents_name: Optional[str] = Field(
default=None, description="The name of the latents")
class Config: class Config:
schema_extra = {"required": ["latents_name"]} schema_extra = {"required": ["latents_name"]}
@ -56,15 +44,14 @@ class LatentsField(BaseModel):
class LatentsOutput(BaseInvocationOutput): class LatentsOutput(BaseInvocationOutput):
"""Base class for invocations that output latents""" """Base class for invocations that output latents"""
#fmt: off
# fmt: off
type: Literal["latents_output"] = "latents_output" type: Literal["latents_output"] = "latents_output"
# Inputs # Inputs
latents: LatentsField = Field(default=None, description="The output latents") latents: LatentsField = Field(default=None, description="The output latents")
width: int = Field(description="The width of the latents in pixels") width: int = Field(description="The width of the latents in pixels")
height: int = Field(description="The height of the latents in pixels") height: int = Field(description="The height of the latents in pixels")
# fmt: on #fmt: on
def build_latents_output(latents_name: str, latents: torch.Tensor): def build_latents_output(latents_name: str, latents: torch.Tensor):
@ -75,7 +62,9 @@ def build_latents_output(latents_name: str, latents: torch.Tensor):
) )
SAMPLER_NAME_VALUES = Literal[tuple(list(SCHEDULER_MAP.keys()))] SAMPLER_NAME_VALUES = Literal[
tuple(list(SCHEDULER_MAP.keys()))
]
def get_scheduler( def get_scheduler(
@ -83,10 +72,11 @@ def get_scheduler(
scheduler_info: ModelInfo, scheduler_info: ModelInfo,
scheduler_name: str, scheduler_name: str,
) -> Scheduler: ) -> Scheduler:
scheduler_class, scheduler_extra_config = SCHEDULER_MAP.get(scheduler_name, SCHEDULER_MAP["ddim"]) scheduler_class, scheduler_extra_config = SCHEDULER_MAP.get(
scheduler_name, SCHEDULER_MAP['ddim']
)
orig_scheduler_info = context.services.model_manager.get_model( orig_scheduler_info = context.services.model_manager.get_model(
**scheduler_info.dict(), **scheduler_info.dict()
context=context,
) )
with orig_scheduler_info as orig_scheduler: with orig_scheduler_info as orig_scheduler:
scheduler_config = orig_scheduler.config scheduler_config = orig_scheduler.config
@ -101,7 +91,7 @@ def get_scheduler(
scheduler = scheduler_class.from_config(scheduler_config) scheduler = scheduler_class.from_config(scheduler_config)
# hack copied over from generate.py # hack copied over from generate.py
if not hasattr(scheduler, "uses_inpainting_model"): if not hasattr(scheduler, 'uses_inpainting_model'):
scheduler.uses_inpainting_model = lambda: False scheduler.uses_inpainting_model = lambda: False
return scheduler return scheduler
@ -122,8 +112,8 @@ class TextToLatentsInvocation(BaseInvocation):
scheduler: SAMPLER_NAME_VALUES = Field(default="euler", description="The scheduler to use" ) scheduler: SAMPLER_NAME_VALUES = Field(default="euler", description="The scheduler to use" )
unet: UNetField = Field(default=None, description="UNet submodel") unet: UNetField = Field(default=None, description="UNet submodel")
control: Union[ControlField, list[ControlField]] = Field(default=None, description="The control to use") control: Union[ControlField, list[ControlField]] = Field(default=None, description="The control to use")
# seamless: bool = Field(default=False, description="Whether or not to generate an image that can tile without seams", ) #seamless: bool = Field(default=False, description="Whether or not to generate an image that can tile without seams", )
# seamless_axes: str = Field(default="", description="The axes to tile the image on, 'x' and/or 'y'") #seamless_axes: str = Field(default="", description="The axes to tile the image on, 'x' and/or 'y'")
# fmt: on # fmt: on
@validator("cfg_scale") @validator("cfg_scale")
@ -132,24 +122,23 @@ class TextToLatentsInvocation(BaseInvocation):
if isinstance(v, list): if isinstance(v, list):
for i in v: for i in v:
if i < 1: if i < 1:
raise ValueError("cfg_scale must be greater than 1") raise ValueError('cfg_scale must be greater than 1')
else: else:
if v < 1: if v < 1:
raise ValueError("cfg_scale must be greater than 1") raise ValueError('cfg_scale must be greater than 1')
return v return v
# Schema customisation # Schema customisation
class Config(InvocationConfig): class Config(InvocationConfig):
schema_extra = { schema_extra = {
"ui": { "ui": {
"title": "Text To Latents",
"tags": ["latents"], "tags": ["latents"],
"type_hints": { "type_hints": {
"model": "model", "model": "model",
"control": "control", "control": "control",
# "cfg_scale": "float", # "cfg_scale": "float",
"cfg_scale": "number", "cfg_scale": "number"
}, }
}, },
} }
@ -171,14 +160,13 @@ class TextToLatentsInvocation(BaseInvocation):
self, self,
context: InvocationContext, context: InvocationContext,
scheduler, scheduler,
unet,
) -> ConditioningData: ) -> ConditioningData:
positive_cond_data = context.services.latents.get(self.positive_conditioning.conditioning_name) c, extra_conditioning_info = context.services.latents.get(
c = positive_cond_data.conditionings[0].embeds.to(device=unet.device, dtype=unet.dtype) self.positive_conditioning.conditioning_name
extra_conditioning_info = positive_cond_data.conditionings[0].extra_conditioning )
uc, _ = context.services.latents.get(
negative_cond_data = context.services.latents.get(self.negative_conditioning.conditioning_name) self.negative_conditioning.conditioning_name
uc = negative_cond_data.conditionings[0].embeds.to(device=unet.device, dtype=unet.dtype) )
conditioning_data = ConditioningData( conditioning_data = ConditioningData(
unconditioned_embeddings=uc, unconditioned_embeddings=uc,
@ -189,16 +177,18 @@ class TextToLatentsInvocation(BaseInvocation):
threshold=0.0, # threshold, threshold=0.0, # threshold,
warmup=0.2, # warmup, warmup=0.2, # warmup,
h_symmetry_time_pct=None, # h_symmetry_time_pct, h_symmetry_time_pct=None, # h_symmetry_time_pct,
v_symmetry_time_pct=None, # v_symmetry_time_pct, v_symmetry_time_pct=None # v_symmetry_time_pct,
), ),
) )
conditioning_data = conditioning_data.add_scheduler_args_if_applicable( conditioning_data = conditioning_data.add_scheduler_args_if_applicable(
scheduler, scheduler,
# for ddim scheduler # for ddim scheduler
eta=0.0, # ddim_eta eta=0.0, # ddim_eta
# for ancestral and sde schedulers # for ancestral and sde schedulers
generator=torch.Generator(device=unet.device).manual_seed(0), generator=torch.Generator(device=uc.device).manual_seed(0),
) )
return conditioning_data return conditioning_data
@ -244,6 +234,7 @@ class TextToLatentsInvocation(BaseInvocation):
exit_stack: ExitStack, exit_stack: ExitStack,
do_classifier_free_guidance: bool = True, do_classifier_free_guidance: bool = True,
) -> List[ControlNetData]: ) -> List[ControlNetData]:
# assuming fixed dimensional scaling of 8:1 for image:latents # assuming fixed dimensional scaling of 8:1 for image:latents
control_height_resize = latents_shape[2] * 8 control_height_resize = latents_shape[2] * 8
control_width_resize = latents_shape[3] * 8 control_width_resize = latents_shape[3] * 8
@ -257,7 +248,7 @@ class TextToLatentsInvocation(BaseInvocation):
control_list = control_input control_list = control_input
else: else:
control_list = None control_list = None
if control_list is None: if (control_list is None):
control_data = None control_data = None
# from above handling, any control that is not None should now be of type list[ControlField] # from above handling, any control that is not None should now be of type list[ControlField]
else: else:
@ -271,19 +262,20 @@ class TextToLatentsInvocation(BaseInvocation):
model_name=control_info.control_model.model_name, model_name=control_info.control_model.model_name,
model_type=ModelType.ControlNet, model_type=ModelType.ControlNet,
base_model=control_info.control_model.base_model, base_model=control_info.control_model.base_model,
context=context,
) )
) )
control_models.append(control_model) control_models.append(control_model)
control_image_field = control_info.image control_image_field = control_info.image
input_image = context.services.images.get_pil_image(control_image_field.image_name) input_image = context.services.images.get_pil_image(
control_image_field.image_name
)
# self.image.image_type, self.image.image_name # self.image.image_type, self.image.image_name
# FIXME: still need to test with different widths, heights, devices, dtypes # FIXME: still need to test with different widths, heights, devices, dtypes
# and add in batch_size, num_images_per_prompt? # and add in batch_size, num_images_per_prompt?
# and do real check for classifier_free_guidance? # and do real check for classifier_free_guidance?
# prepare_control_image should return torch.Tensor of shape(batch_size, 3, height, width) # prepare_control_image should return torch.Tensor of shape(batch_size, 3, height, width)
control_image = prepare_control_image( control_image = model.prepare_control_image(
image=input_image, image=input_image,
do_classifier_free_guidance=do_classifier_free_guidance, do_classifier_free_guidance=do_classifier_free_guidance,
width=control_width_resize, width=control_width_resize,
@ -293,18 +285,13 @@ class TextToLatentsInvocation(BaseInvocation):
device=control_model.device, device=control_model.device,
dtype=control_model.dtype, dtype=control_model.dtype,
control_mode=control_info.control_mode, control_mode=control_info.control_mode,
resize_mode=control_info.resize_mode,
) )
control_item = ControlNetData( control_item = ControlNetData(
model=control_model, model=control_model, image_tensor=control_image,
image_tensor=control_image,
weight=control_info.control_weight, weight=control_info.control_weight,
begin_step_percent=control_info.begin_step_percent, begin_step_percent=control_info.begin_step_percent,
end_step_percent=control_info.end_step_percent, end_step_percent=control_info.end_step_percent,
control_mode=control_info.control_mode, control_mode=control_info.control_mode,
# any resizing needed should currently be happening in prepare_control_image(),
# but adding resize_mode to ControlNetData in case needed in the future
resize_mode=control_info.resize_mode,
) )
control_data.append(control_item) control_data.append(control_item)
# MultiControlNetModel has been refactored out, just need list[ControlNetData] # MultiControlNetModel has been refactored out, just need list[ControlNetData]
@ -312,11 +299,12 @@ class TextToLatentsInvocation(BaseInvocation):
@torch.no_grad() @torch.no_grad()
def invoke(self, context: InvocationContext) -> LatentsOutput: def invoke(self, context: InvocationContext) -> LatentsOutput:
with SilenceWarnings():
noise = context.services.latents.get(self.noise.latents_name) noise = context.services.latents.get(self.noise.latents_name)
# Get the source node id (we are invoking the prepared node) # Get the source node id (we are invoking the prepared node)
graph_execution_state = context.services.graph_execution_manager.get(context.graph_execution_state_id) graph_execution_state = context.services.graph_execution_manager.get(
context.graph_execution_state_id
)
source_node_id = graph_execution_state.prepared_source_mapping[self.id] source_node_id = graph_execution_state.prepared_source_mapping[self.id]
def step_callback(state: PipelineIntermediateState): def step_callback(state: PipelineIntermediateState):
@ -325,21 +313,18 @@ class TextToLatentsInvocation(BaseInvocation):
def _lora_loader(): def _lora_loader():
for lora in self.unet.loras: for lora in self.unet.loras:
lora_info = context.services.model_manager.get_model( lora_info = context.services.model_manager.get_model(
**lora.dict(exclude={"weight"}), **lora.dict(exclude={"weight"})
context=context,
) )
yield (lora_info.context.model, lora.weight) yield (lora_info.context.model, lora.weight)
del lora_info del lora_info
return return
unet_info = context.services.model_manager.get_model( unet_info = context.services.model_manager.get_model(
**self.unet.unet.dict(), **self.unet.unet.dict()
context=context,
) )
with ExitStack() as exit_stack, ModelPatcher.apply_lora_unet( with ExitStack() as exit_stack,\
unet_info.context.model, _lora_loader() ModelPatcher.apply_lora_unet(unet_info.context.model, _lora_loader()),\
), unet_info as unet: unet_info as unet:
noise = noise.to(device=unet.device, dtype=unet.dtype)
scheduler = get_scheduler( scheduler = get_scheduler(
context=context, context=context,
@ -348,12 +333,10 @@ class TextToLatentsInvocation(BaseInvocation):
) )
pipeline = self.create_pipeline(unet, scheduler) pipeline = self.create_pipeline(unet, scheduler)
conditioning_data = self.get_conditioning_data(context, scheduler, unet) conditioning_data = self.get_conditioning_data(context, scheduler)
control_data = self.prep_control_data( control_data = self.prep_control_data(
model=pipeline, model=pipeline, context=context, control_input=self.control,
context=context,
control_input=self.control,
latents_shape=noise.shape, latents_shape=noise.shape,
# do_classifier_free_guidance=(self.cfg_scale >= 1.0)) # do_classifier_free_guidance=(self.cfg_scale >= 1.0))
do_classifier_free_guidance=True, do_classifier_free_guidance=True,
@ -371,10 +354,9 @@ class TextToLatentsInvocation(BaseInvocation):
) )
# https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699 # https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699
result_latents = result_latents.to("cpu")
torch.cuda.empty_cache() torch.cuda.empty_cache()
name = f"{context.graph_execution_state_id}__{self.id}" name = f'{context.graph_execution_state_id}__{self.id}'
context.services.latents.save(name, result_latents) context.services.latents.save(name, result_latents)
return build_latents_output(latents_name=name, latents=result_latents) return build_latents_output(latents_name=name, latents=result_latents)
@ -385,31 +367,34 @@ class LatentsToLatentsInvocation(TextToLatentsInvocation):
type: Literal["l2l"] = "l2l" type: Literal["l2l"] = "l2l"
# Inputs # Inputs
latents: Optional[LatentsField] = Field(description="The latents to use as a base image") latents: Optional[LatentsField] = Field(
strength: float = Field(default=0.7, ge=0, le=1, description="The strength of the latents to use") description="The latents to use as a base image")
strength: float = Field(
default=0.7, ge=0, le=1,
description="The strength of the latents to use")
# Schema customisation # Schema customisation
class Config(InvocationConfig): class Config(InvocationConfig):
schema_extra = { schema_extra = {
"ui": { "ui": {
"title": "Latent To Latents",
"tags": ["latents"], "tags": ["latents"],
"type_hints": { "type_hints": {
"model": "model", "model": "model",
"control": "control", "control": "control",
"cfg_scale": "number", "cfg_scale": "number",
}, }
}, },
} }
@torch.no_grad() @torch.no_grad()
def invoke(self, context: InvocationContext) -> LatentsOutput: def invoke(self, context: InvocationContext) -> LatentsOutput:
with SilenceWarnings(): # this quenches NSFW nag from diffusers
noise = context.services.latents.get(self.noise.latents_name) noise = context.services.latents.get(self.noise.latents_name)
latent = context.services.latents.get(self.latents.latents_name) latent = context.services.latents.get(self.latents.latents_name)
# Get the source node id (we are invoking the prepared node) # Get the source node id (we are invoking the prepared node)
graph_execution_state = context.services.graph_execution_manager.get(context.graph_execution_state_id) graph_execution_state = context.services.graph_execution_manager.get(
context.graph_execution_state_id
)
source_node_id = graph_execution_state.prepared_source_mapping[self.id] source_node_id = graph_execution_state.prepared_source_mapping[self.id]
def step_callback(state: PipelineIntermediateState): def step_callback(state: PipelineIntermediateState):
@ -418,22 +403,18 @@ class LatentsToLatentsInvocation(TextToLatentsInvocation):
def _lora_loader(): def _lora_loader():
for lora in self.unet.loras: for lora in self.unet.loras:
lora_info = context.services.model_manager.get_model( lora_info = context.services.model_manager.get_model(
**lora.dict(exclude={"weight"}), **lora.dict(exclude={"weight"})
context=context,
) )
yield (lora_info.context.model, lora.weight) yield (lora_info.context.model, lora.weight)
del lora_info del lora_info
return return
unet_info = context.services.model_manager.get_model( unet_info = context.services.model_manager.get_model(
**self.unet.unet.dict(), **self.unet.unet.dict()
context=context,
) )
with ExitStack() as exit_stack, ModelPatcher.apply_lora_unet( with ExitStack() as exit_stack,\
unet_info.context.model, _lora_loader() ModelPatcher.apply_lora_unet(unet_info.context.model, _lora_loader()),\
), unet_info as unet: unet_info as unet:
noise = noise.to(device=unet.device, dtype=unet.dtype)
latent = latent.to(device=unet.device, dtype=unet.dtype)
scheduler = get_scheduler( scheduler = get_scheduler(
context=context, context=context,
@ -442,12 +423,10 @@ class LatentsToLatentsInvocation(TextToLatentsInvocation):
) )
pipeline = self.create_pipeline(unet, scheduler) pipeline = self.create_pipeline(unet, scheduler)
conditioning_data = self.get_conditioning_data(context, scheduler, unet) conditioning_data = self.get_conditioning_data(context, scheduler)
control_data = self.prep_control_data( control_data = self.prep_control_data(
model=pipeline, model=pipeline, context=context, control_input=self.control,
context=context,
control_input=self.control,
latents_shape=noise.shape, latents_shape=noise.shape,
# do_classifier_free_guidance=(self.cfg_scale >= 1.0)) # do_classifier_free_guidance=(self.cfg_scale >= 1.0))
do_classifier_free_guidance=True, do_classifier_free_guidance=True,
@ -455,8 +434,8 @@ class LatentsToLatentsInvocation(TextToLatentsInvocation):
) )
# TODO: Verify the noise is the right size # TODO: Verify the noise is the right size
initial_latents = ( initial_latents = latent if self.strength < 1.0 else torch.zeros_like(
latent if self.strength < 1.0 else torch.zeros_like(latent, device=unet.device, dtype=latent.dtype) latent, device=unet.device, dtype=latent.dtype
) )
timesteps, _ = pipeline.get_img2img_timesteps( timesteps, _ = pipeline.get_img2img_timesteps(
@ -472,14 +451,13 @@ class LatentsToLatentsInvocation(TextToLatentsInvocation):
num_inference_steps=self.steps, num_inference_steps=self.steps,
conditioning_data=conditioning_data, conditioning_data=conditioning_data,
control_data=control_data, # list[ControlNetData] control_data=control_data, # list[ControlNetData]
callback=step_callback, callback=step_callback
) )
# https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699 # https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699
result_latents = result_latents.to("cpu")
torch.cuda.empty_cache() torch.cuda.empty_cache()
name = f"{context.graph_execution_state_id}__{self.id}" name = f'{context.graph_execution_state_id}__{self.id}'
context.services.latents.save(name, result_latents) context.services.latents.save(name, result_latents)
return build_latents_output(latents_name=name, latents=result_latents) return build_latents_output(latents_name=name, latents=result_latents)
@ -491,19 +469,19 @@ class LatentsToImageInvocation(BaseInvocation):
type: Literal["l2i"] = "l2i" type: Literal["l2i"] = "l2i"
# Inputs # Inputs
latents: Optional[LatentsField] = Field(description="The latents to generate an image from") latents: Optional[LatentsField] = Field(
description="The latents to generate an image from")
vae: VaeField = Field(default=None, description="Vae submodel") vae: VaeField = Field(default=None, description="Vae submodel")
tiled: bool = Field(default=False, description="Decode latents by overlaping tiles (less memory consumption)") tiled: bool = Field(
fp32: bool = Field(DEFAULT_PRECISION == "float32", description="Decode in full precision") default=False,
metadata: Optional[CoreMetadata] = Field( description="Decode latents by overlaping tiles(less memory consumption)")
default=None, description="Optional core metadata to be written to the image" metadata: Optional[CoreMetadata] = Field(default=None, description="Optional core metadata to be written to the image")
)
# Schema customisation # Schema customisation
class Config(InvocationConfig): class Config(InvocationConfig):
schema_extra = { schema_extra = {
"ui": { "ui": {
"title": "Latents To Image",
"tags": ["latents", "image"], "tags": ["latents", "image"],
}, },
} }
@ -514,36 +492,9 @@ class LatentsToImageInvocation(BaseInvocation):
vae_info = context.services.model_manager.get_model( vae_info = context.services.model_manager.get_model(
**self.vae.vae.dict(), **self.vae.vae.dict(),
context=context,
) )
with vae_info as vae: with vae_info as vae:
latents = latents.to(vae.device)
if self.fp32:
vae.to(dtype=torch.float32)
use_torch_2_0_or_xformers = isinstance(
vae.decoder.mid_block.attentions[0].processor,
(
AttnProcessor2_0,
XFormersAttnProcessor,
LoRAXFormersAttnProcessor,
LoRAAttnProcessor2_0,
),
)
# if xformers or torch_2_0 is used attention block does not need
# to be in float32 which can save lots of memory
if use_torch_2_0_or_xformers:
vae.post_quant_conv.to(latents.dtype)
vae.decoder.conv_in.to(latents.dtype)
vae.decoder.mid_block.to(latents.dtype)
else:
latents = latents.float()
else:
vae.to(dtype=torch.float16)
latents = latents.half()
if self.tiled or context.services.configuration.tiled_decode: if self.tiled or context.services.configuration.tiled_decode:
vae.enable_tiling() vae.enable_tiling()
else: else:
@ -581,7 +532,8 @@ class LatentsToImageInvocation(BaseInvocation):
) )
LATENTS_INTERPOLATION_MODE = Literal["nearest", "linear", "bilinear", "bicubic", "trilinear", "area", "nearest-exact"] LATENTS_INTERPOLATION_MODE = Literal["nearest", "linear",
"bilinear", "bicubic", "trilinear", "area", "nearest-exact"]
class ResizeLatentsInvocation(BaseInvocation): class ResizeLatentsInvocation(BaseInvocation):
@ -590,34 +542,28 @@ class ResizeLatentsInvocation(BaseInvocation):
type: Literal["lresize"] = "lresize" type: Literal["lresize"] = "lresize"
# Inputs # Inputs
latents: Optional[LatentsField] = Field(description="The latents to resize") latents: Optional[LatentsField] = Field(
width: Union[int, None] = Field(default=512, ge=64, multiple_of=8, description="The width to resize to (px)") description="The latents to resize")
height: Union[int, None] = Field(default=512, ge=64, multiple_of=8, description="The height to resize to (px)") width: Union[int, None] = Field(default=512,
mode: LATENTS_INTERPOLATION_MODE = Field(default="bilinear", description="The interpolation mode") ge=64, multiple_of=8, description="The width to resize to (px)")
height: Union[int, None] = Field(default=512,
ge=64, multiple_of=8, description="The height to resize to (px)")
mode: LATENTS_INTERPOLATION_MODE = Field(
default="bilinear", description="The interpolation mode")
antialias: bool = Field( antialias: bool = Field(
default=False, description="Whether or not to antialias (applied in bilinear and bicubic modes only)" default=False,
) description="Whether or not to antialias (applied in bilinear and bicubic modes only)")
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Resize Latents", "tags": ["latents", "resize"]},
}
def invoke(self, context: InvocationContext) -> LatentsOutput: def invoke(self, context: InvocationContext) -> LatentsOutput:
latents = context.services.latents.get(self.latents.latents_name) latents = context.services.latents.get(self.latents.latents_name)
# TODO:
device = choose_torch_device()
resized_latents = torch.nn.functional.interpolate( resized_latents = torch.nn.functional.interpolate(
latents.to(device), latents, size=(self.height // 8, self.width // 8),
size=(self.height // 8, self.width // 8), mode=self.mode, antialias=self.antialias
mode=self.mode, if self.mode in ["bilinear", "bicubic"] else False,
antialias=self.antialias if self.mode in ["bilinear", "bicubic"] else False,
) )
# https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699 # https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699
resized_latents = resized_latents.to("cpu")
torch.cuda.empty_cache() torch.cuda.empty_cache()
name = f"{context.graph_execution_state_id}__{self.id}" name = f"{context.graph_execution_state_id}__{self.id}"
@ -632,34 +578,27 @@ class ScaleLatentsInvocation(BaseInvocation):
type: Literal["lscale"] = "lscale" type: Literal["lscale"] = "lscale"
# Inputs # Inputs
latents: Optional[LatentsField] = Field(description="The latents to scale") latents: Optional[LatentsField] = Field(
scale_factor: float = Field(gt=0, description="The factor by which to scale the latents") description="The latents to scale")
mode: LATENTS_INTERPOLATION_MODE = Field(default="bilinear", description="The interpolation mode") scale_factor: float = Field(
gt=0, description="The factor by which to scale the latents")
mode: LATENTS_INTERPOLATION_MODE = Field(
default="bilinear", description="The interpolation mode")
antialias: bool = Field( antialias: bool = Field(
default=False, description="Whether or not to antialias (applied in bilinear and bicubic modes only)" default=False,
) description="Whether or not to antialias (applied in bilinear and bicubic modes only)")
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Scale Latents", "tags": ["latents", "scale"]},
}
def invoke(self, context: InvocationContext) -> LatentsOutput: def invoke(self, context: InvocationContext) -> LatentsOutput:
latents = context.services.latents.get(self.latents.latents_name) latents = context.services.latents.get(self.latents.latents_name)
# TODO:
device = choose_torch_device()
# resizing # resizing
resized_latents = torch.nn.functional.interpolate( resized_latents = torch.nn.functional.interpolate(
latents.to(device), latents, scale_factor=self.scale_factor, mode=self.mode,
scale_factor=self.scale_factor, antialias=self.antialias
mode=self.mode, if self.mode in ["bilinear", "bicubic"] else False,
antialias=self.antialias if self.mode in ["bilinear", "bicubic"] else False,
) )
# https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699 # https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699
resized_latents = resized_latents.to("cpu")
torch.cuda.empty_cache() torch.cuda.empty_cache()
name = f"{context.graph_execution_state_id}__{self.id}" name = f"{context.graph_execution_state_id}__{self.id}"
@ -676,13 +615,16 @@ class ImageToLatentsInvocation(BaseInvocation):
# Inputs # Inputs
image: Optional[ImageField] = Field(description="The image to encode") image: Optional[ImageField] = Field(description="The image to encode")
vae: VaeField = Field(default=None, description="Vae submodel") vae: VaeField = Field(default=None, description="Vae submodel")
tiled: bool = Field(default=False, description="Encode latents by overlaping tiles(less memory consumption)") tiled: bool = Field(
fp32: bool = Field(DEFAULT_PRECISION == "float32", description="Decode in full precision") default=False,
description="Encode latents by overlaping tiles(less memory consumption)")
# Schema customisation # Schema customisation
class Config(InvocationConfig): class Config(InvocationConfig):
schema_extra = { schema_extra = {
"ui": {"title": "Image To Latents", "tags": ["latents", "image"]}, "ui": {
"tags": ["latents", "image"],
},
} }
@torch.no_grad() @torch.no_grad()
@ -692,10 +634,9 @@ class ImageToLatentsInvocation(BaseInvocation):
# ) # )
image = context.services.images.get_pil_image(self.image.image_name) image = context.services.images.get_pil_image(self.image.image_name)
# vae_info = context.services.model_manager.get_model(**self.vae.vae.dict()) #vae_info = context.services.model_manager.get_model(**self.vae.vae.dict())
vae_info = context.services.model_manager.get_model( vae_info = context.services.model_manager.get_model(
**self.vae.vae.dict(), **self.vae.vae.dict(),
context=context,
) )
image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB")) image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB"))
@ -703,32 +644,6 @@ class ImageToLatentsInvocation(BaseInvocation):
image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w") image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
with vae_info as vae: with vae_info as vae:
orig_dtype = vae.dtype
if self.fp32:
vae.to(dtype=torch.float32)
use_torch_2_0_or_xformers = isinstance(
vae.decoder.mid_block.attentions[0].processor,
(
AttnProcessor2_0,
XFormersAttnProcessor,
LoRAXFormersAttnProcessor,
LoRAAttnProcessor2_0,
),
)
# if xformers or torch_2_0 is used attention block does not need
# to be in float32 which can save lots of memory
if use_torch_2_0_or_xformers:
vae.post_quant_conv.to(orig_dtype)
vae.decoder.conv_in.to(orig_dtype)
vae.decoder.mid_block.to(orig_dtype)
# else:
# latents = latents.float()
else:
vae.to(dtype=torch.float16)
# latents = latents.half()
if self.tiled: if self.tiled:
vae.enable_tiling() vae.enable_tiling()
else: else:
@ -738,12 +653,13 @@ class ImageToLatentsInvocation(BaseInvocation):
image_tensor = image_tensor.to(device=vae.device, dtype=vae.dtype) image_tensor = image_tensor.to(device=vae.device, dtype=vae.dtype)
with torch.inference_mode(): with torch.inference_mode():
image_tensor_dist = vae.encode(image_tensor).latent_dist image_tensor_dist = vae.encode(image_tensor).latent_dist
latents = image_tensor_dist.sample().to(dtype=vae.dtype) # FIXME: uses torch.randn. make reproducible! latents = image_tensor_dist.sample().to(
dtype=vae.dtype
) # FIXME: uses torch.randn. make reproducible!
latents = vae.config.scaling_factor * latents latents = 0.18215 * latents
latents = latents.to(dtype=orig_dtype)
name = f"{context.graph_execution_state_id}__{self.id}" name = f"{context.graph_execution_state_id}__{self.id}"
latents = latents.to("cpu") # context.services.latents.set(name, latents)
context.services.latents.save(name, latents) context.services.latents.save(name, latents)
return build_latents_output(latents_name=name, latents=latents) return build_latents_output(latents_name=name, latents=latents)

View File

@ -52,11 +52,6 @@ class AddInvocation(BaseInvocation, MathInvocationConfig):
b: int = Field(default=0, description="The second number") b: int = Field(default=0, description="The second number")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Add", "tags": ["math", "add"]},
}
def invoke(self, context: InvocationContext) -> IntOutput: def invoke(self, context: InvocationContext) -> IntOutput:
return IntOutput(a=self.a + self.b) return IntOutput(a=self.a + self.b)
@ -70,11 +65,6 @@ class SubtractInvocation(BaseInvocation, MathInvocationConfig):
b: int = Field(default=0, description="The second number") b: int = Field(default=0, description="The second number")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Subtract", "tags": ["math", "subtract"]},
}
def invoke(self, context: InvocationContext) -> IntOutput: def invoke(self, context: InvocationContext) -> IntOutput:
return IntOutput(a=self.a - self.b) return IntOutput(a=self.a - self.b)
@ -88,11 +78,6 @@ class MultiplyInvocation(BaseInvocation, MathInvocationConfig):
b: int = Field(default=0, description="The second number") b: int = Field(default=0, description="The second number")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Multiply", "tags": ["math", "multiply"]},
}
def invoke(self, context: InvocationContext) -> IntOutput: def invoke(self, context: InvocationContext) -> IntOutput:
return IntOutput(a=self.a * self.b) return IntOutput(a=self.a * self.b)
@ -106,11 +91,6 @@ class DivideInvocation(BaseInvocation, MathInvocationConfig):
b: int = Field(default=0, description="The second number") b: int = Field(default=0, description="The second number")
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Divide", "tags": ["math", "divide"]},
}
def invoke(self, context: InvocationContext) -> IntOutput: def invoke(self, context: InvocationContext) -> IntOutput:
return IntOutput(a=int(self.a / self.b)) return IntOutput(a=int(self.a / self.b))
@ -125,11 +105,5 @@ class RandomIntInvocation(BaseInvocation):
default=np.iinfo(np.int32).max, description="The exclusive high value" default=np.iinfo(np.int32).max, description="The exclusive high value"
) )
# fmt: on # fmt: on
class Config(InvocationConfig):
schema_extra = {
"ui": {"title": "Random Integer", "tags": ["math", "random", "integer"]},
}
def invoke(self, context: InvocationContext) -> IntOutput: def invoke(self, context: InvocationContext) -> IntOutput:
return IntOutput(a=np.random.randint(self.low, self.high)) return IntOutput(a=np.random.randint(self.low, self.high))

View File

@ -2,19 +2,16 @@ from typing import Literal, Optional, Union
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from invokeai.app.invocations.baseinvocation import ( from invokeai.app.invocations.baseinvocation import (BaseInvocation,
BaseInvocation,
BaseInvocationOutput, BaseInvocationOutput,
InvocationConfig, InvocationContext)
InvocationContext,
)
from invokeai.app.invocations.controlnet_image_processors import ControlField from invokeai.app.invocations.controlnet_image_processors import ControlField
from invokeai.app.invocations.model import LoRAModelField, MainModelField, VAEModelField from invokeai.app.invocations.model import (LoRAModelField, MainModelField,
VAEModelField)
class LoRAMetadataField(BaseModel): class LoRAMetadataField(BaseModel):
"""LoRA metadata for an image generated in InvokeAI.""" """LoRA metadata for an image generated in InvokeAI."""
lora: LoRAModelField = Field(description="The LoRA model") lora: LoRAModelField = Field(description="The LoRA model")
weight: float = Field(description="The weight of the LoRA model") weight: float = Field(description="The weight of the LoRA model")
@ -22,9 +19,7 @@ class LoRAMetadataField(BaseModel):
class CoreMetadata(BaseModel): class CoreMetadata(BaseModel):
"""Core generation metadata for an image generated in InvokeAI.""" """Core generation metadata for an image generated in InvokeAI."""
generation_mode: str = Field( generation_mode: str = Field(description="The generation mode that output this image",)
description="The generation mode that output this image",
)
positive_prompt: str = Field(description="The positive prompt parameter") positive_prompt: str = Field(description="The positive prompt parameter")
negative_prompt: str = Field(description="The negative prompt parameter") negative_prompt: str = Field(description="The negative prompt parameter")
width: int = Field(description="The width parameter") width: int = Field(description="The width parameter")
@ -34,40 +29,21 @@ class CoreMetadata(BaseModel):
cfg_scale: float = Field(description="The classifier-free guidance scale parameter") cfg_scale: float = Field(description="The classifier-free guidance scale parameter")
steps: int = Field(description="The number of steps used for inference") steps: int = Field(description="The number of steps used for inference")
scheduler: str = Field(description="The scheduler used for inference") scheduler: str = Field(description="The scheduler used for inference")
clip_skip: int = Field( clip_skip: int = Field(description="The number of skipped CLIP layers",)
description="The number of skipped CLIP layers",
)
model: MainModelField = Field(description="The main model used for inference") model: MainModelField = Field(description="The main model used for inference")
controlnets: list[ControlField] = Field(description="The ControlNets used for inference") controlnets: list[ControlField]= Field(description="The ControlNets used for inference")
loras: list[LoRAMetadataField] = Field(description="The LoRAs used for inference") loras: list[LoRAMetadataField] = Field(description="The LoRAs used for inference")
vae: Union[VAEModelField, None] = Field(
default=None,
description="The VAE used for decoding, if the main model's default was not used",
)
# Latents-to-Latents
strength: Union[float, None] = Field( strength: Union[float, None] = Field(
default=None, default=None,
description="The strength used for latents-to-latents", description="The strength used for latents-to-latents",
) )
init_image: Union[str, None] = Field(default=None, description="The name of the initial image") init_image: Union[str, None] = Field(
default=None, description="The name of the initial image"
# SDXL )
positive_style_prompt: Union[str, None] = Field(default=None, description="The positive style prompt parameter") vae: Union[VAEModelField, None] = Field(
negative_style_prompt: Union[str, None] = Field(default=None, description="The negative style prompt parameter")
# SDXL Refiner
refiner_model: Union[MainModelField, None] = Field(default=None, description="The SDXL Refiner model used")
refiner_cfg_scale: Union[float, None] = Field(
default=None, default=None,
description="The classifier-free guidance scale parameter used for the refiner", description="The VAE used for decoding, if the main model's default was not used",
) )
refiner_steps: Union[int, None] = Field(default=None, description="The number of steps used for the refiner")
refiner_scheduler: Union[str, None] = Field(default=None, description="The scheduler used for the refiner")
refiner_aesthetic_store: Union[float, None] = Field(
default=None, description="The aesthetic score used for the refiner"
)
refiner_start: Union[float, None] = Field(default=None, description="The start value used for refiner denoising")
class ImageMetadata(BaseModel): class ImageMetadata(BaseModel):
@ -77,7 +53,9 @@ class ImageMetadata(BaseModel):
default=None, default=None,
description="The image's core metadata, if it was created in the Linear or Canvas UI", description="The image's core metadata, if it was created in the Linear or Canvas UI",
) )
graph: Optional[dict] = Field(default=None, description="The graph that created the image") graph: Optional[dict] = Field(
default=None, description="The graph that created the image"
)
class MetadataAccumulatorOutput(BaseInvocationOutput): class MetadataAccumulatorOutput(BaseInvocationOutput):
@ -93,9 +71,7 @@ class MetadataAccumulatorInvocation(BaseInvocation):
type: Literal["metadata_accumulator"] = "metadata_accumulator" type: Literal["metadata_accumulator"] = "metadata_accumulator"
generation_mode: str = Field( generation_mode: str = Field(description="The generation mode that output this image",)
description="The generation mode that output this image",
)
positive_prompt: str = Field(description="The positive prompt parameter") positive_prompt: str = Field(description="The positive prompt parameter")
negative_prompt: str = Field(description="The negative prompt parameter") negative_prompt: str = Field(description="The negative prompt parameter")
width: int = Field(description="The width parameter") width: int = Field(description="The width parameter")
@ -105,48 +81,44 @@ class MetadataAccumulatorInvocation(BaseInvocation):
cfg_scale: float = Field(description="The classifier-free guidance scale parameter") cfg_scale: float = Field(description="The classifier-free guidance scale parameter")
steps: int = Field(description="The number of steps used for inference") steps: int = Field(description="The number of steps used for inference")
scheduler: str = Field(description="The scheduler used for inference") scheduler: str = Field(description="The scheduler used for inference")
clip_skip: int = Field( clip_skip: int = Field(description="The number of skipped CLIP layers",)
description="The number of skipped CLIP layers",
)
model: MainModelField = Field(description="The main model used for inference") model: MainModelField = Field(description="The main model used for inference")
controlnets: list[ControlField] = Field(description="The ControlNets used for inference") controlnets: list[ControlField]= Field(description="The ControlNets used for inference")
loras: list[LoRAMetadataField] = Field(description="The LoRAs used for inference") loras: list[LoRAMetadataField] = Field(description="The LoRAs used for inference")
strength: Union[float, None] = Field( strength: Union[float, None] = Field(
default=None, default=None,
description="The strength used for latents-to-latents", description="The strength used for latents-to-latents",
) )
init_image: Union[str, None] = Field(default=None, description="The name of the initial image") init_image: Union[str, None] = Field(
default=None, description="The name of the initial image"
)
vae: Union[VAEModelField, None] = Field( vae: Union[VAEModelField, None] = Field(
default=None, default=None,
description="The VAE used for decoding, if the main model's default was not used", description="The VAE used for decoding, if the main model's default was not used",
) )
# SDXL
positive_style_prompt: Union[str, None] = Field(default=None, description="The positive style prompt parameter")
negative_style_prompt: Union[str, None] = Field(default=None, description="The negative style prompt parameter")
# SDXL Refiner
refiner_model: Union[MainModelField, None] = Field(default=None, description="The SDXL Refiner model used")
refiner_cfg_scale: Union[float, None] = Field(
default=None,
description="The classifier-free guidance scale parameter used for the refiner",
)
refiner_steps: Union[int, None] = Field(default=None, description="The number of steps used for the refiner")
refiner_scheduler: Union[str, None] = Field(default=None, description="The scheduler used for the refiner")
refiner_aesthetic_store: Union[float, None] = Field(
default=None, description="The aesthetic score used for the refiner"
)
refiner_start: Union[float, None] = Field(default=None, description="The start value used for refiner denoising")
class Config(InvocationConfig):
schema_extra = {
"ui": {
"title": "Metadata Accumulator",
"tags": ["image", "metadata", "generation"],
},
}
def invoke(self, context: InvocationContext) -> MetadataAccumulatorOutput: def invoke(self, context: InvocationContext) -> MetadataAccumulatorOutput:
"""Collects and outputs a CoreMetadata object""" """Collects and outputs a CoreMetadata object"""
return MetadataAccumulatorOutput(metadata=CoreMetadata(**self.dict())) return MetadataAccumulatorOutput(
metadata=CoreMetadata(
generation_mode=self.generation_mode,
positive_prompt=self.positive_prompt,
negative_prompt=self.negative_prompt,
width=self.width,
height=self.height,
seed=self.seed,
rand_device=self.rand_device,
cfg_scale=self.cfg_scale,
steps=self.steps,
scheduler=self.scheduler,
model=self.model,
strength=self.strength,
init_image=self.init_image,
vae=self.vae,
controlnets=self.controlnets,
loras=self.loras,
clip_skip=self.clip_skip,
)
)

View File

@ -4,14 +4,17 @@ from typing import List, Literal, Optional, Union
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from ...backend.model_management import BaseModelType, ModelType, SubModelType from ...backend.model_management import BaseModelType, ModelType, SubModelType
from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationConfig, InvocationContext from .baseinvocation import (BaseInvocation, BaseInvocationOutput,
InvocationConfig, InvocationContext)
class ModelInfo(BaseModel): class ModelInfo(BaseModel):
model_name: str = Field(description="Info to load submodel") model_name: str = Field(description="Info to load submodel")
base_model: BaseModelType = Field(description="Base model") base_model: BaseModelType = Field(description="Base model")
model_type: ModelType = Field(description="Info to load submodel") model_type: ModelType = Field(description="Info to load submodel")
submodel: Optional[SubModelType] = Field(default=None, description="Info to load submodel") submodel: Optional[SubModelType] = Field(
default=None, description="Info to load submodel"
)
class LoraInfo(ModelInfo): class LoraInfo(ModelInfo):
@ -53,7 +56,6 @@ class MainModelField(BaseModel):
model_name: str = Field(description="Name of the model") model_name: str = Field(description="Name of the model")
base_model: BaseModelType = Field(description="Base model") base_model: BaseModelType = Field(description="Base model")
model_type: ModelType = Field(description="Model Type")
class LoRAModelField(BaseModel): class LoRAModelField(BaseModel):
@ -155,22 +157,6 @@ class MainModelLoaderInvocation(BaseInvocation):
loras=[], loras=[],
skipped_layers=0, skipped_layers=0,
), ),
clip2=ClipField(
tokenizer=ModelInfo(
model_name=model_name,
base_model=base_model,
model_type=model_type,
submodel=SubModelType.Tokenizer2,
),
text_encoder=ModelInfo(
model_name=model_name,
base_model=base_model,
model_type=model_type,
submodel=SubModelType.TextEncoder2,
),
loras=[],
skipped_layers=0,
),
vae=VaeField( vae=VaeField(
vae=ModelInfo( vae=ModelInfo(
model_name=model_name, model_name=model_name,
@ -198,7 +184,9 @@ class LoraLoaderInvocation(BaseInvocation):
type: Literal["lora_loader"] = "lora_loader" type: Literal["lora_loader"] = "lora_loader"
lora: Union[LoRAModelField, None] = Field(default=None, description="Lora model name") lora: Union[LoRAModelField, None] = Field(
default=None, description="Lora model name"
)
weight: float = Field(default=0.75, description="With what weight to apply lora") weight: float = Field(default=0.75, description="With what weight to apply lora")
unet: Optional[UNetField] = Field(description="UNet model for applying lora") unet: Optional[UNetField] = Field(description="UNet model for applying lora")
@ -227,10 +215,14 @@ class LoraLoaderInvocation(BaseInvocation):
): ):
raise Exception(f"Unkown lora name: {lora_name}!") raise Exception(f"Unkown lora name: {lora_name}!")
if self.unet is not None and any(lora.model_name == lora_name for lora in self.unet.loras): if self.unet is not None and any(
lora.model_name == lora_name for lora in self.unet.loras
):
raise Exception(f'Lora "{lora_name}" already applied to unet') raise Exception(f'Lora "{lora_name}" already applied to unet')
if self.clip is not None and any(lora.model_name == lora_name for lora in self.clip.loras): if self.clip is not None and any(
lora.model_name == lora_name for lora in self.clip.loras
):
raise Exception(f'Lora "{lora_name}" already applied to clip') raise Exception(f'Lora "{lora_name}" already applied to clip')
output = LoraLoaderOutput() output = LoraLoaderOutput()

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