Compare commits

..

63 Commits

Author SHA1 Message Date
787df67ceb feat(backend): test node cache selective invalidation 2023-09-20 00:26:07 +10:00
4b149ab521 fix(tests): add config service to mock invocation services
invoking needs access to `node_cache_size` to occur
2023-09-19 22:17:15 +10:00
25ecf08962 fix(ui): fix missing/incorrect translation keys 2023-09-19 21:52:11 +10:00
f051daea4f feat(ui): update floating buttons & other incidental UI tweaks 2023-09-19 21:30:41 +10:00
143d7e03ef feat(ui): add queue controls to workflow editor 2023-09-19 20:23:54 +10:00
9d959b8cf3 feat(ui): prevent enqueue when no prompts generated 2023-09-19 20:01:14 +10:00
1b3a6f4540 fix(ui): reset api cache on connect/disconnect 2023-09-19 20:00:50 +10:00
3623112807 feat(nodes): bypass cache codepath if caches is disabled
more better no do thing
2023-09-19 19:44:03 +10:00
8c65ade392 feat(nodes): handle KeyError on node cache pop 2023-09-19 19:39:25 +10:00
cba25766aa feat(nodes): bail on node caching logic if node_cache_size == 0 2023-09-19 19:37:21 +10:00
c2177c1778 fix(ui): fix circular dependency 2023-09-19 19:34:29 +10:00
7f2f085658 feat(ui): fix progress bar when canceling 2023-09-19 19:30:48 +10:00
d9c816bdbb tidy(queue): update comment, query format 2023-09-19 19:16:03 +10:00
b86b72437f feat(ui): click queue count to go to queue tab 2023-09-19 18:59:46 +10:00
508c7ca9eb feat(ui): fix up param collapse labels 2023-09-19 18:52:41 +10:00
e7824ed176 feat(ui): strip out old logic related to sessions
Things like `isProcessing` are no longer relevant with queue. Removed them all & updated everything be appropriate for queue. May be a few little quirks I've missed...
2023-09-19 18:24:46 +10:00
0deb588a02 feat(nodes): do not send the whole node on every generator progress 2023-09-19 18:23:38 +10:00
567a19e47a fix(ui): fix queue list item width 2023-09-19 15:08:58 +10:00
c572e0a6a0 feat: add invocation cache
Supersedes #4574

The invocation cache provides simple node memoization functionality. Nodes that use the cache are memoized and not re-executed if their inputs haven't changed. Instead, the stored output is returned.

## Results

This feature provides anywhere some significant to massive performance improvement.

The improvement is most marked on large batches of generations where you only change a couple things (e.g. different seed or prompt for each iteration) and low-VRAM systems, where skipping an extraneous model load is a big deal.

## Overview

A new `invocation_cache` service is added to handle the caching. There's not much to it.

All nodes now inherit a boolean `use_cache` field from `BaseInvocation`. This is a node field and not a class attribute, because specific instances of nodes may want to opt in or out of caching.

The recently-added `invoke_internal()` method on `BaseInvocation` is used as an entrypoint for the cache logic.

To create a cache key, the invocation is first serialized using pydantic's provided `json()` method, skipping the unique `id` field. Then python's very fast builtin `hash()` is used to create an integer key. All implementations of `InvocationCacheBase` must provide a class method `create_key()` which accepts an invocation and outputs a string or integer key.

## In-Memory Implementation

An in-memory implementation is provided. In this implementation, the node outputs are stored in memory as python classes. The in-memory cache does not persist application restarts.

Max node cache size is added as `node_cache_size` under the `Generation` config category.

It defaults to 512 - this number is up for discussion, but given that these are relatively lightweight pydantic models, I think it's safe to up this even higher.

Note that the cache isn't storing the big stuff - tensors and images are store on disk, and outputs include only references to them.

## Node Definition

The default for all nodes is to use the cache. The `@invocation` decorator now accepts an optional `use_cache: bool` argument to override the default of `True`.

Non-deterministic nodes, however, should set this to `False`. Currently, all random-stuff nodes, including `dynamic_prompt`, are set to `False`.

The field name `use_cache` is now effectively a reserved field name and possibly a breaking change if any community nodes use this as a field name. In hindsight, all our reserved field names should have been prefixed with underscores or something.

## One Gotcha

Leaf nodes probably want to opt out of the cache, because if they are not cached, their outputs are not saved again.

If you run the same graph multiple times, you only end up with a single image output, because the image storage side-effects are in the `invoke()` method, which is bypassed if we have a cache hit.

## Linear UI

The linear graphs _almost_ just work, but due to the gotcha, we need to be careful about the final image-outputting node. To resolve this, a `SaveImageInvocation` node is added and used in the linear graphs.

This node is similar to `ImagePrimitive`, except it saves a copy of its input image, and has `use_cache` set to `False` by default.

This is now the leaf node in all linear graphs, and is the only node in those graphs with `use_cache == False` _and_ the only node with `is_intermedate == False`.

## Workflow Editor

All nodes now have a footer with a new `Use Cache [ ]` checkbox. It defaults to the value set by the invocation in its python definition, but can be changed by the user.

The workflow/node validation logic has been updated to migrate old workflows to use the new default values for `use_cache`. Users may still want to review the settings that have been chosen. In the event of catastrophic failure when running this migration, the default value of `True` is applied, as this is correct for most nodes.

Users should consider saving their workflows after loading them in and having them updated.

## Future Enhancements - Callback

A future enhancement would be to provide a callback to the `use_cache` flag that would be run as the node is executed to determine, based on its own internal state, if the cache should be used or not.

This would be useful for `DynamicPromptInvocation`, where the deterministic behaviour is determined by the `combinatorial: bool` field.

## Future Enhancements - Persisted Cache

Similar to how the latents storage is backed by disk, the invocation cache could be persisted to the database or disk. We'd need to be very careful about deserializing outputs, but it's perhaps worth exploring in the future.
2023-09-19 15:05:24 +10:00
c8869f543c feat(backend): add config.log_sql (enables sql trace logging) 2023-09-19 14:01:12 +10:00
8ec5e07011 fix(backend): respect use_memory_db 2023-09-19 14:01:12 +10:00
6ba83350ff chore(backend): tidy logging in dependencies.py 2023-09-19 14:01:12 +10:00
5ec405ebe6 feat(api): deprecate sessions router 2023-09-19 14:01:12 +10:00
e8ac82a492 feat(queue): change all execution-related events to use the queue_id as the room, also include queue_item_id in InvocationQueueItem
This allows for much simpler handling of queue items.
2023-09-19 14:01:12 +10:00
0c5bafdeb6 feat(queue): add timestamps to queue status updates
Also show execution time in queue list
2023-09-19 14:01:12 +10:00
507a429d42 fix(ui): fix controlnet autoprocess not working when queue is running 2023-09-19 14:01:12 +10:00
7a6ea8a67f fix(ui): remove extraneous random seed nodes from linear graphs 2023-09-19 14:01:12 +10:00
0738bcfe9b feat(ui): add seed behaviour parameter
This dynamic prompts parameter allows the seed to be randomized per prompt or per iteration:
- Per iteration: Use the same seed for all prompts in a single dynamic prompt expansion
- Per prompt: Use a different seed for every single prompt

"Per iteration" is appropriate for exploring a the latents space with a stable starting noise, while "Per prompt" provides more variation.
2023-09-19 14:01:12 +10:00
bdc7227b61 feat(nodes): optimize batch permutation
Use a generator to do only as much work as is needed.

Previously, though we only ended up creating exactly as many queue items as was needed, there was still some intermediary work that calculated *all* permutations. When that number was very high, the system had a very hard time and used a lot of memory.

The logic has been refactored to use a generator. Additionally, the batch validators are optimized to return early and use less memory.
2023-09-19 14:01:12 +10:00
d93d5561b1 feat(ui): cleanup & abstract queue hooks 2023-09-19 14:01:12 +10:00
aab7c2c152 feat(nodes,ui): consolidate events, reduce network requests 2023-09-19 14:01:12 +10:00
593d91815d feat(ui): ui tweaks 2023-09-19 14:01:12 +10:00
cd9f0e026f feat(nodes,ui): add processor events 2023-09-19 14:01:12 +10:00
7a1fe7548b feat(nodes,ui): fully migrate queue to session_processor 2023-09-19 14:01:12 +10:00
e1b8874bc5 wip threaded session_processor 2023-09-19 14:01:12 +10:00
d2f102b6ab feat(ui): update queue list
Queue items expand to show the full queue item. Just as JSON for now.
2023-09-19 14:01:12 +10:00
b039fb1e78 feat(ui): QueueTable performance tweaks 2023-09-19 14:01:12 +10:00
91cdccd217 fix(ui): fix non-serializable DOMRect in redux state 2023-09-19 14:01:12 +10:00
968bc41bcc feat(ui): move controlnet autoprocess to queue 2023-09-19 14:01:12 +10:00
e44fbd0d53 feat(nodes): return queue item when enqueuing a single graph
This facilitates one-off graph async workflows in the client.
2023-09-19 14:01:12 +10:00
bf7780079e feat(ui): fix upscale button
It prepends the upscale operation to queue
2023-09-19 14:01:12 +10:00
385a8afacf feat: add queue_id & support logic 2023-09-19 14:01:12 +10:00
fc0a2ddef3 feat(ui): dynamicPrompts.combinatorial is always on
There seems to be little purpose in using the combinatorial generation for dynamic prompts. I've disabled it by hiding it from the UI and defaulting combinatorial to true. If we want to enable it again in the future it's straightforward to do so.
2023-09-19 14:01:12 +10:00
b6dea0d3b5 fix(api): return [""] when dynamic prompts generates no prompts
Just a simple fallback so we always have a prompt.
2023-09-19 14:01:12 +10:00
e720c2cf19 chore: isort 2023-09-19 14:01:12 +10:00
68cef6d90a fix(ui): only show cancel batch tooltip when batches were canceled 2023-09-19 14:01:12 +10:00
77150ab7cd fix(nodes): fix cancel_by_batch_ids result count 2023-09-19 14:01:12 +10:00
916404745c fix(ui): remove orphaned nodes from canvas graphs 2023-09-19 14:01:12 +10:00
a786615783 fix(ui): cleanup and use fixedCacheKey for all mutations 2023-09-19 14:01:12 +10:00
430e9346e6 fix(nodes): remove Batch.batch_id from required 2023-09-19 14:01:12 +10:00
2cd3bd8234 feat(ui): wip batch graphs & UI 2023-09-19 14:01:12 +10:00
89c01547cb feat(nodes): add cancel_by_batch_ids 2023-09-19 14:01:12 +10:00
eb65c12e61 feat(nodes): move cleanup til after invoker startup 2023-09-19 14:01:12 +10:00
ab5c3ed189 feat(ui): wip queue graphs 2023-09-19 14:01:12 +10:00
b0e3791a80 feat(queue): add queue_status_changed event 2023-09-19 14:01:12 +10:00
1dd6b3a508 feat(ui): dynamic prompts is always enabled 2023-09-19 14:01:12 +10:00
94e58a2254 feat(tests): add unit tests for session queue helper functions 2023-09-19 14:01:12 +10:00
d6aa55b965 fix(tests): fix up tests after changes to batches/queue 2023-09-19 14:01:12 +10:00
184a6cd85f fix(nodes): improve names of a few variables 2023-09-19 14:01:12 +10:00
e2924067ef fix(nodes): fix incorrect service stop() method 2023-09-19 14:01:12 +10:00
21d1e76ea9 chore: flake8, isort, black 2023-09-19 14:01:12 +10:00
bdeab50a82 feat: queued generation and batches
Due to a very messy branch with broad addition of `isort` on `main` alongside it, some git surgery was needed to get an agreeable git history. This commit represents all of the work on queued generation. See PR for notes.
2023-09-19 14:01:12 +10:00
40ff9ce672 fix(config): fix typing issues in config/
`config/invokeai_config.py`:
- use `Optional` for things that are optional
- fix typing of `ram_cache_size()` and `vram_cache_size()`
- remove unused and incorrectly typed method `autoconvert_path`
- fix types and logic for `parse_args()`, in which `InvokeAIAppConfig.initconf` *must* be a `DictConfig`, but function would allow it to be set as a `ListConfig`, which presumably would cause issues elsewhere

`config/base.py`:
- use `cls` for first arg of class methods
- use `Optional` for things that are optional
- fix minor type issue related to setting of `env_prefix`
- remove unused `add_subparser()` method, which calls `add_parser()` on an `ArgumentParser` (method only available on the `_SubParsersAction` object, which is returned from ArgumentParser.add_subparsers()`)
2023-09-19 14:01:12 +10:00
882 changed files with 23903 additions and 47445 deletions

1
.gitattributes vendored
View File

@ -2,4 +2,3 @@
# Only affects text files and ignores other file types. # Only affects text files and ignores other file types.
# For more info see: https://www.aleksandrhovhannisyan.com/blog/crlf-vs-lf-normalizing-line-endings-in-git/ # For more info see: https://www.aleksandrhovhannisyan.com/blog/crlf-vs-lf-normalizing-line-endings-in-git/
* text=auto * text=auto
docker/** text eol=lf

20
.github/workflows/pyflakes.yml vendored Normal file
View File

@ -0,0 +1,20 @@
on:
pull_request:
push:
branches:
- main
- development
- 'release-candidate-*'
jobs:
pyflakes:
name: runner / pyflakes
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: pyflakes
uses: reviewdog/action-pyflakes@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
reporter: github-pr-review

View File

@ -28,7 +28,7 @@ jobs:
run: twine check dist/* run: twine check dist/*
- name: check PyPI versions - name: check PyPI versions
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/') if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/v2.3'
run: | run: |
pip install --upgrade requests pip install --upgrade requests
python -c "\ python -c "\

View File

@ -6,7 +6,7 @@ on:
branches: main branches: main
jobs: jobs:
ruff: black:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -18,7 +18,8 @@ jobs:
- name: Install dependencies with pip - name: Install dependencies with pip
run: | run: |
pip install ruff pip install black flake8 Flake8-pyproject isort
- run: ruff check --output-format=github . - run: isort --check-only .
- run: ruff format --check . - run: black --check .
- run: flake8

12
.gitignore vendored
View File

@ -1,5 +1,8 @@
.idea/ .idea/
# ignore the Anaconda/Miniconda installer used while building Docker image
anaconda.sh
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
@ -133,10 +136,12 @@ celerybeat.pid
# Environments # Environments
.env .env
.venv* .venv
env/ env/
venv/ venv/
ENV/ ENV/
env.bak/
venv.bak/
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject
@ -181,6 +186,11 @@ cython_debug/
.scratch/ .scratch/
.vscode/ .vscode/
# ignore environment.yml and requirements.txt
# these are links to the real files in environments-and-requirements
environment.yml
requirements.txt
# source installer files # source installer files
installer/*zip installer/*zip
installer/install.bat installer/install.bat

View File

@ -123,7 +123,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.10 through 3.11 installed on your machine. Earlier or You must have Python 3.9 through 3.11 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)
@ -161,7 +161,7 @@ the command `npm install -g yarn` if needed)
_For Windows/Linux with an NVIDIA GPU:_ _For Windows/Linux with an NVIDIA GPU:_
```terminal ```terminal
pip install "InvokeAI[xformers]" --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu121 pip install "InvokeAI[xformers]" --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu118
``` ```
_For Linux with an AMD GPU:_ _For Linux with an AMD GPU:_
@ -175,7 +175,7 @@ the command `npm install -g yarn` if needed)
pip install InvokeAI --use-pep517 --extra-index-url https://download.pytorch.org/whl/cpu pip install InvokeAI --use-pep517 --extra-index-url https://download.pytorch.org/whl/cpu
``` ```
_For Macintoshes, either Intel or M1/M2/M3:_ _For Macintoshes, either Intel or M1/M2:_
```sh ```sh
pip install InvokeAI --use-pep517 pip install InvokeAI --use-pep517

View File

@ -1,15 +1,13 @@
## Make a copy of this file named `.env` and fill in the values below. ## Make a copy of this file named `.env` and fill in the values below.
## Any environment variables supported by InvokeAI can be specified here, ## Any environment variables supported by InvokeAI can be specified here.
## in addition to the examples below.
# INVOKEAI_ROOT is the path to a path on the local filesystem where InvokeAI will store data. # INVOKEAI_ROOT is the path to a path on the local filesystem where InvokeAI will store data.
# Outputs will also be stored here by default. # Outputs will also be stored here by default.
# This **must** be an absolute path. # This **must** be an absolute path.
INVOKEAI_ROOT= INVOKEAI_ROOT=
# Get this value from your HuggingFace account settings page. HUGGINGFACE_TOKEN=
# HUGGING_FACE_HUB_TOKEN=
## optional variables specific to the docker setup. ## optional variables specific to the docker setup
# GPU_DRIVER=cuda # or rocm # GPU_DRIVER=cuda
# CONTAINER_UID=1000 # CONTAINER_UID=1000

View File

@ -2,7 +2,7 @@
## Builder stage ## Builder stage
FROM library/ubuntu:23.04 AS builder FROM library/ubuntu:22.04 AS builder
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
@ -10,7 +10,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \
apt update && apt-get install -y \ apt update && apt-get install -y \
git \ git \
python3-venv \ python3.10-venv \
python3-pip \ python3-pip \
build-essential build-essential
@ -18,8 +18,8 @@ ENV INVOKEAI_SRC=/opt/invokeai
ENV VIRTUAL_ENV=/opt/venv/invokeai ENV VIRTUAL_ENV=/opt/venv/invokeai
ENV PATH="$VIRTUAL_ENV/bin:$PATH" ENV PATH="$VIRTUAL_ENV/bin:$PATH"
ARG TORCH_VERSION=2.1.0 ARG TORCH_VERSION=2.0.1
ARG TORCHVISION_VERSION=0.16 ARG TORCHVISION_VERSION=0.15.2
ARG GPU_DRIVER=cuda ARG GPU_DRIVER=cuda
ARG TARGETPLATFORM="linux/amd64" ARG TARGETPLATFORM="linux/amd64"
# unused but available # unused but available
@ -35,9 +35,9 @@ RUN --mount=type=cache,target=/root/.cache/pip \
if [ "$TARGETPLATFORM" = "linux/arm64" ] || [ "$GPU_DRIVER" = "cpu" ]; then \ if [ "$TARGETPLATFORM" = "linux/arm64" ] || [ "$GPU_DRIVER" = "cpu" ]; then \
extra_index_url_arg="--extra-index-url https://download.pytorch.org/whl/cpu"; \ extra_index_url_arg="--extra-index-url https://download.pytorch.org/whl/cpu"; \
elif [ "$GPU_DRIVER" = "rocm" ]; then \ elif [ "$GPU_DRIVER" = "rocm" ]; then \
extra_index_url_arg="--index-url https://download.pytorch.org/whl/rocm5.6"; \ extra_index_url_arg="--extra-index-url https://download.pytorch.org/whl/rocm5.4.2"; \
else \ else \
extra_index_url_arg="--extra-index-url https://download.pytorch.org/whl/cu121"; \ extra_index_url_arg="--extra-index-url https://download.pytorch.org/whl/cu118"; \
fi &&\ fi &&\
pip install $extra_index_url_arg \ pip install $extra_index_url_arg \
torch==$TORCH_VERSION \ torch==$TORCH_VERSION \
@ -70,7 +70,7 @@ RUN --mount=type=cache,target=/usr/lib/node_modules \
#### Runtime stage --------------------------------------- #### Runtime stage ---------------------------------------
FROM library/ubuntu:23.04 AS runtime FROM library/ubuntu:22.04 AS runtime
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
@ -85,7 +85,6 @@ RUN apt update && apt install -y --no-install-recommends \
iotop \ iotop \
bzip2 \ bzip2 \
gosu \ gosu \
magic-wormhole \
libglib2.0-0 \ libglib2.0-0 \
libgl1-mesa-glx \ libgl1-mesa-glx \
python3-venv \ python3-venv \
@ -95,6 +94,10 @@ RUN apt update && apt install -y --no-install-recommends \
libstdc++-10-dev &&\ libstdc++-10-dev &&\
apt-get clean && apt-get autoclean apt-get clean && apt-get autoclean
# globally add magic-wormhole
# for ease of transferring data to and from the container
# when running in sandboxed cloud environments; e.g. Runpod etc.
RUN pip install magic-wormhole
ENV INVOKEAI_SRC=/opt/invokeai ENV INVOKEAI_SRC=/opt/invokeai
ENV VIRTUAL_ENV=/opt/venv/invokeai ENV VIRTUAL_ENV=/opt/venv/invokeai
@ -117,7 +120,9 @@ WORKDIR ${INVOKEAI_SRC}
RUN cd /usr/lib/$(uname -p)-linux-gnu/pkgconfig/ && ln -sf opencv4.pc opencv.pc RUN cd /usr/lib/$(uname -p)-linux-gnu/pkgconfig/ && ln -sf opencv4.pc opencv.pc
RUN python3 -c "from patchmatch import patch_match" RUN python3 -c "from patchmatch import patch_match"
RUN mkdir -p ${INVOKEAI_ROOT} && chown -R 1000:1000 ${INVOKEAI_ROOT} # Create unprivileged user and make the local dir
RUN useradd --create-home --shell /bin/bash -u 1000 --comment "container local user" invoke
RUN mkdir -p ${INVOKEAI_ROOT} && chown -R invoke:invoke ${INVOKEAI_ROOT}
COPY docker/docker-entrypoint.sh ./ COPY docker/docker-entrypoint.sh ./
ENTRYPOINT ["/opt/invokeai/docker-entrypoint.sh"] ENTRYPOINT ["/opt/invokeai/docker-entrypoint.sh"]

View File

@ -5,7 +5,7 @@ All commands are to be run from the `docker` directory: `cd docker`
#### Linux #### Linux
1. Ensure builkit is enabled in the Docker daemon settings (`/etc/docker/daemon.json`) 1. Ensure builkit is enabled in the Docker daemon settings (`/etc/docker/daemon.json`)
2. Install the `docker compose` plugin using your package manager, or follow a [tutorial](https://docs.docker.com/compose/install/linux/#install-using-the-repository). 2. Install the `docker compose` plugin using your package manager, or follow a [tutorial](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-compose-on-ubuntu-22-04).
- The deprecated `docker-compose` (hyphenated) CLI continues to work for now. - The deprecated `docker-compose` (hyphenated) CLI continues to work for now.
3. Ensure docker daemon is able to access the GPU. 3. Ensure docker daemon is able to access the GPU.
- You may need to install [nvidia-container-toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) - You may need to install [nvidia-container-toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)
@ -20,6 +20,7 @@ This is done via Docker Desktop preferences
## Quickstart ## Quickstart
1. Make a copy of `env.sample` and name it `.env` (`cp env.sample .env` (Mac/Linux) or `copy example.env .env` (Windows)). Make changes as necessary. Set `INVOKEAI_ROOT` to an absolute path to: 1. Make a copy of `env.sample` and name it `.env` (`cp env.sample .env` (Mac/Linux) or `copy example.env .env` (Windows)). Make changes as necessary. Set `INVOKEAI_ROOT` to an absolute path to:
a. the desired location of the InvokeAI runtime directory, or a. the desired location of the InvokeAI runtime directory, or
b. an existing, v3.0.0 compatible runtime directory. b. an existing, v3.0.0 compatible runtime directory.
@ -41,22 +42,20 @@ The Docker daemon on the system must be already set up to use the GPU. In case o
Check the `.env.sample` file. It contains some environment variables for running in Docker. Copy it, name it `.env`, and fill it in with your own values. Next time you run `docker compose up`, your custom values will be used. Check the `.env.sample` file. It contains some environment variables for running in Docker. Copy it, name it `.env`, and fill it in with your own values. Next time you run `docker compose up`, your custom values will be used.
You can also set these values in `docker-compose.yml` directly, but `.env` will help avoid conflicts when code is updated. You can also set these values in `docker compose.yml` directly, but `.env` will help avoid conflicts when code is updated.
Example (values are optional, but setting `INVOKEAI_ROOT` is highly recommended): Example (most values are optional):
```bash ```
INVOKEAI_ROOT=/Volumes/WorkDrive/invokeai INVOKEAI_ROOT=/Volumes/WorkDrive/invokeai
HUGGINGFACE_TOKEN=the_actual_token HUGGINGFACE_TOKEN=the_actual_token
CONTAINER_UID=1000 CONTAINER_UID=1000
GPU_DRIVER=cuda GPU_DRIVER=cuda
``` ```
Any environment variables supported by InvokeAI can be set here - please see the [Configuration docs](https://invoke-ai.github.io/InvokeAI/features/CONFIGURATION/) for further detail.
## Even Moar Customizing! ## Even Moar Customizing!
See the `docker-compose.yml` file. The `command` instruction can be uncommented and used to run arbitrary startup commands. Some examples below. See the `docker compose.yaml` file. The `command` instruction can be uncommented and used to run arbitrary startup commands. Some examples below.
### Reconfigure the runtime directory ### Reconfigure the runtime directory
@ -64,7 +63,7 @@ Can be used to download additional models from the supported model list
In conjunction with `INVOKEAI_ROOT` can be also used to initialize a runtime directory In conjunction with `INVOKEAI_ROOT` can be also used to initialize a runtime directory
```yaml ```
command: command:
- invokeai-configure - invokeai-configure
- --yes - --yes
@ -72,7 +71,7 @@ command:
Or install models: Or install models:
```yaml ```
command: command:
- invokeai-model-install - invokeai-model-install
``` ```

View File

@ -5,7 +5,7 @@ build_args=""
[[ -f ".env" ]] && build_args=$(awk '$1 ~ /\=[^$]/ {print "--build-arg " $0 " "}' .env) [[ -f ".env" ]] && build_args=$(awk '$1 ~ /\=[^$]/ {print "--build-arg " $0 " "}' .env)
echo "docker compose build args:" echo "docker-compose build args:"
echo $build_args echo $build_args
docker compose build $build_args docker-compose build $build_args

View File

@ -15,10 +15,6 @@ services:
- driver: nvidia - driver: nvidia
count: 1 count: 1
capabilities: [gpu] capabilities: [gpu]
# For AMD support, comment out the deploy section above and uncomment the devices section below:
#devices:
# - /dev/kfd:/dev/kfd
# - /dev/dri:/dev/dri
build: build:
context: .. context: ..
dockerfile: docker/Dockerfile dockerfile: docker/Dockerfile

View File

@ -19,7 +19,7 @@ set -e -o pipefail
# Default UID: 1000 chosen due to popularity on Linux systems. Possibly 501 on MacOS. # Default UID: 1000 chosen due to popularity on Linux systems. Possibly 501 on MacOS.
USER_ID=${CONTAINER_UID:-1000} USER_ID=${CONTAINER_UID:-1000}
USER=ubuntu USER=invoke
usermod -u ${USER_ID} ${USER} 1>/dev/null usermod -u ${USER_ID} ${USER} 1>/dev/null
configure() { configure() {

View File

@ -1,11 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
# This script is provided for backwards compatibility with the old docker setup.
# it doesn't do much aside from wrapping the usual docker compose CLI.
SCRIPTDIR=$(dirname "${BASH_SOURCE[0]}") SCRIPTDIR=$(dirname "${BASH_SOURCE[0]}")
cd "$SCRIPTDIR" || exit 1 cd "$SCRIPTDIR" || exit 1
docker compose up -d docker-compose up --build -d
docker compose logs -f docker-compose logs -f

View File

@ -488,7 +488,7 @@ sections describe what's new for InvokeAI.
- A choice of installer scripts that automate installation and configuration. - A choice of installer scripts that automate installation and configuration.
See See
[Installation](installation/INSTALLATION.md). [Installation](installation/index.md).
- A streamlined manual installation process that works for both Conda and - A streamlined manual installation process that works for both Conda and
PIP-only installs. See PIP-only installs. See
[Manual Installation](installation/020_INSTALL_MANUAL.md). [Manual Installation](installation/020_INSTALL_MANUAL.md).
@ -657,7 +657,7 @@ sections describe what's new for InvokeAI.
## v1.13 <small>(3 September 2022)</small> ## v1.13 <small>(3 September 2022)</small>
- Support image variations (see [VARIATIONS](deprecated/VARIATIONS.md) - Support image variations (see [VARIATIONS](features/VARIATIONS.md)
([Kevin Gibbons](https://github.com/bakkot) and many contributors and ([Kevin Gibbons](https://github.com/bakkot) and many contributors and
reviewers) reviewers)
- Supports a Google Colab notebook for a standalone server running on Google - Supports a Google Colab notebook for a standalone server running on Google

View File

@ -47,9 +47,34 @@ pip install ".[dev,test]"
These are optional groups of packages which are defined within the `pyproject.toml` These are optional groups of packages which are defined within the `pyproject.toml`
and will be required for testing the changes you make to the code. and will be required for testing the changes you make to the code.
### Tests ### Running Tests
We use [pytest](https://docs.pytest.org/en/7.2.x/) for our test suite. Tests can
be found under the `./tests` folder and can be run with a single `pytest`
command. Optionally, to review test coverage you can append `--cov`.
```zsh
pytest --cov
```
Test outcomes and coverage will be reported in the terminal. In addition a more
detailed report is created in both XML and HTML format in the `./coverage`
folder. The HTML one in particular can help identify missing statements
requiring tests to ensure coverage. This can be run by opening
`./coverage/html/index.html`.
For example.
```zsh
pytest --cov; open ./coverage/html/index.html
```
??? info "HTML coverage report output"
![html-overview](../assets/contributing/html-overview.png)
![html-detail](../assets/contributing/html-detail.png)
See the [tests documentation](./TESTS.md) for information about running and writing tests.
### Reloading Changes ### Reloading Changes
Experimenting with changes to the Python source code is a drag if you have to re-start the server — Experimenting with changes to the Python source code is a drag if you have to re-start the server —
@ -142,23 +167,6 @@ and so you'll have access to the same python environment as the InvokeAI app.
This is _super_ handy. This is _super_ handy.
#### Enabling Type-Checking with Pylance
We use python's typing system in InvokeAI. PR reviews will include checking that types are present and correct. We don't enforce types with `mypy` at this time, but that is on the horizon.
Using a code analysis tool to automatically type check your code (and types) is very important when writing with types. These tools provide immediate feedback in your editor when types are incorrect, and following their suggestions lead to fewer runtime bugs.
Pylance, installed at the beginning of this guide, is the de-facto python LSP (language server protocol). It provides type checking in the editor (among many other features). Once installed, you do need to enable type checking manually:
- Open a python file
- Look along the status bar in VSCode for `{ } Python`
- Click the `{ }`
- Turn type checking on - basic is fine
You'll now see red squiggly lines where type issues are detected. Hover your cursor over the indicated symbols to see what's wrong.
In 99% of cases when the type checker says there is a problem, there really is a problem, and you should take some time to understand and resolve what it is pointing out.
#### Debugging configs with `launch.json` #### Debugging configs with `launch.json`
Debugging configs are managed in a `launch.json` file. Like most VSCode configs, Debugging configs are managed in a `launch.json` file. Like most VSCode configs,

File diff suppressed because it is too large Load Diff

View File

@ -1,89 +0,0 @@
# InvokeAI Backend Tests
We use `pytest` to run the backend python tests. (See [pyproject.toml](/pyproject.toml) for the default `pytest` options.)
## Fast vs. Slow
All tests are categorized as either 'fast' (no test annotation) or 'slow' (annotated with the `@pytest.mark.slow` decorator).
'Fast' tests are run to validate every PR, and are fast enough that they can be run routinely during development.
'Slow' tests are currently only run manually on an ad-hoc basis. In the future, they may be automated to run nightly. Most developers are only expected to run the 'slow' tests that directly relate to the feature(s) that they are working on.
As a rule of thumb, tests should be marked as 'slow' if there is a chance that they take >1s (e.g. on a CPU-only machine with slow internet connection). Common examples of slow tests are tests that depend on downloading a model, or running model inference.
## Running Tests
Below are some common test commands:
```bash
# Run the fast tests. (This implicitly uses the configured default option: `-m "not slow"`.)
pytest tests/
# Equivalent command to run the fast tests.
pytest tests/ -m "not slow"
# Run the slow tests.
pytest tests/ -m "slow"
# Run the slow tests from a specific file.
pytest tests/path/to/slow_test.py -m "slow"
# Run all tests (fast and slow).
pytest tests -m ""
```
## Test Organization
All backend tests are in the [`tests/`](/tests/) directory. This directory mirrors the organization of the `invokeai/` directory. For example, tests for `invokeai/model_management/model_manager.py` would be found in `tests/model_management/test_model_manager.py`.
TODO: The above statement is aspirational. A re-organization of legacy tests is required to make it true.
## Tests that depend on models
There are a few things to keep in mind when adding tests that depend on models.
1. If a required model is not already present, it should automatically be downloaded as part of the test setup.
2. If a model is already downloaded, it should not be re-downloaded unnecessarily.
3. Take reasonable care to keep the total number of models required for the tests low. Whenever possible, re-use models that are already required for other tests. If you are adding a new model, consider including a comment to explain why it is required/unique.
There are several utilities to help with model setup for tests. Here is a sample test that depends on a model:
```python
import pytest
import torch
from invokeai.backend.model_management.models.base import BaseModelType, ModelType
from invokeai.backend.util.test_utils import install_and_load_model
@pytest.mark.slow
def test_model(model_installer, torch_device):
model_info = install_and_load_model(
model_installer=model_installer,
model_path_id_or_url="HF/dummy_model_id",
model_name="dummy_model",
base_model=BaseModelType.StableDiffusion1,
model_type=ModelType.Dummy,
)
dummy_input = build_dummy_input(torch_device)
with torch.no_grad(), model_info as model:
model.to(torch_device, dtype=torch.float32)
output = model(dummy_input)
# Validate output...
```
## Test Coverage
To review test coverage, append `--cov` to your pytest command:
```bash
pytest tests/ --cov
```
Test outcomes and coverage will be reported in the terminal. In addition, a more detailed report is created in both XML and HTML format in the `./coverage` folder. The HTML output is particularly helpful in identifying untested statements where coverage should be improved. The HTML report can be viewed by opening `./coverage/html/index.html`.
??? info "HTML coverage report output"
![html-overview](../assets/contributing/html-overview.png)
![html-detail](../assets/contributing/html-detail.png)

View File

@ -12,7 +12,7 @@ To get started, take a look at our [new contributors checklist](newContributorCh
Once you're setup, for more information, you can review the documentation specific to your area of interest: Once you're setup, for more information, you can review the documentation specific to your area of interest:
* #### [InvokeAI Architecure](../ARCHITECTURE.md) * #### [InvokeAI Architecure](../ARCHITECTURE.md)
* #### [Frontend Documentation](./contributingToFrontend.md) * #### [Frontend Documentation](development_guides/contributingToFrontend.md)
* #### [Node Documentation](../INVOCATIONS.md) * #### [Node Documentation](../INVOCATIONS.md)
* #### [Local Development](../LOCAL_DEVELOPMENT.md) * #### [Local Development](../LOCAL_DEVELOPMENT.md)
@ -38,12 +38,12 @@ There are two paths to making a development contribution:
If you need help, you can ask questions in the [#dev-chat](https://discord.com/channels/1020123559063990373/1049495067846524939) channel of the Discord. 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, **@psychedelicious** is the best person to reach out to. 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 **@psychedelicious**. For backend related work, please reach out to **@blessedcoolant**, **@lstein**, **@StAlKeR7779** or **@pyschedelicious**.
## **What does the Code of Conduct mean for me?** ## **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. 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

@ -10,4 +10,4 @@ When updating or creating documentation, please keep in mind InvokeAI is a tool
## Help & Questions ## Help & Questions
Please ping @imic or @hipsterusername in the [Discord](https://discord.com/channels/1020123559063990373/1049495067846524939) if you have any questions. Please ping @imic1 or @hipsterusername in the [Discord](https://discord.com/channels/1020123559063990373/1049495067846524939) if you have any questions.

View File

@ -211,8 +211,8 @@ Here are the invoke> command that apply to txt2img:
| `--facetool <name>` | `-ft <name>` | `-ft gfpgan` | Select face restoration algorithm to use: gfpgan, codeformer | | `--facetool <name>` | `-ft <name>` | `-ft gfpgan` | Select face restoration algorithm to use: gfpgan, codeformer |
| `--codeformer_fidelity` | `-cf <float>` | `0.75` | Used along with CodeFormer. Takes values between 0 and 1. 0 produces high quality but low accuracy. 1 produces high accuracy but low quality | | `--codeformer_fidelity` | `-cf <float>` | `0.75` | Used along with CodeFormer. Takes values between 0 and 1. 0 produces high quality but low accuracy. 1 produces high accuracy but low quality |
| `--save_original` | `-save_orig` | `False` | When upscaling or fixing faces, this will cause the original image to be saved rather than replaced. | | `--save_original` | `-save_orig` | `False` | When upscaling or fixing faces, this will cause the original image to be saved rather than replaced. |
| `--variation <float>` | `-v<float>` | `0.0` | Add a bit of noise (0.0=none, 1.0=high) to the image in order to generate a series of variations. Usually used in combination with `-S<seed>` and `-n<int>` to generate a series a riffs on a starting image. See [Variations](VARIATIONS.md). | | `--variation <float>` | `-v<float>` | `0.0` | Add a bit of noise (0.0=none, 1.0=high) to the image in order to generate a series of variations. Usually used in combination with `-S<seed>` and `-n<int>` to generate a series a riffs on a starting image. See [Variations](../features/VARIATIONS.md). |
| `--with_variations <pattern>` | | `None` | Combine two or more variations. See [Variations](VARIATIONS.md) for now to use this. | | `--with_variations <pattern>` | | `None` | Combine two or more variations. See [Variations](../features/VARIATIONS.md) for now to use this. |
| `--save_intermediates <n>` | | `None` | Save the image from every nth step into an "intermediates" folder inside the output directory | | `--save_intermediates <n>` | | `None` | Save the image from every nth step into an "intermediates" folder inside the output directory |
| `--h_symmetry_time_pct <float>` | | `None` | Create symmetry along the X axis at the desired percent complete of the generation process. (Must be between 0.0 and 1.0; set to a very small number like 0.0001 for just after the first step of generation.) | | `--h_symmetry_time_pct <float>` | | `None` | Create symmetry along the X axis at the desired percent complete of the generation process. (Must be between 0.0 and 1.0; set to a very small number like 0.0001 for just after the first step of generation.) |
| `--v_symmetry_time_pct <float>` | | `None` | Create symmetry along the Y axis at the desired percent complete of the generation process. (Must be between 0.0 and 1.0; set to a very small number like 0.0001 for just after the first step of generation.) | | `--v_symmetry_time_pct <float>` | | `None` | Create symmetry along the Y axis at the desired percent complete of the generation process. (Must be between 0.0 and 1.0; set to a very small number like 0.0001 for just after the first step of generation.) |

View File

@ -28,9 +28,8 @@ by placing them in the designated directory for the compatible model type
### An Example ### An Example
Here are a few examples to illustrate how it works. All these images Here are a few examples to illustrate how it works. All these images were
were generated using the legacy command-line client and the Stable generated using the command-line client and the Stable Diffusion 1.5 model:
Diffusion 1.5 model:
| Japanese gardener | Japanese gardener &lt;ghibli-face&gt; | Japanese gardener &lt;hoi4-leaders&gt; | Japanese gardener &lt;cartoona-animals&gt; | | Japanese gardener | Japanese gardener &lt;ghibli-face&gt; | Japanese gardener &lt;hoi4-leaders&gt; | Japanese gardener &lt;cartoona-animals&gt; |
| :--------------------------------: | :-----------------------------------: | :------------------------------------: | :----------------------------------------: | | :--------------------------------: | :-----------------------------------: | :------------------------------------: | :----------------------------------------: |

View File

@ -82,7 +82,7 @@ format of YAML files can be found
[here](https://circleci.com/blog/what-is-yaml-a-beginner-s-guide/). [here](https://circleci.com/blog/what-is-yaml-a-beginner-s-guide/).
You can fix a broken `invokeai.yaml` by deleting it and running the You can fix a broken `invokeai.yaml` by deleting it and running the
configuration script again -- option [6] in the launcher, "Re-run the configuration script again -- option [7] in the launcher, "Re-run the
configure script". configure script".
#### Reading Environment Variables #### Reading Environment Variables
@ -159,7 +159,7 @@ groups in `invokeia.yaml`:
| `host` | `localhost` | Name or IP address of the network interface that the web server will listen on | | `host` | `localhost` | Name or IP address of the network interface that the web server will listen on |
| `port` | `9090` | Network port number that the web server will listen on | | `port` | `9090` | Network port number that the web server will listen on |
| `allow_origins` | `[]` | A list of host names or IP addresses that are allowed to connect to the InvokeAI API in the format `['host1','host2',...]` | | `allow_origins` | `[]` | A list of host names or IP addresses that are allowed to connect to the InvokeAI API in the format `['host1','host2',...]` |
| `allow_credentials` | `true` | Require credentials for a foreign host to access the InvokeAI API (don't change this) | | `allow_credentials | `true` | Require credentials for a foreign host to access the InvokeAI API (don't change this) |
| `allow_methods` | `*` | List of HTTP methods ("GET", "POST") that the web server is allowed to use when accessing the API | | `allow_methods` | `*` | List of HTTP methods ("GET", "POST") that the web server is allowed to use when accessing the API |
| `allow_headers` | `*` | List of HTTP headers that the web server will accept when accessing the API | | `allow_headers` | `*` | List of HTTP headers that the web server will accept when accessing the API |

View File

@ -1,11 +1,13 @@
--- ---
title: Control Adapters title: ControlNet
--- ---
# :material-loupe: Control Adapters # :material-loupe: ControlNet
## 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 community (notably, Stanford researcher
[**@ilyasviel**](https://github.com/lllyasviel)) that allows you to [**@ilyasviel**](https://github.com/lllyasviel)) that allows you to
@ -17,6 +19,9 @@ image generation, providing you with a way to direct the network
towards generating images that better fit your desired style or towards generating images that better fit your desired style or
outcome. outcome.
### 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 image to identify relevant information that can be interpreted by each
specific ControlNet model, and then inserting that control information specific ControlNet model, and then inserting that control information
@ -24,21 +29,35 @@ into the generation process. This can be used to adjust the style,
composition, or other aspects of the image to better achieve a composition, or other aspects of the image to better achieve a
specific result. specific result.
#### Installation
### Models
InvokeAI provides access to a series of ControlNet models that provide InvokeAI provides access to a series of ControlNet models that provide
different effects or styles in your generated images. 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.
To install ControlNet Models: ***InvokeAI does not currently support checkpoint-format
ControlNets. These come in the form of a single file with the
extension `.safetensors`.***
1. The easiest way to install them is 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 to use the InvokeAI model installer application. Use the
`invoke.sh`/`invoke.bat` launcher to select item [4] and then navigate `invoke.sh`/`invoke.bat` launcher to select item [5] and then navigate
to the CONTROLNETS section. Select the models you wish to install and to the CONTROLNETS section. Select the models you wish to install and
press "APPLY CHANGES". You may also enter additional HuggingFace press "APPLY CHANGES". You may also enter additional HuggingFace
repo_ids in the "Additional models" textbox. repo_ids in the "Additional models" textbox:
2. Using the "Add Model" function of the model manager, enter the HuggingFace Repo ID of the ControlNet. The ID is in the format "author/repoName"
![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 _Be aware that some ControlNet models require additional code
functionality in order to work properly, so just installing a functionality in order to work properly, so just installing a
@ -46,17 +65,6 @@ third-party ControlNet model may not have the desired effect._ Please
read and follow the documentation for installing a third party model read and follow the documentation for installing a third party model
not currently included among InvokeAI's default list. not currently included among InvokeAI's default list.
Currently InvokeAI **only** supports 🤗 Diffusers-format 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.
🤗 Diffusers-format ControlNet models are available at HuggingFace
(http://huggingface.co) and accessed via their repo IDs (identifiers
in the format "author/modelname").
#### ControlNet Models
The models currently supported include: The models currently supported include:
**Canny**: **Canny**:
@ -88,8 +96,6 @@ A model that generates normal maps from input images, allowing for more realisti
**Image Segmentation**: **Image Segmentation**:
A model that divides input images into segments or regions, each of which corresponds to a different object or part of the image. (More details coming soon) A model that divides input images into segments or regions, each of which corresponds to a different object or part of the image. (More details coming soon)
**QR Code Monster**:
A model that helps generate creative QR codes that still scan. Can also be used to create images with text, logos or shapes within them.
**Openpose**: **Openpose**:
The OpenPose control model allows for the identification of the general pose of a character by pre-processing an existing image with a clear human structure. With advanced options, Openpose can also detect the face or hands in the image. The OpenPose control model allows for the identification of the general pose of a character by pre-processing an existing image with a clear human structure. With advanced options, Openpose can also detect the face or hands in the image.
@ -114,7 +120,7 @@ With Pix2Pix, you can input an image into the controlnet, and then "instruct" th
Each of these models can be adjusted and combined with other ControlNet models to achieve different results, giving you even more control over your image generation process. Each of these models can be adjusted and combined with other ControlNet models to achieve different results, giving you even more control over your image generation process.
### Using ControlNet ## Using ControlNet
To use ControlNet, you can simply select the desired model and adjust both the ControlNet and Pre-processor settings to achieve the desired result. You can also use multiple ControlNet models at the same time, allowing you to achieve even more complex effects or styles in your generated images. To use ControlNet, you can simply select the desired model and adjust both the ControlNet and Pre-processor settings to achieve the desired result. You can also use multiple ControlNet models at the same time, allowing you to achieve even more complex effects or styles in your generated images.
@ -126,54 +132,3 @@ Weight - Strength of the Controlnet model applied to the generation for the sect
Start/End - 0 represents the start of the generation, 1 represents the end. The Start/end setting controls what steps during the generation process have the ControlNet applied. Start/End - 0 represents the start of the generation, 1 represents the end. The Start/end setting controls what steps during the generation process have the ControlNet applied.
Additionally, each ControlNet section can be expanded in order to manipulate settings for the image pre-processor that adjusts your uploaded image before using it in when you Invoke. Additionally, each ControlNet section can be expanded in order to manipulate settings for the image pre-processor that adjusts your uploaded image before using it in when you Invoke.
## T2I-Adapter
[T2I-Adapter](https://github.com/TencentARC/T2I-Adapter) is a tool similar to ControlNet that allows for control over the generation process by providing control information during the generation process. T2I-Adapter models tend to be smaller and more efficient than ControlNets.
##### Installation
To install T2I-Adapter Models:
1. The easiest way to install models is
to use the InvokeAI model installer application. Use the
`invoke.sh`/`invoke.bat` launcher to select item [5] and then navigate
to the T2I-Adapters 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.
2. Using the "Add Model" function of the model manager, enter the HuggingFace Repo ID of the T2I-Adapter. The ID is in the format "author/repoName"
#### Usage
Each T2I Adapter has two settings that are applied.
Weight - Strength of the model applied to the generation for the section, defined by start/end.
Start/End - 0 represents the start of the generation, 1 represents the end. The Start/end setting controls what steps during the generation process have the ControlNet applied.
Additionally, each section can be expanded with the "Show Advanced" button in order to manipulate settings for the image pre-processor that adjusts your uploaded image before using it in during the generation process.
## IP-Adapter
[IP-Adapter](https://ip-adapter.github.io) is a tooling that allows for image prompt capabilities with text-to-image diffusion models. IP-Adapter works by analyzing the given image prompt to extract features, then passing those features to the UNet along with any other conditioning provided.
![IP-Adapter + T2I](https://github.com/tencent-ailab/IP-Adapter/raw/main/assets/demo/ip_adpter_plus_multi.jpg)
![IP-Adapter + IMG2IMG](https://raw.githubusercontent.com/tencent-ailab/IP-Adapter/main/assets/demo/image-to-image.jpg)
#### Installation
There are several ways to install IP-Adapter models with an existing InvokeAI installation:
1. Through the command line interface launched from the invoke.sh / invoke.bat scripts, option [4] to download models.
2. Through the Model Manager UI with models from the *Tools* section of [www.models.invoke.ai](https://www.models.invoke.ai). To do this, copy the repo ID from the desired model page, and paste it in the Add Model field of the model manager. **Note** Both the IP-Adapter and the Image Encoder must be installed for IP-Adapter to work. For example, the [SD 1.5 IP-Adapter](https://models.invoke.ai/InvokeAI/ip_adapter_plus_sd15) and [SD1.5 Image Encoder](https://models.invoke.ai/InvokeAI/ip_adapter_sd_image_encoder) must be installed to use IP-Adapter with SD1.5 based models.
3. **Advanced -- Not recommended ** Manually downloading the IP-Adapter and Image Encoder files - Image Encoder folders shouid be placed in the `models\any\clip_vision` folders. IP Adapter Model folders should be placed in the relevant `ip-adapter` folder of relevant base model folder of Invoke root directory. For example, for the SDXL IP-Adapter, files should be added to the `model/sdxl/ip_adapter/` folder.
#### Using IP-Adapter
IP-Adapter can be used by navigating to the *Control Adapters* options and enabling IP-Adapter.
IP-Adapter requires an image to be used as the Image Prompt. It can also be used in conjunction with text prompts, Image-to-Image, Inpainting, Outpainting, ControlNets and LoRAs.
Each IP-Adapter has two settings that are applied to the IP-Adapter:
* Weight - Strength of the IP-Adapter model applied to the generation for the section, defined by start/end
* Start/End - 0 represents the start of the generation, 1 represents the end. The Start/end setting controls what steps during the generation process have the IP-Adapter applied.

View File

@ -16,10 +16,9 @@ Model Merging can be be done by navigating to the Model Manager and clicking the
display all the diffusers-style models that InvokeAI knows about. display all the diffusers-style models that InvokeAI knows about.
If you do not see the model you are looking for, then it is probably If you do not see the model you are looking for, then it is probably
a legacy checkpoint model and needs to be converted using the a legacy checkpoint model and needs to be converted using the
"Convert" option in the Web-based Model Manager tab. `invoke` command-line client and its `!optimize` command. You
must select at least two models to merge. The third can be left at
You must select at least two models to merge. The third can be left "None" if you desire.
at "None" if you desire.
* Alpha: This is the ratio to use when combining models. It ranges * Alpha: This is the ratio to use when combining models. It ranges
from 0 to 1. The higher the value, the more weight is given to the from 0 to 1. The higher the value, the more weight is given to the

View File

@ -1,336 +0,0 @@
---
title: Command-line Utilities
---
# :material-file-document: Utilities
# Command-line Utilities
InvokeAI comes with several scripts that are accessible via the
command line. To access these commands, start the "developer's
console" from the launcher (`invoke.bat` menu item [7]). Users who are
familiar with Python can alternatively activate InvokeAI's virtual
environment (typically, but not necessarily `invokeai/.venv`).
In the developer's console, type the script's name to run it. To get a
synopsis of what a utility does and the command-line arguments it
accepts, pass it the `-h` argument, e.g.
```bash
invokeai-merge -h
```
## **invokeai-web**
This script launches the web server and is effectively identical to
selecting option [1] in the launcher. An advantage of launching the
server from the command line is that you can override any setting
configuration option in `invokeai.yaml` using like-named command-line
arguments. For example, to temporarily change the size of the RAM
cache to 7 GB, you can launch as follows:
```bash
invokeai-web --ram 7
```
## **invokeai-merge**
This is the model merge script, the same as launcher option [3]. Call
it with the `--gui` command-line argument to start the interactive
console-based GUI. Alternatively, you can run it non-interactively
using command-line arguments as illustrated in the example below which
merges models named `stable-diffusion-1.5` and `inkdiffusion` into a new model named
`my_new_model`:
```bash
invokeai-merge --force --base-model sd-1 --models stable-diffusion-1.5 inkdiffusion --merged_model_name my_new_model
```
## **invokeai-ti**
This is the textual inversion training script that is run by launcher
option [2]. Call it with `--gui` to run the interactive console-based
front end. It can also be run non-interactively. It has about a
zillion arguments, but a typical training session can be launched
with:
```bash
invokeai-ti --model stable-diffusion-1.5 \
--placeholder_token 'jello' \
--learnable_property object \
--num_train_epochs 50 \
--train_data_dir /path/to/training/images \
--output_dir /path/to/trained/model
```
(Note that \\ is the Linux/Mac long-line continuation character. Use ^
in Windows).
## **invokeai-install**
This is the console-based model install script that is run by launcher
option [4]. If called without arguments, it will launch the
interactive console-based interface. It can also be used
non-interactively to list, add and remove models as shown by these
examples:
* This will download and install three models from CivitAI, HuggingFace,
and local disk:
```bash
invokeai-install --add https://civitai.com/api/download/models/161302 ^
gsdf/Counterfeit-V3.0 ^
D:\Models\merge_model_two.safetensors
```
(Note that ^ is the Windows long-line continuation character. Use \\ on
Linux/Mac).
* This will list installed models of type `main`:
```bash
invokeai-model-install --list-models main
```
* This will delete the models named `voxel-ish` and `realisticVision`:
```bash
invokeai-model-install --delete voxel-ish realisticVision
```
## **invokeai-configure**
This is the console-based configure script that ran when InvokeAI was
first installed. You can run it again at any time to change the
configuration, repair a broken install.
Called without any arguments, `invokeai-configure` enters interactive
mode with two screens. The first screen is a form that provides access
to most of InvokeAI's configuration options. The second screen lets
you download, add, and delete models interactively. When you exit the
second screen, the script will add any missing "support models"
needed for core functionality, and any selected "sd weights" which are
the model checkpoint/diffusers files.
This behavior can be changed via a series of command-line
arguments. Here are some of the useful ones:
* `invokeai-configure --skip-sd-weights --skip-support-models`
This will run just the configuration part of the utility, skipping
downloading of support models and stable diffusion weights.
* `invokeai-configure --yes`
This will run the configure script non-interactively. It will set the
configuration options to their default values, install/repair support
models, and download the "recommended" set of SD models.
* `invokeai-configure --yes --default_only`
This will run the configure script non-interactively. In contrast to
the previous command, it will only download the default SD model,
Stable Diffusion v1.5
* `invokeai-configure --yes --default_only --skip-sd-weights`
This is similar to the previous command, but will not download any
SD models at all. It is usually used to repair a broken install.
By default, `invokeai-configure` runs on the currently active InvokeAI
root folder. To run it against a different root, pass it the `--root
</path/to/root>` argument.
Lastly, you can use `invokeai-configure` to create a working root
directory entirely from scratch. Assuming you wish to make a root directory
named `InvokeAI-New`, run this command:
```bash
invokeai-configure --root InvokeAI-New --yes --default_only
```
This will create a minimally functional root directory. You can now
launch the web server against it with `invokeai-web --root InvokeAI-New`.
## **invokeai-update**
This is the interactive console-based script that is run by launcher
menu item [8] to update to a new version of InvokeAI. It takes no
command-line arguments.
## **invokeai-metadata**
This is a script which takes a list of InvokeAI-generated images and
outputs their metadata in the same JSON format that you get from the
`</>` button in the Web GUI. For example:
```bash
$ invokeai-metadata ffe2a115-b492-493c-afff-7679aa034b50.png
ffe2a115-b492-493c-afff-7679aa034b50.png:
{
"app_version": "3.1.0",
"cfg_scale": 8.0,
"clip_skip": 0,
"controlnets": [],
"generation_mode": "sdxl_txt2img",
"height": 1024,
"loras": [],
"model": {
"base_model": "sdxl",
"model_name": "stable-diffusion-xl-base-1.0",
"model_type": "main"
},
"negative_prompt": "",
"negative_style_prompt": "",
"positive_prompt": "military grade sushi dinner for shock troopers",
"positive_style_prompt": "",
"rand_device": "cpu",
"refiner_cfg_scale": 7.5,
"refiner_model": {
"base_model": "sdxl-refiner",
"model_name": "sd_xl_refiner_1.0",
"model_type": "main"
},
"refiner_negative_aesthetic_score": 2.5,
"refiner_positive_aesthetic_score": 6.0,
"refiner_scheduler": "euler",
"refiner_start": 0.8,
"refiner_steps": 20,
"scheduler": "euler",
"seed": 387129902,
"steps": 25,
"width": 1024
}
```
You may list multiple files on the command line.
## **invokeai-import-images**
InvokeAI uses a database to store information about images it
generated, and just copying the image files from one InvokeAI root
directory to another does not automatically import those images into
the destination's gallery. This script allows you to bulk import
images generated by one instance of InvokeAI into a gallery maintained
by another. It also works on images generated by older versions of
InvokeAI, going way back to version 1.
This script has an interactive mode only. The following example shows
it in action:
```bash
$ invokeai-import-images
===============================================================================
This script will import images generated by earlier versions of
InvokeAI into the currently installed root directory:
/home/XXXX/invokeai-main
If this is not what you want to do, type ctrl-C now to cancel.
===============================================================================
= Configuration & Settings
Found invokeai.yaml file at /home/XXXX/invokeai-main/invokeai.yaml:
Database : /home/XXXX/invokeai-main/databases/invokeai.db
Outputs : /home/XXXX/invokeai-main/outputs/images
Use these paths for import (yes) or choose different ones (no) [Yn]:
Inputs: Specify absolute path containing InvokeAI .png images to import: /home/XXXX/invokeai-2.3/outputs/images/
Include files from subfolders recursively [yN]?
Options for board selection for imported images:
1) Select an existing board name. (found 4)
2) Specify a board name to create/add to.
3) Create/add to board named 'IMPORT'.
4) Create/add to board named 'IMPORT' with the current datetime string appended (.e.g IMPORT_20230919T203519Z).
5) Create/add to board named 'IMPORT' with a the original file app_version appended (.e.g IMPORT_2.2.5).
Specify desired board option: 3
===============================================================================
= Import Settings Confirmation
Database File Path : /home/XXXX/invokeai-main/databases/invokeai.db
Outputs/Images Directory : /home/XXXX/invokeai-main/outputs/images
Import Image Source Directory : /home/XXXX/invokeai-2.3/outputs/images/
Recurse Source SubDirectories : No
Count of .png file(s) found : 5785
Board name option specified : IMPORT
Database backup will be taken at : /home/XXXX/invokeai-main/databases/backup
Notes about the import process:
- Source image files will not be modified, only copied to the outputs directory.
- If the same file name already exists in the destination, the file will be skipped.
- If the same file name already has a record in the database, the file will be skipped.
- Invoke AI metadata tags will be updated/written into the imported copy only.
- On the imported copy, only Invoke AI known tags (latest and legacy) will be retained (dream, sd-metadata, invokeai, invokeai_metadata)
- A property 'imported_app_version' will be added to metadata that can be viewed in the UI's metadata viewer.
- The new 3.x InvokeAI outputs folder structure is flat so recursively found source imges will all be placed into the single outputs/images folder.
Do you wish to continue with the import [Yn] ?
Making DB Backup at /home/lstein/invokeai-main/databases/backup/backup-20230919T203519Z-invokeai.db...Done!
===============================================================================
Importing /home/XXXX/invokeai-2.3/outputs/images/17d09907-297d-4db3-a18a-60b337feac66.png
... (5785 more lines) ...
===============================================================================
= Import Complete - Elpased Time: 0.28 second(s)
Source File(s) : 5785
Total Imported : 5783
Skipped b/c file already exists on disk : 1
Skipped b/c file already exists in db : 0
Errors during import : 1
```
## **invokeai-db-maintenance**
This script helps maintain the integrity of your InvokeAI database by
finding and fixing three problems that can arise over time:
1. An image was manually deleted from the outputs directory, leaving a
dangling image record in the InvokeAI database. This will cause a
black image to appear in the gallery. This is an "orphaned database
image record." The script can fix this by running a "clean"
operation on the database, removing the orphaned entries.
2. An image is present in the outputs directory but there is no
corresponding entry in the database. This can happen when the image
is added manually to the outputs directory, or if a crash occurred
after the image was generated but before the database was
completely updated. The symptom is that the image is present in the
outputs folder but doesn't appear in the InvokeAI gallery. This is
called an "orphaned image file." The script can fix this problem by
running an "archive" operation in which orphaned files are moved
into a directory named `outputs/images-archive`. If you wish, you
can then run `invokeai-image-import` to reimport these images back
into the database.
3. The thumbnail for an image is missing, again causing a black
gallery thumbnail. This is fixed by running the "thumbnaiils"
operation, which simply regenerates and re-registers the missing
thumbnail.
You can find and fix all three of these problems in a single go by
executing this command:
```bash
invokeai-db-maintenance --operation all
```
Or you can run just the clean and thumbnail operations like this:
```bash
invokeai-db-maintenance -operation clean, thumbnail
```
If called without any arguments, the script will ask you which
operations you wish to perform.
## **invokeai-migrate3**
This script will migrate settings and models (but not images!) from an
InvokeAI v2.3 root folder to an InvokeAI 3.X folder. Call it with the
source and destination root folders like this:
```bash
invokeai-migrate3 --from ~/invokeai-2.3 --to invokeai-3.1.1
```
Both directories must previously have been properly created and
initialized by `invokeai-configure`. If you wish to migrate the images
contained in the older root as well, you can use the
`invokeai-image-migrate` script described earlier.
---
Copyright (c) 2023, Lincoln Stein and the InvokeAI Development Team

View File

@ -126,6 +126,6 @@ amounts of image-to-image variation even when the seed is fixed and the
`-v` argument is very low. Others are more deterministic. Feel free to `-v` argument is very low. Others are more deterministic. Feel free to
experiment until you find the combination that you like. experiment until you find the combination that you like.
Also be aware of the [Perlin Noise](../features/OTHER.md#thresholding-and-perlin-noise-initialization-options) Also be aware of the [Perlin Noise](OTHER.md#thresholding-and-perlin-noise-initialization-options)
feature, which provides another way of introducing variability into your feature, which provides another way of introducing variability into your
image generation requests. image generation requests.

View File

@ -28,7 +28,7 @@ Learn how to install and use ControlNet models for fine control over
image output. image output.
### * [Image-to-Image Guide](IMG2IMG.md) ### * [Image-to-Image Guide](IMG2IMG.md)
Use a seed image to build new creations. Use a seed image to build new creations in the CLI.
## Model Management ## Model Management
@ -51,9 +51,6 @@ 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.
### * [Command-line Utilities](UTILITIES.md)
A list of the command-line utilities available with InvokeAI.
<!-- OUT OF DATE <!-- 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,

View File

@ -57,9 +57,7 @@ Prompts provide the models directions on what to generate. As a general rule of
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!) 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 https://models.invoke.ai 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!
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* - *Models that contain “inpainting” in the name are designed for use with the inpainting feature of the Unified Canvas*

View File

@ -143,10 +143,10 @@ Mac and Linux machines, and runs on GPU cards with as little as 4 GB of RAM.
<!-- seperator --> <!-- seperator -->
### Prompt Engineering ### Prompt Engineering
- [Prompt Syntax](features/PROMPTS.md) - [Prompt Syntax](features/PROMPTS.md)
- [Generating Variations](features/VARIATIONS.md)
### InvokeAI Configuration ### InvokeAI Configuration
- [Guide to InvokeAI Runtime Settings](features/CONFIGURATION.md) - [Guide to InvokeAI Runtime Settings](features/CONFIGURATION.md)
- [Database Maintenance and other Command Line Utilities](features/UTILITIES.md)
## :octicons-log-16: Important Changes Since Version 2.3 ## :octicons-log-16: Important Changes Since Version 2.3
@ -165,8 +165,10 @@ still a work in progress, but coming soon.
### Command-Line Interface Retired ### Command-Line Interface Retired
All "invokeai" command-line interfaces have been retired as of version The original "invokeai" command-line interface has been retired. The
3.4. `invokeai` command will now launch a new command-line client that can
be used by developers to create and test nodes. It is not intended to
be used for routine image generation or manipulation.
To launch the Web GUI from the command-line, use the command To launch the Web GUI from the command-line, use the command
`invokeai-web` rather than the traditional `invokeai --web`. `invokeai-web` rather than the traditional `invokeai --web`.
@ -198,7 +200,6 @@ The list of schedulers has been completely revamped and brought up to date:
| **dpmpp_2m** | DPMSolverMultistepScheduler | original noise scnedule | | **dpmpp_2m** | DPMSolverMultistepScheduler | original noise scnedule |
| **dpmpp_2m_k** | DPMSolverMultistepScheduler | using karras noise schedule | | **dpmpp_2m_k** | DPMSolverMultistepScheduler | using karras noise schedule |
| **unipc** | UniPCMultistepScheduler | CPU only | | **unipc** | UniPCMultistepScheduler | CPU only |
| **lcm** | LCMScheduler | |
Please see [3.0.0 Release Notes](https://github.com/invoke-ai/InvokeAI/releases/tag/v3.0.0) for further details. Please see [3.0.0 Release Notes](https://github.com/invoke-ai/InvokeAI/releases/tag/v3.0.0) for further details.

View File

@ -40,7 +40,7 @@ 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.10.*` or `3.11.*` you meet number. If it is version `3.9.*`, `3.10.*` or `3.11.*` you meet
requirements. requirements.
!!! warning "What to do if you have an unsupported version" !!! warning "What to do if you have an unsupported version"
@ -48,7 +48,7 @@ experimental versions later.
Go to [Python Downloads](https://www.python.org/downloads/) Go to [Python Downloads](https://www.python.org/downloads/)
and download the appropriate installer package for your and download the appropriate installer package for your
platform. We recommend [Version platform. We recommend [Version
3.10.12](https://www.python.org/downloads/release/python-3109/), 3.10.9](https://www.python.org/downloads/release/python-3109/),
which has been extensively tested with InvokeAI. which has been extensively tested with InvokeAI.
_Please select your platform in the section below for platform-specific _Please select your platform in the section below for platform-specific
@ -179,7 +179,7 @@ experimental versions later.
you will have the choice of CUDA (NVidia cards), ROCm (AMD cards), you will have the choice of CUDA (NVidia cards), ROCm (AMD cards),
or CPU (no graphics acceleration). On Windows, you'll have the or CPU (no graphics acceleration). On Windows, you'll have the
choice of CUDA vs CPU, and on Macs you'll be offered CPU only. When choice of CUDA vs CPU, and on Macs you'll be offered CPU only. When
you select CPU on M1/M2/M3 Macintoshes, you will get MPS-based you select CPU on M1 or M2 Macintoshes, you will get MPS-based
graphics acceleration without installing additional drivers. If you graphics acceleration without installing additional drivers. If you
are unsure what GPU you are using, you can ask the installer to are unsure what GPU you are using, you can ask the installer to
guess. guess.
@ -471,7 +471,7 @@ Then type the following commands:
=== "NVIDIA System" === "NVIDIA System"
```bash ```bash
pip install torch torchvision --force-reinstall --extra-index-url https://download.pytorch.org/whl/cu121 pip install torch torchvision --force-reinstall --extra-index-url https://download.pytorch.org/whl/cu118
pip install xformers pip install xformers
``` ```

View File

@ -32,7 +32,7 @@ gaming):
* **Python** * **Python**
version 3.10 through 3.11 version 3.9 through 3.11
* **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.10 through 3.11. The rest of the install 1. Please make sure you are using Python 3.9 through 3.11. 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
@ -148,7 +148,7 @@ manager, please follow these steps:
=== "CUDA (NVidia)" === "CUDA (NVidia)"
```bash ```bash
pip install "InvokeAI[xformers]" --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu121 pip install "InvokeAI[xformers]" --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu118
``` ```
=== "ROCm (AMD)" === "ROCm (AMD)"
@ -256,10 +256,6 @@ manager, please follow these steps:
*highly recommended** if your virtual environment is located outside of *highly recommended** if your virtual environment is located outside of
your runtime directory. your runtime directory.
!!! tip
On linux, it is recommended to run invokeai with the following env var: `MALLOC_MMAP_THRESHOLD_=1048576`. For example: `MALLOC_MMAP_THRESHOLD_=1048576 invokeai --web`. This helps to prevent memory fragmentation that can lead to memory accumulation over time. This env var is set automatically when running via `invoke.sh`.
10. Render away! 10. Render away!
Browse the [features](../features/index.md) section to learn about all the Browse the [features](../features/index.md) section to learn about all the
@ -300,18 +296,8 @@ code for InvokeAI. For this to work, you will need to install the
on your system, please see the [Git Installation on your system, please see the [Git Installation
Guide](https://github.com/git-guides/install-git) Guide](https://github.com/git-guides/install-git)
You will also need to install the [frontend development toolchain](https://github.com/invoke-ai/InvokeAI/blob/main/docs/contributing/contribution_guides/contributingToFrontend.md).
If you have a "normal" installation, you should create a totally separate virtual environment for the git-based installation, else the two may interfere.
> **Why do I need the frontend toolchain**?
>
> The InvokeAI project uses trunk-based development. That means our `main` branch is the development branch, and releases are tags on that branch. Because development is very active, we don't keep an updated build of the UI in `main` - we only build it for production releases.
>
> That means that between releases, to have a functioning application when running directly from the repo, you will need to run the UI in dev mode or build it regularly (any time the UI code changes).
1. Create a fork of the InvokeAI repository through the GitHub UI or [this link](https://github.com/invoke-ai/InvokeAI/fork) 1. Create a fork of the InvokeAI repository through the GitHub UI or [this link](https://github.com/invoke-ai/InvokeAI/fork)
2. From the command line, run this command: 1. From the command line, run this command:
```bash ```bash
git clone https://github.com/<your_github_username>/InvokeAI.git git clone https://github.com/<your_github_username>/InvokeAI.git
``` ```
@ -319,15 +305,15 @@ If you have a "normal" installation, you should create a totally separate virtua
This will create a directory named `InvokeAI` and populate it with the This will create a directory named `InvokeAI` and populate it with the
full source code from your fork of the InvokeAI repository. full source code from your fork of the InvokeAI repository.
3. Activate the InvokeAI virtual environment as per step (4) of the manual 2. Activate the InvokeAI virtual environment as per step (4) of the manual
installation protocol (important!) installation protocol (important!)
4. Enter the InvokeAI repository directory and run one of these 3. Enter the InvokeAI repository directory and run one of these
commands, based on your GPU: commands, based on your GPU:
=== "CUDA (NVidia)" === "CUDA (NVidia)"
```bash ```bash
pip install -e .[xformers] --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu121 pip install -e .[xformers] --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu118
``` ```
=== "ROCm (AMD)" === "ROCm (AMD)"
@ -348,15 +334,11 @@ installation protocol (important!)
Be sure to pass `-e` (for an editable install) and don't forget the Be sure to pass `-e` (for an editable install) and don't forget the
dot ("."). It is part of the command. dot ("."). It is part of the command.
5. Install the [frontend toolchain](https://github.com/invoke-ai/InvokeAI/blob/main/docs/contributing/contribution_guides/contributingToFrontend.md) and do a production build of the UI as described. You can now run `invokeai` and its related commands. The code will be
6. You can now run `invokeai` and its related commands. The code will be
read from the repository, so that you can edit the .py source files read from the repository, so that you can edit the .py source files
and watch the code's behavior change. and watch the code's behavior change.
When you pull in new changes to the repo, be sure to re-build the UI. 4. If you wish to contribute to the InvokeAI project, you are
7. If you wish to contribute to the InvokeAI project, you are
encouraged to establish a GitHub account and "fork" encouraged to establish a GitHub account and "fork"
https://github.com/invoke-ai/InvokeAI into your own copy of the https://github.com/invoke-ai/InvokeAI into your own copy of the
repository. You can then use GitHub functions to create and submit repository. You can then use GitHub functions to create and submit
@ -375,7 +357,7 @@ you can do so using this unsupported recipe:
mkdir ~/invokeai mkdir ~/invokeai
conda create -n invokeai python=3.10 conda create -n invokeai python=3.10
conda activate invokeai conda activate invokeai
pip install InvokeAI[xformers] --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu121 pip install InvokeAI[xformers] --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu118
invokeai-configure --root ~/invokeai invokeai-configure --root ~/invokeai
invokeai --root ~/invokeai --web invokeai --root ~/invokeai --web
``` ```

View File

@ -85,7 +85,7 @@ You can find which version you should download from [this link](https://docs.nvi
When installing torch and torchvision manually with `pip`, remember to provide When installing torch and torchvision manually with `pip`, remember to provide
the argument `--extra-index-url the argument `--extra-index-url
https://download.pytorch.org/whl/cu121` as described in the [Manual https://download.pytorch.org/whl/cu118` as described in the [Manual
Installation Guide](020_INSTALL_MANUAL.md). Installation Guide](020_INSTALL_MANUAL.md).
## :simple-amd: ROCm ## :simple-amd: ROCm

View File

@ -4,49 +4,38 @@ title: Installing with Docker
# :fontawesome-brands-docker: Docker # :fontawesome-brands-docker: Docker
!!! warning "macOS and AMD GPU Users" !!! warning "For most users"
We highly recommend to Install InvokeAI locally using [these instructions](INSTALLATION.md), We highly recommend to Install InvokeAI locally using [these instructions](INSTALLATION.md)
because Docker containers can not access the GPU on macOS.
!!! warning "AMD GPU Users" !!! tip "For developers"
Container support for AMD GPUs has been reported to work by the community, but has not received For container-related development tasks or for enabling easy
extensive testing. Please make sure to set the `GPU_DRIVER=rocm` environment variable (see below), and deployment to other environments (on-premises or cloud), follow these
use the `build.sh` script to build the image for this to take effect at build time. instructions.
!!! tip "Linux and Windows Users" For general use, install locally to leverage your machine's GPU.
For optimal performance, configure your Docker daemon to access your machine's GPU.
Docker Desktop on Windows [includes GPU support](https://www.docker.com/blog/wsl-2-gpu-support-for-docker-desktop-on-nvidia-gpus/).
Linux users should install and configure the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)
## Why containers? ## Why containers?
They provide a flexible, reliable way to build and deploy InvokeAI. They provide a flexible, reliable way to build and deploy InvokeAI. You'll also
See [Processes](https://12factor.net/processes) under the Twelve-Factor App use a Docker volume to store the largest model files and image outputs as a
methodology for details on why running applications in such a stateless fashion is important. first step in decoupling storage and compute. Future enhancements can do this
for other assets. See [Processes](https://12factor.net/processes) under the
Twelve-Factor App methodology for details on why running applications in such a
stateless fashion is important.
The container is configured for CUDA by default, but can be built to support AMD GPUs You can specify the target platform when building the image and running the
by setting the `GPU_DRIVER=rocm` environment variable at Docker image build time. container. You'll also need to specify the InvokeAI requirements file that
matches the container's OS and the architecture it will run on.
Developers on Apple silicon (M1/M2/M3): You Developers on Apple silicon (M1/M2): You
[can't access your GPU cores from Docker containers](https://github.com/pytorch/pytorch/issues/81224) [can't access your GPU cores from Docker containers](https://github.com/pytorch/pytorch/issues/81224)
and performance is reduced compared with running it directly on macOS but for and performance is reduced compared with running it directly on macOS but for
development purposes it's fine. Once you're done with development tasks on your development purposes it's fine. Once you're done with development tasks on your
laptop you can build for the target platform and architecture and deploy to laptop you can build for the target platform and architecture and deploy to
another environment with NVIDIA GPUs on-premises or in the cloud. another environment with NVIDIA GPUs on-premises or in the cloud.
## TL;DR
This assumes properly configured Docker on Linux or Windows/WSL2. Read on for detailed customization options.
```bash
# docker compose commands should be run from the `docker` directory
cd docker
docker compose up
```
## Installation in a Linux container (desktop) ## Installation in a Linux container (desktop)
### Prerequisites ### Prerequisites
@ -69,44 +58,222 @@ a token and copy it, since you will need in for the next step.
### Setup ### Setup
Set up your environmnent variables. In the `docker` directory, make a copy of `env.sample` and name it `.env`. Make changes as necessary. Set the fork you want to use and other variables.
Any environment variables supported by InvokeAI can be set here - please see the [CONFIGURATION](../features/CONFIGURATION.md) for further detail. !!! tip
At a minimum, you might want to set the `INVOKEAI_ROOT` environment variable I preffer to save my env vars
to point to the location where you wish to store your InvokeAI models, configuration, and outputs. in the repository root in a `.env` (or `.envrc`) file to automatically re-apply
them when I come back.
The build- and run- scripts contain default values for almost everything,
besides the [Hugging Face Token](https://huggingface.co/settings/tokens) you
created in the last step.
Some Suggestions of variables you may want to change besides the Token:
<figure markdown> <figure markdown>
| Environment-Variable <img width="220" align="right"/> | Default value <img width="360" align="right"/> | Description | | Environment-Variable <img width="220" align="right"/> | Default value <img width="360" align="right"/> | Description |
| ----------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ----------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INVOKEAI_ROOT` | `~/invokeai` | **Required** - the location of your InvokeAI root directory. It will be created if it does not exist. | `HUGGING_FACE_HUB_TOKEN` | No default, but **required**! | This is the only **required** variable, without it you can't download the huggingface models |
| `HUGGING_FACE_HUB_TOKEN` | | InvokeAI will work without it, but some of the integrations with HuggingFace (like downloading from models from private repositories) may not work| | `REPOSITORY_NAME` | The Basename of the Repo folder | This name will used as the container repository/image name |
| `GPU_DRIVER` | `cuda` | Optionally change this to `rocm` to build the image for AMD GPUs. NOTE: Use the `build.sh` script to build the image for this to take effect. | `VOLUMENAME` | `${REPOSITORY_NAME,,}_data` | Name of the Docker Volume where model files will be stored |
| `ARCH` | arch of the build machine | Can be changed if you want to build the image for another arch |
| `CONTAINER_REGISTRY` | ghcr.io | Name of the Container Registry to use for the full tag |
| `CONTAINER_REPOSITORY` | `$(whoami)/${REPOSITORY_NAME}` | Name of the Container Repository |
| `CONTAINER_FLAVOR` | `cuda` | The flavor of the image to built, available options are `cuda`, `rocm` and `cpu`. If you choose `rocm` or `cpu`, the extra-index-url will be selected automatically, unless you set one yourself. |
| `CONTAINER_TAG` | `${INVOKEAI_BRANCH##*/}-${CONTAINER_FLAVOR}` | The Container Repository / Tag which will be used |
| `INVOKE_DOCKERFILE` | `Dockerfile` | The Dockerfile which should be built, handy for development |
| `PIP_EXTRA_INDEX_URL` | | If you want to use a custom pip-extra-index-url |
</figure> </figure>
#### Build the Image #### Build the Image
Use the standard `docker compose build` command from within the `docker` directory. I provided a build script, which is located next to the Dockerfile in
`docker/build.sh`. It can be executed from repository root like this:
If using an AMD GPU: ```bash
a: set the `GPU_DRIVER=rocm` environment variable in `docker-compose.yml` and continue using `docker compose build` as usual, or ./docker/build.sh
b: set `GPU_DRIVER=rocm` in the `.env` file and use the `build.sh` script, provided for convenience ```
The build Script not only builds the container, but also creates the docker
volume if not existing yet.
#### Run the Container #### Run the Container
Use the standard `docker compose up` command, and generally the `docker compose` [CLI](https://docs.docker.com/compose/reference/) as usual. After the build process is done, you can run the container via the provided
`docker/run.sh` script
Once the container starts up (and configures the InvokeAI root directory if this is a new installation), you can access InvokeAI at [http://localhost:9090](http://localhost:9090) ```bash
./docker/run.sh
```
## Troubleshooting / FAQ When used without arguments, the container will start the webserver and provide
you the link to open it. But if you want to use some other parameters you can
also do so.
- Q: I am running on Windows under WSL2, and am seeing a "no such file or directory" error. !!! example "run script example"
- A: Your `docker-entrypoint.sh` file likely has Windows (CRLF) as opposed to Unix (LF) line endings,
and you may have cloned this repository before the issue was fixed. To solve this, please change ```bash
the line endings in the `docker-entrypoint.sh` file to `LF`. You can do this in VSCode ./docker/run.sh "banana sushi" -Ak_lms -S42 -s10
(`Ctrl+P` and search for "line endings"), or by using the `dos2unix` utility in WSL. ```
Finally, you may delete `docker-entrypoint.sh` followed by `git pull; git checkout docker/docker-entrypoint.sh`
to reset the file to its most recent version. This would generate the legendary "banana sushi" with Seed 42, k_lms Sampler and 10 steps.
For more information on this issue, please see the [Docker Desktop documentation](https://docs.docker.com/desktop/troubleshoot/topics/#avoid-unexpected-syntax-errors-use-unix-style-line-endings-for-files-in-containers)
Find out more about available CLI-Parameters at [features/CLI.md](../../features/CLI/#arguments)
---
## Running the container on your GPU
If you have an Nvidia GPU, you can enable InvokeAI to run on the GPU by running
the container with an extra environment variable to enable GPU usage and have
the process run much faster:
```bash
GPU_FLAGS=all ./docker/run.sh
```
This passes the `--gpus all` to docker and uses the GPU.
If you don't have a GPU (or your host is not yet setup to use it) you will see a
message like this:
`docker: Error response from daemon: could not select device driver "" with capabilities: [[gpu]].`
You can use the full set of GPU combinations documented here:
https://docs.docker.com/config/containers/resource_constraints/#gpu
For example, use `GPU_FLAGS=device=GPU-3a23c669-1f69-c64e-cf85-44e9b07e7a2a` to
choose a specific device identified by a UUID.
---
!!! warning "Deprecated"
From here on you will find the the previous Docker-Docs, which will still
provide some usefull informations.
## Usage (time to have fun)
### Startup
If you're on a **Linux container** the `invoke` script is **automatically
started** and the output dir set to the Docker volume you created earlier.
If you're **directly on macOS follow these startup instructions**. With the
Conda environment activated (`conda activate ldm`), run the interactive
interface that combines the functionality of the original scripts `txt2img` and
`img2img`: Use the more accurate but VRAM-intensive full precision math because
half-precision requires autocast and won't work. By default the images are saved
in `outputs/img-samples/`.
```Shell
python3 scripts/invoke.py --full_precision
```
You'll get the script's prompt. You can see available options or quit.
```Shell
invoke> -h
invoke> q
```
### Text to Image
For quick (but bad) image results test with 5 steps (default 50) and 1 sample
image. This will let you know that everything is set up correctly. Then increase
steps to 100 or more for good (but slower) results. The prompt can be in quotes
or not.
```Shell
invoke> The hulk fighting with sheldon cooper -s5 -n1
invoke> "woman closeup highly detailed" -s 150
# Reuse previous seed and apply face restoration
invoke> "woman closeup highly detailed" --steps 150 --seed -1 -G 0.75
```
You'll need to experiment to see if face restoration is making it better or
worse for your specific prompt.
If you're on a container the output is set to the Docker volume. You can copy it
wherever you want. You can download it from the Docker Desktop app, Volumes,
my-vol, data. Or you can copy it from your Mac terminal. Keep in mind
`docker cp` can't expand `*.png` so you'll need to specify the image file name.
On your host Mac (you can use the name of any container that mounted the
volume):
```Shell
docker cp dummy:/data/000001.928403745.png /Users/<your-user>/Pictures
```
### Image to Image
You can also do text-guided image-to-image translation. For example, turning a
sketch into a detailed drawing.
`strength` is a value between 0.0 and 1.0 that controls the amount of noise that
is added to the input image. Values that approach 1.0 allow for lots of
variations but will also produce images that are not semantically consistent
with the input. 0.0 preserves image exactly, 1.0 replaces it completely.
Make sure your input image size dimensions are multiples of 64 e.g. 512x512.
Otherwise you'll get `Error: product of dimension sizes > 2**31'`. If you still
get the error
[try a different size](https://support.apple.com/guide/preview/resize-rotate-or-flip-an-image-prvw2015/mac#:~:text=image's%20file%20size-,In%20the%20Preview%20app%20on%20your%20Mac%2C%20open%20the%20file,is%20shown%20at%20the%20bottom.)
like 512x256.
If you're on a Docker container, copy your input image into the Docker volume
```Shell
docker cp /Users/<your-user>/Pictures/sketch-mountains-input.jpg dummy:/data/
```
Try it out generating an image (or more). The `invoke` script needs absolute
paths to find the image so don't use `~`.
If you're on your Mac
```Shell
invoke> "A fantasy landscape, trending on artstation" -I /Users/<your-user>/Pictures/sketch-mountains-input.jpg --strength 0.75 --steps 100 -n4
```
If you're on a Linux container on your Mac
```Shell
invoke> "A fantasy landscape, trending on artstation" -I /data/sketch-mountains-input.jpg --strength 0.75 --steps 50 -n1
```
### Web Interface
You can use the `invoke` script with a graphical web interface. Start the web
server with:
```Shell
python3 scripts/invoke.py --full_precision --web
```
If it's running on your Mac point your Mac web browser to
<http://127.0.0.1:9090>
Press Control-C at the command line to stop the web server.
### Notes
Some text you can add at the end of the prompt to make it very pretty:
```Shell
cinematic photo, highly detailed, cinematic lighting, ultra-detailed, ultrarealistic, photorealism, Octane Rendering, cyberpunk lights, Hyper Detail, 8K, HD, Unreal Engine, V-Ray, full hd, cyberpunk, abstract, 3d octane render + 4k UHD + immense detail + dramatic lighting + well lit + black, purple, blue, pink, cerulean, teal, metallic colours, + fine details, ultra photoreal, photographic, concept art, cinematic composition, rule of thirds, mysterious, eerie, photorealism, breathtaking detailed, painting art deco pattern, by hsiao, ron cheng, john james audubon, bizarre compositions, exquisite detail, extremely moody lighting, painted by greg rutkowski makoto shinkai takashi takeuchi studio ghibli, akihiko yoshida
```
The original scripts should work as well.
```Shell
python3 scripts/orig_scripts/txt2img.py --help
python3 scripts/orig_scripts/txt2img.py --ddim_steps 100 --n_iter 1 --n_samples 1 --plms --prompt "new born baby kitten. Hyper Detail, Octane Rendering, Unreal Engine, V-Ray"
python3 scripts/orig_scripts/txt2img.py --ddim_steps 5 --n_iter 1 --n_samples 1 --plms --prompt "ocean" # or --klms
```

View File

@ -84,7 +84,7 @@ InvokeAI root directory's `autoimport` folder.
### Installation via `invokeai-model-install` ### Installation via `invokeai-model-install`
From the `invoke` launcher, choose option [4] "Download and install From the `invoke` launcher, choose option [5] "Download and install
models." This will launch the same script that prompted you to select 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 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 skipped the first time around. It is all right to specify a model that
@ -171,16 +171,3 @@ subfolders and organize them as you wish.
The location of the autoimport directories are controlled by settings The location of the autoimport directories are controlled by settings
in `invokeai.yaml`. See [Configuration](../features/CONFIGURATION.md). in `invokeai.yaml`. See [Configuration](../features/CONFIGURATION.md).
### Installing models that live in HuggingFace subfolders
On rare occasions you may need to install a diffusers-style model that
lives in a subfolder of a HuggingFace repo id. In this event, simply
add ":_subfolder-name_" to the end of the repo id. For example, if the
repo id is "monster-labs/control_v1p_sd15_qrcode_monster" and the model
you wish to fetch lives in a subfolder named "v2", then the repo id to
pass to the various model installers should be
```
monster-labs/control_v1p_sd15_qrcode_monster:v2
```

View File

@ -59,7 +59,8 @@ Prior to installing PyPatchMatch, you need to take the following steps:
`from patchmatch import patch_match`: It should look like the following: `from patchmatch import patch_match`: It should look like the following:
```py ```py
Python 3.10.12 (main, Jun 11 2023, 05:26:28) [GCC 11.4.0] on linux Python 3.9.5 (default, Nov 23 2021, 15:27:38)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information. Type "help", "copyright", "credits" or "license" for more information.
>>> from patchmatch import patch_match >>> from patchmatch import patch_match
Compiling and loading c extensions from "/home/lstein/Projects/InvokeAI/.invokeai-env/src/pypatchmatch/patchmatch". Compiling and loading c extensions from "/home/lstein/Projects/InvokeAI/.invokeai-env/src/pypatchmatch/patchmatch".

View File

@ -28,7 +28,7 @@ command line, then just be sure to activate it's virtual environment.
Then run the following three commands: Then run the following three commands:
```sh ```sh
pip install xformers~=0.0.22 pip install xformers~=0.0.19
pip install triton # WON'T WORK ON WINDOWS pip install triton # WON'T WORK ON WINDOWS
python -m xformers.info output python -m xformers.info output
``` ```
@ -42,7 +42,7 @@ If all goes well, you'll see a report like the
following: following:
```sh ```sh
xFormers 0.0.22 xFormers 0.0.20
memory_efficient_attention.cutlassF: available memory_efficient_attention.cutlassF: available
memory_efficient_attention.cutlassB: available memory_efficient_attention.cutlassB: available
memory_efficient_attention.flshattF: available memory_efficient_attention.flshattF: available
@ -59,14 +59,14 @@ swiglu.gemm_fused_operand_sum: available
swiglu.fused.p.cpp: available swiglu.fused.p.cpp: available
is_triton_available: True is_triton_available: True
is_functorch_available: False is_functorch_available: False
pytorch.version: 2.1.0+cu121 pytorch.version: 2.0.1+cu118
pytorch.cuda: available pytorch.cuda: available
gpu.compute_capability: 8.9 gpu.compute_capability: 8.9
gpu.name: NVIDIA GeForce RTX 4070 gpu.name: NVIDIA GeForce RTX 4070
build.info: available build.info: available
build.cuda_version: 1108 build.cuda_version: 1108
build.python_version: 3.10.11 build.python_version: 3.10.11
build.torch_version: 2.1.0+cu121 build.torch_version: 2.0.1+cu118
build.env.TORCH_CUDA_ARCH_LIST: 5.0+PTX 6.0 6.1 7.0 7.5 8.0 8.6 build.env.TORCH_CUDA_ARCH_LIST: 5.0+PTX 6.0 6.1 7.0 7.5 8.0 8.6
build.env.XFORMERS_BUILD_TYPE: Release build.env.XFORMERS_BUILD_TYPE: Release
build.env.XFORMERS_ENABLE_DEBUG_ASSERTIONS: None build.env.XFORMERS_ENABLE_DEBUG_ASSERTIONS: None
@ -92,22 +92,33 @@ installed from source. These instructions were written for a system
running Ubuntu 22.04, but other Linux distributions should be able to running Ubuntu 22.04, but other Linux distributions should be able to
adapt this recipe. adapt this recipe.
#### 1. Install CUDA Toolkit 12.1 #### 1. Install CUDA Toolkit 11.8
You will need the CUDA developer's toolkit in order to compile and You will need the CUDA developer's toolkit in order to compile and
install xFormers. **Do not try to install Ubuntu's nvidia-cuda-toolkit install xFormers. **Do not try to install Ubuntu's nvidia-cuda-toolkit
package.** It is out of date and will cause conflicts among the NVIDIA package.** It is out of date and will cause conflicts among the NVIDIA
driver and binaries. Instead install the CUDA Toolkit package provided driver and binaries. Instead install the CUDA Toolkit package provided
by NVIDIA itself. Go to [CUDA Toolkit 12.1 by NVIDIA itself. Go to [CUDA Toolkit 11.8
Downloads](https://developer.nvidia.com/cuda-12-1-0-download-archive) Downloads](https://developer.nvidia.com/cuda-11-8-0-download-archive)
and use the target selection wizard to choose your platform and Linux and use the target selection wizard to choose your platform and Linux
distribution. Select an installer type of "runfile (local)" at the distribution. Select an installer type of "runfile (local)" at the
last step. last step.
This will provide you with a recipe for downloading and running a This will provide you with a recipe for downloading and running a
install shell script that will install the toolkit and drivers. install shell script that will install the toolkit and drivers. For
example, the install script recipe for Ubuntu 22.04 running on a
x86_64 system is:
#### 2. Confirm/Install pyTorch 2.1.0 with CUDA 12.1 support ```
wget https://developer.download.nvidia.com/compute/cuda/11.8.0/local_installers/cuda_11.8.0_520.61.05_linux.run
sudo sh cuda_11.8.0_520.61.05_linux.run
```
Rather than cut-and-paste this example, We recommend that you walk
through the toolkit wizard in order to get the most up to date
installer for your system.
#### 2. Confirm/Install pyTorch 2.01 with CUDA 11.8 support
If you are using InvokeAI 3.0.2 or higher, these will already be If you are using InvokeAI 3.0.2 or higher, these will already be
installed. If not, you can check whether you have the needed libraries installed. If not, you can check whether you have the needed libraries
@ -122,7 +133,7 @@ Then run the command:
python -c 'exec("import torch\nprint(torch.__version__)")' python -c 'exec("import torch\nprint(torch.__version__)")'
``` ```
If it prints __2.1.0+cu121__ you're good. If not, you can install the If it prints __1.13.1+cu118__ you're good. If not, you can install the
most up to date libraries with this command: most up to date libraries with this command:
```sh ```sh

View File

@ -79,7 +79,7 @@ title: Manual Installation, Linux
and obtaining an access token for downloading. It will then download and and obtaining an access token for downloading. It will then download and
install the weights files for you. install the weights files for you.
Please look [here](../020_INSTALL_MANUAL.md) for a manual process for doing Please look [here](../INSTALL_MANUAL.md) for a manual process for doing
the same thing. the same thing.
7. Start generating images! 7. Start generating images!
@ -112,7 +112,7 @@ title: Manual Installation, Linux
To use an alternative model you may invoke the `!switch` command in To use an alternative model you may invoke the `!switch` command in
the CLI, or pass `--model <model_name>` during `invoke.py` launch for the CLI, or pass `--model <model_name>` during `invoke.py` launch for
either the CLI or the Web UI. See [Command Line either the CLI or the Web UI. See [Command Line
Client](../../deprecated/CLI.md#model-selection-and-importation). The Client](../../features/CLI.md#model-selection-and-importation). The
model names are defined in `configs/models.yaml`. model names are defined in `configs/models.yaml`.
8. Subsequently, to relaunch the script, be sure to run "conda activate 8. Subsequently, to relaunch the script, be sure to run "conda activate

View File

@ -150,7 +150,7 @@ will do our best to help.
To use an alternative model you may invoke the `!switch` command in To use an alternative model you may invoke the `!switch` command in
the CLI, or pass `--model <model_name>` during `invoke.py` launch for the CLI, or pass `--model <model_name>` during `invoke.py` launch for
either the CLI or the Web UI. See [Command Line either the CLI or the Web UI. See [Command Line
Client](../../deprecated/CLI.md#model-selection-and-importation). The Client](../../features/CLI.md#model-selection-and-importation). The
model names are defined in `configs/models.yaml`. model names are defined in `configs/models.yaml`.
--- ---

View File

@ -128,7 +128,7 @@ python scripts/invoke.py --web --max_load_models=3 \
``` ```
These options are described in detail in the These options are described in detail in the
[Command-Line Interface](../../deprecated/CLI.md) documentation. [Command-Line Interface](../../features/CLI.md) documentation.
## Troubleshooting ## Troubleshooting

View File

@ -75,7 +75,7 @@ Note that you will need NVIDIA drivers, Python 3.10, and Git installed beforehan
obtaining an access token for downloading. It will then download and install the obtaining an access token for downloading. It will then download and install the
weights files for you. weights files for you.
Please look [here](../020_INSTALL_MANUAL.md) for a manual process for doing the Please look [here](../INSTALL_MANUAL.md) for a manual process for doing the
same thing. same thing.
8. Start generating images! 8. Start generating images!
@ -108,7 +108,7 @@ Note that you will need NVIDIA drivers, Python 3.10, and Git installed beforehan
To use an alternative model you may invoke the `!switch` command in To use an alternative model you may invoke the `!switch` command in
the CLI, or pass `--model <model_name>` during `invoke.py` launch for the CLI, or pass `--model <model_name>` during `invoke.py` launch for
either the CLI or the Web UI. See [Command Line either the CLI or the Web UI. See [Command Line
Client](../../deprecated/CLI.md#model-selection-and-importation). The Client](../../features/CLI.md#model-selection-and-importation). The
model names are defined in `configs/models.yaml`. model names are defined in `configs/models.yaml`.
9. Subsequently, to relaunch the script, first activate the Anaconda 9. Subsequently, to relaunch the script, first activate the Anaconda

View File

@ -4,12 +4,12 @@ The workflow editor is a blank canvas allowing for the use of individual functio
If you're not familiar with Diffusion, take a look at our [Diffusion Overview.](../help/diffusion.md) Understanding how diffusion works will enable you to more easily use the Workflow Editor and build workflows to suit your needs. If you're not familiar with Diffusion, take a look at our [Diffusion Overview.](../help/diffusion.md) Understanding how diffusion works will enable you to more easily use the Workflow Editor and build workflows to suit your needs.
## Features ## UI Features
### Linear View ### Linear View
The Workflow Editor allows you to create a UI for your workflow, to make it easier to iterate on your generations. The Workflow Editor allows you to create a UI for your workflow, to make it easier to iterate on your generations.
To add an input to the Linear UI, right click on the input label and select "Add to Linear View". To add an input to the Linear UI, right click on the input and select "Add to Linear View".
The Linear UI View will also be part of the saved workflow, allowing you share workflows and enable other to use them, regardless of complexity. The Linear UI View will also be part of the saved workflow, allowing you share workflows and enable other to use them, regardless of complexity.
@ -25,10 +25,6 @@ Any node or input field can be renamed in the workflow editor. If the input fiel
* Backspace/Delete to delete a node * Backspace/Delete to delete a node
* Shift+Click to drag and select multiple nodes * Shift+Click to drag and select multiple nodes
### Node Caching
Nodes have a "Use Cache" option in their footer. This allows for performance improvements by using the previously cached values during the workflow processing.
## Important Concepts ## Important Concepts

View File

@ -4,58 +4,30 @@ These are nodes that have been developed by the community, for the community. If
If you'd like to submit a node for the community, please refer to the [node creation overview](contributingNodes.md). If you'd like to submit a node for the community, please refer to the [node creation overview](contributingNodes.md).
To use a node, add the node to the `nodes` folder found in your InvokeAI install location. 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. If you used the automated installation, this can be found inside the `.venv` folder. Along with the node, an example node graph should be provided to help you get started with the node.
The suggested method is to use `git clone` to clone the repository the node is found in. This allows for easy updates of the node in the future.
If you'd prefer, you can also just download the `.py` file from the linked repository and add it to the `nodes` folder.
To use a community workflow, download the the `.json` node graph file and load it into Invoke AI via the **Load Workflow** button in the Workflow Editor. To use a community workflow, download the the `.json` node graph file and load it into Invoke AI via the **Load Workflow** button in the Workflow Editor.
- Community Nodes ## Community Nodes
+ [Average Images](#average-images)
+ [Depth Map from Wavefront OBJ](#depth-map-from-wavefront-obj)
+ [Film Grain](#film-grain)
+ [Generative Grammar-Based Prompt Nodes](#generative-grammar-based-prompt-nodes)
+ [GPT2RandomPromptMaker](#gpt2randompromptmaker)
+ [Grid to Gif](#grid-to-gif)
+ [Halftone](#halftone)
+ [Ideal Size](#ideal-size)
+ [Image and Mask Composition Pack](#image-and-mask-composition-pack)
+ [Image to Character Art Image Nodes](#image-to-character-art-image-nodes)
+ [Image Picker](#image-picker)
+ [Load Video Frame](#load-video-frame)
+ [Make 3D](#make-3d)
+ [Oobabooga](#oobabooga)
+ [Prompt Tools](#prompt-tools)
+ [Retroize](#retroize)
+ [Size Stepper Nodes](#size-stepper-nodes)
+ [Text font to Image](#text-font-to-image)
+ [Thresholding](#thresholding)
+ [XY Image to Grid and Images to Grids nodes](#xy-image-to-grid-and-images-to-grids-nodes)
- [Example Node Template](#example-node-template)
- [Disclaimer](#disclaimer)
- [Help](#help)
### 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)
-------------------------------- --------------------------------
### Average Images ### Ideal Size
**Description:** This node takes in a collection of images of the same size and averages them as output. It converts everything to RGB mode first. **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/average-images-node **Node Link:** https://github.com/JPPhoto/ideal-size-node
--------------------------------
### Depth Map from Wavefront OBJ
**Description:** Render depth maps from Wavefront .obj files (triangulated) using this simple 3D renderer utilizing numpy and matplotlib to compute and color the scene. There are simple parameters to change the FOV, camera position, and model orientation.
To be imported, an .obj must use triangulated meshes, so make sure to enable that option if exporting from a 3D modeling program. This renderer makes each triangle a solid color based on its average depth, so it will cause anomalies if your .obj has large triangles. In Blender, the Remesh modifier can be helpful to subdivide a mesh into small pieces that work well given these limitations.
**Node Link:** https://github.com/dwringer/depth-from-obj-node
**Example Usage:**
</br><img src="https://raw.githubusercontent.com/dwringer/depth-from-obj-node/main/depth_from_obj_usage.jpg" width="500" />
-------------------------------- --------------------------------
### Film Grain ### Film Grain
@ -65,19 +37,22 @@ To be imported, an .obj must use triangulated meshes, so make sure to enable tha
**Node Link:** https://github.com/JPPhoto/film-grain-node **Node Link:** https://github.com/JPPhoto/film-grain-node
-------------------------------- --------------------------------
### Generative Grammar-Based Prompt Nodes ### Image Picker
**Description:** This set of 3 nodes generates prompts from simple user-defined grammar rules (loaded from custom files - examples provided below). The prompts are made by recursively expanding a special template string, replacing nonterminal "parts-of-speech" until no nonterminal terms remain in the string. **Description:** This InvokeAI node takes in a collection of images and randomly chooses one. This can be useful when you have a number of poses to choose from for a ControlNet node, or a number of input images for another purpose.
This includes 3 Nodes: **Node Link:** https://github.com/JPPhoto/image-picker-node
- *Lookup Table from File* - loads a YAML file "prompt" section (or of a whole folder of YAML's) into a JSON-ified dictionary (Lookups output)
- *Lookups Entry from Prompt* - places a single entry in a new Lookups output under the specified heading
- *Prompt from Lookup Table* - uses a Collection of Lookups as grammar rules from which to randomly generate prompts.
**Node Link:** https://github.com/dwringer/generative-grammar-prompt-nodes --------------------------------
### Retroize
**Example Usage:** **Description:** Retroize is a collection of nodes for InvokeAI to "Retroize" images. Any image can be given a fresh coat of retro paint with these nodes, either from your gallery or from within the graph itself. It includes nodes to pixelize, quantize, palettize, and ditherize images; as well as to retrieve palettes from existing images.
</br><img src="https://raw.githubusercontent.com/dwringer/generative-grammar-prompt-nodes/main/lookuptables_usage.jpg" width="500" />
**Node Link:** https://github.com/Ar7ific1al/invokeai-retroizeinode/
**Retroize Output Examples**
![image](https://github.com/Ar7ific1al/InvokeAI_nodes_retroize/assets/2306586/de8b4fa6-324c-4c2d-b36c-297600c73974)
-------------------------------- --------------------------------
### GPT2RandomPromptMaker ### GPT2RandomPromptMaker
@ -90,129 +65,31 @@ This includes 3 Nodes:
Generated Prompt: An enchanted weapon will be usable by any character regardless of their alignment. Generated Prompt: An enchanted weapon will be usable by any character regardless of their alignment.
<img src="https://github.com/mickr777/InvokeAI/assets/115216705/8496ba09-bcdd-4ff7-8076-ff213b6a1e4c" width="200" /> ![9acf5aef-7254-40dd-95b3-8eac431dfab0 (1)](https://github.com/mickr777/InvokeAI/assets/115216705/8496ba09-bcdd-4ff7-8076-ff213b6a1e4c)
--------------------------------
### Grid to Gif
**Description:** One node that turns a grid image into an image collection, one node that turns an image collection into a gif.
**Node Link:** https://github.com/mildmisery/invokeai-GridToGifNode/blob/main/GridToGif.py
**Example Node Graph:** https://github.com/mildmisery/invokeai-GridToGifNode/blob/main/Grid%20to%20Gif%20Example%20Workflow.json
**Output Examples**
<img src="https://raw.githubusercontent.com/mildmisery/invokeai-GridToGifNode/main/input.png" width="300" />
<img src="https://raw.githubusercontent.com/mildmisery/invokeai-GridToGifNode/main/output.gif" width="300" />
--------------------------------
### Halftone
**Description**: Halftone converts the source image to grayscale and then performs halftoning. CMYK Halftone converts the image to CMYK and applies a per-channel halftoning to make the source image look like a magazine or newspaper. For both nodes, you can specify angles and halftone dot spacing.
**Node Link:** https://github.com/JPPhoto/halftone-node
**Example**
Input:
<img src="https://github.com/invoke-ai/InvokeAI/assets/34005131/fd5efb9f-4355-4409-a1c2-c1ca99e0cab4" width="300" />
Halftone Output:
<img src="https://github.com/invoke-ai/InvokeAI/assets/34005131/7e606f29-e68f-4d46-b3d5-97f799a4ec2f" width="300" />
CMYK Halftone Output:
<img src="https://github.com/invoke-ai/InvokeAI/assets/34005131/c59c578f-db8e-4d66-8c66-2851752d75ea" width="300" />
--------------------------------
### Ideal Size
**Description:** This node calculates an ideal image size for a first pass of a multi-pass upscaling. The aim is to avoid duplication that results from choosing a size larger than the model is capable of.
**Node Link:** https://github.com/JPPhoto/ideal-size-node
--------------------------------
### Image and Mask Composition Pack
**Description:** This is a pack of nodes for composing masks and images, including a simple text mask creator and both image and latent offset nodes. The offsets wrap around, so these can be used in conjunction with the Seamless node to progressively generate centered on different parts of the seamless tiling.
This includes 15 Nodes:
- *Adjust Image Hue Plus* - Rotate the hue of an image in one of several different color spaces.
- *Blend Latents/Noise (Masked)* - Use a mask to blend part of one latents tensor [including Noise outputs] into another. Can be used to "renoise" sections during a multi-stage [masked] denoising process.
- *Enhance Image* - Boost or reduce color saturation, contrast, brightness, sharpness, or invert colors of any image at any stage with this simple wrapper for pillow [PIL]'s ImageEnhance module.
- *Equivalent Achromatic Lightness* - Calculates image lightness accounting for Helmholtz-Kohlrausch effect based on a method described by High, Green, and Nussbaum (2023).
- *Text to Mask (Clipseg)* - Input a prompt and an image to generate a mask representing areas of the image matched by the prompt.
- *Text to Mask Advanced (Clipseg)* - Output up to four prompt masks combined with logical "and", logical "or", or as separate channels of an RGBA image.
- *Image Layer Blend* - Perform a layered blend of two images using alpha compositing. Opacity of top layer is selectable, with optional mask and several different blend modes/color spaces.
- *Image Compositor* - Take a subject from an image with a flat backdrop and layer it on another image using a chroma key or flood select background removal.
- *Image Dilate or Erode* - Dilate or expand a mask (or any image!). This is equivalent to an expand/contract operation.
- *Image Value Thresholds* - Clip an image to pure black/white beyond specified thresholds.
- *Offset Latents* - Offset a latents tensor in the vertical and/or horizontal dimensions, wrapping it around.
- *Offset Image* - Offset an image in the vertical and/or horizontal dimensions, wrapping it around.
- *Rotate/Flip Image* - Rotate an image in degrees clockwise/counterclockwise about its center, optionally resizing the image boundaries to fit, or flipping it about the vertical and/or horizontal axes.
- *Shadows/Highlights/Midtones* - Extract three masks (with adjustable hard or soft thresholds) representing shadows, midtones, and highlights regions of an image.
- *Text Mask (simple 2D)* - create and position a white on black (or black on white) line of text using any font locally available to Invoke.
**Node Link:** https://github.com/dwringer/composition-nodes
</br><img src="https://raw.githubusercontent.com/dwringer/composition-nodes/main/composition_pack_overview.jpg" width="500" />
--------------------------------
### Image to Character Art Image Nodes
**Description:** Group of nodes to convert an input image into ascii/unicode art Image
**Node Link:** https://github.com/mickr777/imagetoasciiimage
**Output Examples**
<img src="https://user-images.githubusercontent.com/115216705/271817646-8e061fcc-9a2c-4fa9-bcc7-c0f7b01e9056.png" width="300" /><img src="https://github.com/mickr777/imagetoasciiimage/assets/115216705/3c4990eb-2f42-46b9-90f9-0088b939dc6a" width="300" /></br>
<img src="https://github.com/mickr777/imagetoasciiimage/assets/115216705/fee7f800-a4a8-41e2-a66b-c66e4343307e" width="300" />
<img src="https://github.com/mickr777/imagetoasciiimage/assets/115216705/1d9c1003-a45f-45c2-aac7-46470bb89330" width="300" />
--------------------------------
### Image Picker
**Description:** This InvokeAI node takes in a collection of images and randomly chooses one. This can be useful when you have a number of poses to choose from for a ControlNet node, or a number of input images for another purpose.
**Node Link:** https://github.com/JPPhoto/image-picker-node
-------------------------------- --------------------------------
### Load Video Frame ### Load Video Frame
**Description:** This is a video frame image provider + indexer/video creation nodes for hooking up to iterators and ranges and ControlNets and such for invokeAI node experimentation. Think animation + ControlNet outputs. **Description:** This is a video frame image provider + indexer/video creation nodes for hooking up to iterators and ranges and ControlNets and such for invokeAI node experimentation. Think animation + ControlNet outputs.
**Node Link:** https://github.com/helix4u/load_video_frame **Node Link:** https://github.com/helix4u/load_video_frame
**Example Node Graph:** https://github.com/helix4u/load_video_frame/blob/main/Example_Workflow.json
**Output Example:** **Output Example:**
<img src="https://raw.githubusercontent.com/helix4u/load_video_frame/main/_git_assets/testmp4_embed_converted.gif" width="500" /> =======
![Example animation](https://github.com/helix4u/load_video_frame/blob/main/testmp4_embed_converted.gif)
[Full mp4 of Example Output test.mp4](https://github.com/helix4u/load_video_frame/blob/main/test.mp4)
-------------------------------- --------------------------------
### Make 3D
**Description:** Create compelling 3D stereo images from 2D originals.
**Node Link:** [https://gitlab.com/srcrr/shift3d/-/raw/main/make3d.py](https://gitlab.com/srcrr/shift3d)
**Example Node Graph:** https://gitlab.com/srcrr/shift3d/-/raw/main/example-workflow.json?ref_type=heads&inline=false
**Output Examples**
<img src="https://gitlab.com/srcrr/shift3d/-/raw/main/example-1.png" width="300" />
<img src="https://gitlab.com/srcrr/shift3d/-/raw/main/example-2.png" width="300" />
--------------------------------
### Oobabooga ### Oobabooga
**Description:** asks a local LLM running in Oobabooga's Text-Generation-Webui to write a prompt based on the user input. **Description:** asks a local LLM running in Oobabooga's Text-Generation-Webui to write a prompt based on the user input.
**Link:** https://github.com/sammyf/oobabooga-node **Link:** https://github.com/sammyf/oobabooga-node
**Example:** **Example:**
"describe a new mystical creature in its natural environment" "describe a new mystical creature in its natural environment"
@ -222,7 +99,7 @@ This includes 15 Nodes:
"The mystical creature I am describing to you is called the "Glimmerwing". It is a majestic, iridescent being that inhabits the depths of the most enchanted forests and glimmering lakes. Its body is covered in shimmering scales that reflect every color of the rainbow, and it has delicate, translucent wings that sparkle like diamonds in the sunlight. The Glimmerwing's home is a crystal-clear lake, surrounded by towering trees with leaves that shimmer like jewels. In this serene environment, the Glimmerwing spends its days swimming gracefully through the water, chasing schools of glittering fish and playing with the gentle ripples of the lake's surface. "The mystical creature I am describing to you is called the "Glimmerwing". It is a majestic, iridescent being that inhabits the depths of the most enchanted forests and glimmering lakes. Its body is covered in shimmering scales that reflect every color of the rainbow, and it has delicate, translucent wings that sparkle like diamonds in the sunlight. The Glimmerwing's home is a crystal-clear lake, surrounded by towering trees with leaves that shimmer like jewels. In this serene environment, the Glimmerwing spends its days swimming gracefully through the water, chasing schools of glittering fish and playing with the gentle ripples of the lake's surface.
As the sun sets, the Glimmerwing perches on a branch of one of the trees, spreading its wings to catch the last rays of light. The creature's scales glow softly, casting a rainbow of colors across the forest floor. The Glimmerwing sings a haunting melody, its voice echoing through the stillness of the night air. Its song is said to have the power to heal the sick and bring peace to troubled souls. Those who are lucky enough to hear the Glimmerwing's song are forever changed by its beauty and grace." As the sun sets, the Glimmerwing perches on a branch of one of the trees, spreading its wings to catch the last rays of light. The creature's scales glow softly, casting a rainbow of colors across the forest floor. The Glimmerwing sings a haunting melody, its voice echoing through the stillness of the night air. Its song is said to have the power to heal the sick and bring peace to troubled souls. Those who are lucky enough to hear the Glimmerwing's song are forever changed by its beauty and grace."
<img src="https://github.com/sammyf/oobabooga-node/assets/42468608/cecdd820-93dd-4c35-abbf-607e001fb2ed" width="300" /> ![glimmerwing_small](https://github.com/sammyf/oobabooga-node/assets/42468608/cecdd820-93dd-4c35-abbf-607e001fb2ed)
**Requirement** **Requirement**
@ -230,37 +107,62 @@ a Text-Generation-Webui instance (might work remotely too, but I never tried it)
**Note** **Note**
This node works best with SDXL models, especially as the style can be described independently of the LLM's output. This node works best with SDXL models, especially as the style can be described independantly of the LLM's output.
-------------------------------- --------------------------------
### Prompt Tools ### Depth Map from Wavefront OBJ
**Description:** A set of InvokeAI nodes that add general prompt manipulation tools. These were written to accompany the PromptsFromFile node and other prompt generation nodes. **Description:** Render depth maps from Wavefront .obj files (triangulated) using this simple 3D renderer utilizing numpy and matplotlib to compute and color the scene. There are simple parameters to change the FOV, camera position, and model orientation.
1. PromptJoin - Joins to prompts into one. To be imported, an .obj must use triangulated meshes, so make sure to enable that option if exporting from a 3D modeling program. This renderer makes each triangle a solid color based on its average depth, so it will cause anomalies if your .obj has large triangles. In Blender, the Remesh modifier can be helpful to subdivide a mesh into small pieces that work well given these limitations.
2. PromptReplace - performs a search and replace on a prompt. With the option of using regex.
3. PromptSplitNeg - splits a prompt into positive and negative using the old V2 method of [] for negative.
4. PromptToFile - saves a prompt or collection of prompts to a file. one per line. There is an append/overwrite option.
5. PTFieldsCollect - Converts image generation fields into a Json format string that can be passed to Prompt to file.
6. PTFieldsExpand - Takes Json string and converts it to individual generation parameters This can be fed from the Prompts to file node.
7. PromptJoinThree - Joins 3 prompt together.
8. PromptStrength - This take a string and float and outputs another string in the format of (string)strength like the weighted format of compel.
9. PromptStrengthCombine - This takes a collection of prompt strength strings and outputs a string in the .and() or .blend() format that can be fed into a proper prompt node.
See full docs here: https://github.com/skunkworxdark/Prompt-tools-nodes/edit/main/README.md **Node Link:** https://github.com/dwringer/depth-from-obj-node
**Node Link:** https://github.com/skunkworxdark/Prompt-tools-nodes **Example Usage:**
![depth from obj usage graph](https://raw.githubusercontent.com/dwringer/depth-from-obj-node/main/depth_from_obj_usage.jpg)
-------------------------------- --------------------------------
### Retroize ### Enhance Image (simple adjustments)
**Description:** Retroize is a collection of nodes for InvokeAI to "Retroize" images. Any image can be given a fresh coat of retro paint with these nodes, either from your gallery or from within the graph itself. It includes nodes to pixelize, quantize, palettize, and ditherize images; as well as to retrieve palettes from existing images. **Description:** Boost or reduce color saturation, contrast, brightness, sharpness, or invert colors of any image at any stage with this simple wrapper for pillow [PIL]'s ImageEnhance module.
**Node Link:** https://github.com/Ar7ific1al/invokeai-retroizeinode/ Color inversion is toggled with a simple switch, while each of the four enhancer modes are activated by entering a value other than 1 in each corresponding input field. Values less than 1 will reduce the corresponding property, while values greater than 1 will enhance it.
**Retroize Output Examples** **Node Link:** https://github.com/dwringer/image-enhance-node
<img src="https://github.com/Ar7ific1al/InvokeAI_nodes_retroize/assets/2306586/de8b4fa6-324c-4c2d-b36c-297600c73974" width="500" /> **Example Usage:**
![enhance image usage graph](https://raw.githubusercontent.com/dwringer/image-enhance-node/main/image_enhance_usage.jpg)
--------------------------------
### Generative Grammar-Based Prompt Nodes
**Description:** This set of 3 nodes generates prompts from simple user-defined grammar rules (loaded from custom files - examples provided below). The prompts are made by recursively expanding a special template string, replacing nonterminal "parts-of-speech" until no more nonterminal terms remain in the string.
This includes 3 Nodes:
- *Lookup Table from File* - loads a YAML file "prompt" section (or of a whole folder of YAML's) into a JSON-ified dictionary (Lookups output)
- *Lookups Entry from Prompt* - places a single entry in a new Lookups output under the specified heading
- *Prompt from Lookup Table* - uses a Collection of Lookups as grammar rules from which to randomly generate prompts.
**Node Link:** https://github.com/dwringer/generative-grammar-prompt-nodes
**Example Usage:**
![lookups usage example graph](https://raw.githubusercontent.com/dwringer/generative-grammar-prompt-nodes/main/lookuptables_usage.jpg)
--------------------------------
### Image and Mask Composition Pack
**Description:** This is a pack of nodes for composing masks and images, including a simple text mask creator and both image and latent offset nodes. The offsets wrap around, so these can be used in conjunction with the Seamless node to progressively generate centered on different parts of the seamless tiling.
This includes 4 Nodes:
- *Text Mask (simple 2D)* - create and position a white on black (or black on white) line of text using any font locally available to Invoke.
- *Image Compositor* - Take a subject from an image with a flat backdrop and layer it on another image using a chroma key or flood select background removal.
- *Offset Latents* - Offset a latents tensor in the vertical and/or horizontal dimensions, wrapping it around.
- *Offset Image* - Offset an image in the vertical and/or horizontal dimensions, wrapping it around.
**Node Link:** https://github.com/dwringer/composition-nodes
**Example Usage:**
![composition nodes usage graph](https://raw.githubusercontent.com/dwringer/composition-nodes/main/composition_nodes_usage.jpg)
-------------------------------- --------------------------------
### Size Stepper Nodes ### Size Stepper Nodes
@ -272,9 +174,10 @@ A third node is included, *Random Switch (Integers)*, which is just a generic ve
**Node Link:** https://github.com/dwringer/size-stepper-nodes **Node Link:** https://github.com/dwringer/size-stepper-nodes
**Example Usage:** **Example Usage:**
</br><img src="https://raw.githubusercontent.com/dwringer/size-stepper-nodes/main/size_nodes_usage.jpg" width="500" /> ![size stepper usage graph](https://raw.githubusercontent.com/dwringer/size-stepper-nodes/main/size_nodes_usage.jpg)
-------------------------------- --------------------------------
### Text font to Image ### Text font to Image
**Description:** text font to text image node for InvokeAI, download a font to use (or if in font cache uses it from there), the text is always resized to the image size, but can control that with padding, optional 2nd line **Description:** text font to text image node for InvokeAI, download a font to use (or if in font cache uses it from there), the text is always resized to the image size, but can control that with padding, optional 2nd line
@ -283,63 +186,27 @@ A third node is included, *Random Switch (Integers)*, which is just a generic ve
**Output Examples** **Output Examples**
<img src="https://github.com/mickr777/InvokeAI/assets/115216705/c21b0af3-d9c6-4c16-9152-846a23effd36" width="300" /> ![a3609d48-d9b7-41f0-b280-063d857986fb](https://github.com/mickr777/InvokeAI/assets/115216705/c21b0af3-d9c6-4c16-9152-846a23effd36)
Results after using the depth controlnet Results after using the depth controlnet
<img src="https://github.com/mickr777/InvokeAI/assets/115216705/915f1a53-968e-43eb-aa61-07cd8f1a733a" width="300" /> ![9133eabb-bcda-4326-831e-1b641228b178](https://github.com/mickr777/InvokeAI/assets/115216705/915f1a53-968e-43eb-aa61-07cd8f1a733a)
<img src="https://github.com/mickr777/InvokeAI/assets/115216705/821ef89e-8a60-44f5-b94e-471a9d8690cc" width="300" /> ![4f9a3fa8-9be9-4236-8a3e-fcec66decd2a](https://github.com/mickr777/InvokeAI/assets/115216705/821ef89e-8a60-44f5-b94e-471a9d8690cc)
<img src="https://github.com/mickr777/InvokeAI/assets/115216705/2befcb6d-49f4-4bfd-b5fc-1fee19274f89" width="300" /> ![babd69c4-9d60-4a55-a834-5e8397f62610](https://github.com/mickr777/InvokeAI/assets/115216705/2befcb6d-49f4-4bfd-b5fc-1fee19274f89)
-------------------------------- --------------------------------
### Thresholding
**Description:** This node generates masks for highlights, midtones, and shadows given an input image. You can optionally specify a blur for the lookup table used in making those masks from the source image.
**Node Link:** https://github.com/JPPhoto/thresholding-node
**Examples**
Input:
<img src="https://github.com/invoke-ai/InvokeAI/assets/34005131/c88ada13-fb3d-484c-a4fe-947b44712632" width="300" />
Highlights/Midtones/Shadows:
<img src="https://github.com/invoke-ai/InvokeAI/assets/34005131/727021c1-36ff-4ec8-90c8-105e00de986d" width="300" />
<img src="https://github.com/invoke-ai/InvokeAI/assets/34005131/0b721bfc-f051-404e-b905-2f16b824ddfe" width="300" />
<img src="https://github.com/invoke-ai/InvokeAI/assets/34005131/04c1297f-1c88-42b6-a7df-dd090b976286" width="300" />
Highlights/Midtones/Shadows (with LUT blur enabled):
<img src="https://github.com/invoke-ai/InvokeAI/assets/34005131/19aa718a-70c1-4668-8169-d68f4bd13771" width="300" />
<img src="https://github.com/invoke-ai/InvokeAI/assets/34005131/0a440e43-697f-4d17-82ee-f287467df0a5" width="300" />
<img src="https://github.com/invoke-ai/InvokeAI/assets/34005131/0701fd0f-2ca7-4fe2-8613-2b52547bafce" width="300" />
--------------------------------
### XY Image to Grid and Images to Grids nodes
**Description:** Image to grid nodes and supporting tools.
1. "Images To Grids" node - Takes a collection of images and creates a grid(s) of images. If there are more images than the size of a single grid then multiple grids will be created until it runs out of images.
2. "XYImage To Grid" node - Converts a collection of XYImages into a labeled Grid of images. The XYImages collection has to be built using the supporting nodes. See example node setups for more details.
See full docs here: https://github.com/skunkworxdark/XYGrid_nodes/edit/main/README.md
**Node Link:** https://github.com/skunkworxdark/XYGrid_nodes
--------------------------------
### Example Node Template ### Example Node Template
**Description:** This node allows you to do super cool things with InvokeAI. **Description:** This node allows you to do super cool things with InvokeAI.
**Node Link:** https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/app/invocations/prompt.py **Node Link:** https://github.com/invoke-ai/InvokeAI/fake_node.py
**Example Workflow:** https://github.com/invoke-ai/InvokeAI/blob/docs/main/docs/workflows/Prompt_from_File.json **Example Node Graph:** https://github.com/invoke-ai/InvokeAI/fake_node_graph.json
**Output Examples** **Output Examples**
</br><img src="https://invoke-ai.github.io/InvokeAI/assets/invoke_ai_banner.png" width="500" /> ![Example Image](https://invoke-ai.github.io/InvokeAI/assets/invoke_ai_banner.png){: style="height:115px;width:240px"}
## Disclaimer ## Disclaimer

View File

@ -4,7 +4,7 @@ To learn about the specifics of creating a new node, please visit our [Node crea
Once youve created a node and confirmed that it behaves as expected locally, follow these steps: 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. Preferably, the node is in a repo with a README detailing the nodes usage & examples to help others more easily use your node. Including the tag "invokeai-node" in your repository's README can also help other users find it more easily. - Make sure the node is contained in a new Python (.py) file. Preferrably, the node is in a repo with a README detaling the nodes usage & examples to help others more easily use your node.
- Submit a pull request with a link to your node(s) repo in GitHub against the `main` branch to add the node to the [Community Nodes](communityNodes.md) list - Submit a pull request with a link to your node(s) repo in GitHub against the `main` branch to add the node to the [Community Nodes](communityNodes.md) list
- Make sure you are following the template below and have provided all relevant details about the node and what it does. Example output images and workflows are very helpful for other users looking to use your node. - Make sure you are following the template below and have provided all relevant details about the node and what it does. Example output images and workflows are very helpful for other users looking to use your node.
- A maintainer will review the pull request and node. If the node is aligned with the direction of the project, you may be asked for permission to include it in the core project. - A maintainer will review the pull request and node. If the node is aligned with the direction of the project, you may be asked for permission to include it in the core project.

View File

@ -1,6 +1,6 @@
# List of Default Nodes # List of Default Nodes
The table below contains a list of the default nodes shipped with InvokeAI and their descriptions. The table below contains a list of the default nodes shipped with InvokeAI and their descriptions.
| Node <img width=160 align="right"> | Function | | Node <img width=160 align="right"> | Function |
|: ---------------------------------- | :--------------------------------------------------------------------------------------| |: ---------------------------------- | :--------------------------------------------------------------------------------------|
@ -17,12 +17,11 @@ The table below contains a list of the default nodes shipped with InvokeAI and t
|Conditioning Primitive | A conditioning tensor primitive value| |Conditioning Primitive | A conditioning tensor primitive value|
|Content Shuffle Processor | Applies content shuffle processing to image| |Content Shuffle Processor | Applies content shuffle processing to image|
|ControlNet | Collects ControlNet info to pass to other nodes| |ControlNet | Collects ControlNet info to pass to other nodes|
|OpenCV Inpaint | Simple inpaint using opencv.|
|Denoise Latents | Denoises noisy latents to decodable images| |Denoise Latents | Denoises noisy latents to decodable images|
|Divide Integers | Divides two numbers| |Divide Integers | Divides two numbers|
|Dynamic Prompt | Parses a prompt using adieyal/dynamicprompts' random or combinatorial generator| |Dynamic Prompt | Parses a prompt using adieyal/dynamicprompts' random or combinatorial generator|
|[FaceMask](./detailedNodes/faceTools.md#facemask) | Generates masks for faces in an image to use with Inpainting| |Upscale (RealESRGAN) | Upscales an image using RealESRGAN.|
|[FaceIdentifier](./detailedNodes/faceTools.md#faceidentifier) | Identifies and labels faces in an image|
|[FaceOff](./detailedNodes/faceTools.md#faceoff) | Creates a new image that is a scaled bounding box with a mask on the face for Inpainting|
|Float Math | Perform basic math operations on two floats| |Float Math | Perform basic math operations on two floats|
|Float Primitive Collection | A collection of float primitive values| |Float Primitive Collection | A collection of float primitive values|
|Float Primitive | A float primitive value| |Float Primitive | A float primitive value|
@ -77,7 +76,6 @@ The table below contains a list of the default nodes shipped with InvokeAI and t
|ONNX Prompt (Raw) | A node to process inputs and produce outputs. May use dependency injection in __init__ to receive providers.| |ONNX Prompt (Raw) | A node to process inputs and produce outputs. May use dependency injection in __init__ to receive providers.|
|ONNX Text to Latents | Generates latents from conditionings.| |ONNX Text to Latents | Generates latents from conditionings.|
|ONNX Model Loader | Loads a main model, outputting its submodels.| |ONNX Model Loader | Loads a main model, outputting its submodels.|
|OpenCV Inpaint | Simple inpaint using opencv.|
|Openpose Processor | Applies Openpose processing to image| |Openpose Processor | Applies Openpose processing to image|
|PIDI Processor | Applies PIDI processing to image| |PIDI Processor | Applies PIDI processing to image|
|Prompts from File | Loads prompts from a text file| |Prompts from File | Loads prompts from a text file|
@ -99,6 +97,5 @@ The table below contains a list of the default nodes shipped with InvokeAI and t
|String Primitive | A string primitive value| |String Primitive | A string primitive value|
|Subtract Integers | Subtracts two numbers| |Subtract Integers | Subtracts two numbers|
|Tile Resample Processor | Tile resampler processor| |Tile Resample Processor | Tile resampler processor|
|Upscale (RealESRGAN) | Upscales an image using RealESRGAN.|
|VAE Loader | Loads a VAE model, outputting a VaeLoaderOutput| |VAE Loader | Loads a VAE model, outputting a VaeLoaderOutput|
|Zoe (Depth) Processor | Applies Zoe depth processing to image| |Zoe (Depth) Processor | Applies Zoe depth processing to image|

View File

@ -1,154 +0,0 @@
# Face Nodes
## FaceOff
FaceOff mimics a user finding a face in an image and resizing the bounding box
around the head in Canvas.
Enter a face ID (found with FaceIdentifier) to choose which face to mask.
Just as you would add more context inside the bounding box by making it larger
in Canvas, the node gives you a padding input (in pixels) which will
simultaneously add more context, and increase the resolution of the bounding box
so the face remains the same size inside it.
The "Minimum Confidence" input defaults to 0.5 (50%), and represents a pass/fail
threshold a detected face must reach for it to be processed. Lowering this value
may help if detection is failing. If the detected masks are imperfect and stray
too far outside/inside of faces, the node gives you X & Y offsets to shrink/grow
the masks by a multiplier.
FaceOff will output the face in a bounded image, taking the face off of the
original image for input into any node that accepts image inputs. The node also
outputs a face mask with the dimensions of the bounded image. The X & Y outputs
are for connecting to the X & Y inputs of the Paste Image node, which will place
the bounded image back on the original image using these coordinates.
###### Inputs/Outputs
| Input | Description |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Image | Image for face detection |
| Face ID | The face ID to process, numbered from 0. Multiple faces not supported. Find a face's ID with FaceIdentifier node. |
| Minimum Confidence | Minimum confidence for face detection (lower if detection is failing) |
| X Offset | X-axis offset of the mask |
| Y Offset | Y-axis offset of the mask |
| Padding | All-axis padding around the mask in pixels |
| Chunk | Chunk (or divide) the image into sections to greatly improve face detection success. Defaults to off, but will activate if no faces are detected normally. Activate to chunk by default. |
| Output | Description |
| ------------- | ------------------------------------------------ |
| Bounded Image | Original image bound, cropped, and resized |
| Width | The width of the bounded image in pixels |
| Height | The height of the bounded image in pixels |
| Mask | The output mask |
| X | The x coordinate of the bounding box's left side |
| Y | The y coordinate of the bounding box's top side |
## FaceMask
FaceMask mimics a user drawing masks on faces in an image in Canvas.
The "Face IDs" input allows the user to select specific faces to be masked.
Leave empty to detect and mask all faces, or a comma-separated list for a
specific combination of faces (ex: `1,2,4`). A single integer will detect and
mask that specific face. Find face IDs with the FaceIdentifier node.
The "Minimum Confidence" input defaults to 0.5 (50%), and represents a pass/fail
threshold a detected face must reach for it to be processed. Lowering this value
may help if detection is failing.
If the detected masks are imperfect and stray too far outside/inside of faces,
the node gives you X & Y offsets to shrink/grow the masks by a multiplier. All
masks shrink/grow together by the X & Y offset values.
By default, masks are created to change faces. When masks are inverted, they
change surrounding areas, protecting faces.
###### Inputs/Outputs
| Input | Description |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Image | Image for face detection |
| Face IDs | Comma-separated list of face ids to mask eg '0,2,7'. Numbered from 0. Leave empty to mask all. Find face IDs with FaceIdentifier node. |
| Minimum Confidence | Minimum confidence for face detection (lower if detection is failing) |
| X Offset | X-axis offset of the mask |
| Y Offset | Y-axis offset of the mask |
| Chunk | Chunk (or divide) the image into sections to greatly improve face detection success. Defaults to off, but will activate if no faces are detected normally. Activate to chunk by default. |
| Invert Mask | Toggle to invert the face mask |
| Output | Description |
| ------ | --------------------------------- |
| Image | The original image |
| Width | The width of the image in pixels |
| Height | The height of the image in pixels |
| Mask | The output face mask |
## FaceIdentifier
FaceIdentifier outputs an image with detected face IDs printed in white numbers
onto each face.
Face IDs can then be used in FaceMask and FaceOff to selectively mask all, a
specific combination, or single faces.
The FaceIdentifier output image is generated for user reference, and isn't meant
to be passed on to other image-processing nodes.
The "Minimum Confidence" input defaults to 0.5 (50%), and represents a pass/fail
threshold a detected face must reach for it to be processed. Lowering this value
may help if detection is failing. If an image is changed in the slightest, run
it through FaceIdentifier again to get updated FaceIDs.
###### Inputs/Outputs
| Input | Description |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Image | Image for face detection |
| Minimum Confidence | Minimum confidence for face detection (lower if detection is failing) |
| Chunk | Chunk (or divide) the image into sections to greatly improve face detection success. Defaults to off, but will activate if no faces are detected normally. Activate to chunk by default. |
| Output | Description |
| ------ | ------------------------------------------------------------------------------------------------ |
| Image | The original image with small face ID numbers printed in white onto each face for user reference |
| Width | The width of the original image in pixels |
| Height | The height of the original image in pixels |
## Tips
- If not all target faces are being detected, activate Chunk to bypass full
image face detection and greatly improve detection success.
- Final results will vary between full-image detection and chunking for faces
that are detectable by both due to the nature of the process. Try either to
your taste.
- Be sure Minimum Confidence is set the same when using FaceIdentifier with
FaceOff/FaceMask.
- For FaceOff, use the color correction node before faceplace to correct edges
being noticeable in the final image (see example screenshot).
- Non-inpainting models may struggle to paint/generate correctly around faces.
- If your face won't change the way you want it to no matter what you change,
consider that the change you're trying to make is too much at that resolution.
For example, if an image is only 512x768 total, the face might only be 128x128
or 256x256, much smaller than the 512x512 your SD1.5 model was probably
trained on. Try increasing the resolution of the image by upscaling or
resizing, add padding to increase the bounding box's resolution, or use an
image where the face takes up more pixels.
- If the resulting face seems out of place pasted back on the original image
(ie. too large, not proportional), add more padding on the FaceOff node to
give inpainting more context. Context and good prompting are important to
keeping things proportional.
- If you find the mask is too big/small and going too far outside/inside the
area you want to affect, adjust the x & y offsets to shrink/grow the mask area
- Use a higher denoise start value to resemble aspects of the original face or
surroundings. Denoise start = 0 & denoise end = 1 will make something new,
while denoise start = 0.50 & denoise end = 1 will be 50% old and 50% new.
- mediapipe isn't good at detecting faces with lots of face paint, hair covering
the face, etc. Anything that obstructs the face will likely result in no faces
being detected.
- If you find your face isn't being detected, try lowering the minimum
confidence value from 0.5. This could result in false positives, however
(random areas being detected as faces and masked).
- After altering an image and wanting to process a different face in the newly
altered image, run the altered image through FaceIdentifier again to see the
new Face IDs. MediaPipe will most likely detect faces in a different order
after an image has been changed in the slightest.

View File

@ -2,17 +2,12 @@
We've curated some example workflows for you to get started with Workflows in InvokeAI We've curated some example workflows for you to get started with Workflows in InvokeAI
To use them, right click on your desired workflow, follow the link to GitHub and click the "⬇" button to download the raw file. You can then use the "Load Workflow" functionality in InvokeAI to load the workflow and start generating images! To use them, right click on your desired workflow, press "Download Linked File". You can then use the "Load Workflow" functionality in InvokeAI to load the workflow and start generating images!
If you're interested in finding more workflows, checkout the [#share-your-workflows](https://discord.com/channels/1020123559063990373/1130291608097661000) channel in the InvokeAI Discord. If you're interested in finding more workflows, checkout the [#share-your-workflows](https://discord.com/channels/1020123559063990373/1130291608097661000) channel in the InvokeAI Discord.
* [SD1.5 / SD2 Text to Image](https://github.com/invoke-ai/InvokeAI/blob/main/docs/workflows/Text_to_Image.json) * [SD1.5 / SD2 Text to Image](https://github.com/invoke-ai/InvokeAI/blob/main/docs/workflows/Text_to_Image.json)
* [SDXL Text to Image](https://github.com/invoke-ai/InvokeAI/blob/docs/main/docs/workflows/SDXL_Text_to_Image.json) * [SDXL Text to Image](https://github.com/invoke-ai/InvokeAI/blob/main/docs/workflows/SDXL_Text_to_Image.json)
* [SDXL Text to Image with Refiner](https://github.com/invoke-ai/InvokeAI/blob/docs/main/docs/workflows/SDXL_w_Refiner_Text_to_Image.json) * [SDXL (with Refiner) Text to Image](https://github.com/invoke-ai/InvokeAI/blob/main/docs/workflows/SDXL_Text_to_Image.json)
* [Multi ControlNet (Canny & Depth)](https://github.com/invoke-ai/InvokeAI/blob/docs/main/docs/workflows/Multi_ControlNet_Canny_and_Depth.json) * [Tiled Upscaling with ControlNet](https://github.com/invoke-ai/InvokeAI/blob/main/docs/workflows/ESRGAN_img2img_upscale w_Canny_ControlNet.json)ß
* [Tiled Upscaling with ControlNet](https://github.com/invoke-ai/InvokeAI/blob/main/docs/workflows/ESRGAN_img2img_upscale_w_Canny_ControlNet.json)
* [Prompt From File](https://github.com/invoke-ai/InvokeAI/blob/docs/main/docs/workflows/Prompt_from_File.json)
* [Face Detailer with IP-Adapter & ControlNet](https://github.com/invoke-ai/InvokeAI/blob/docs/main/docs/workflows/Face_Detailer_with_IP-Adapter_and_Canny.json.json)
* [FaceMask](https://github.com/invoke-ai/InvokeAI/blob/main/docs/workflows/FaceMask.json)
* [FaceOff with 2x Face Scaling](https://github.com/invoke-ai/InvokeAI/blob/main/docs/workflows/FaceOff_FaceScale2x.json)
* [QR Code Monster](https://github.com/invoke-ai/InvokeAI/blob/docs/main/docs/workflows/QR_Code_Monster.json)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,985 +0,0 @@
{
"name": "Multi ControlNet (Canny & Depth)",
"author": "Millu",
"description": "A sample workflow using canny & depth ControlNets to guide the generation process. ",
"version": "0.1.0",
"contact": "millun@invoke.ai",
"tags": "ControlNet, canny, depth",
"notes": "",
"exposedFields": [
{
"nodeId": "54486974-835b-4d81-8f82-05f9f32ce9e9",
"fieldName": "model"
},
{
"nodeId": "7ce68934-3419-42d4-ac70-82cfc9397306",
"fieldName": "prompt"
},
{
"nodeId": "273e3f96-49ea-4dc5-9d5b-9660390f14e1",
"fieldName": "prompt"
},
{
"nodeId": "c4b23e64-7986-40c4-9cad-46327b12e204",
"fieldName": "image"
},
{
"nodeId": "8e860e51-5045-456e-bf04-9a62a2a5c49e",
"fieldName": "image"
}
],
"meta": {
"version": "1.0.0"
},
"nodes": [
{
"id": "8e860e51-5045-456e-bf04-9a62a2a5c49e",
"type": "invocation",
"data": {
"id": "8e860e51-5045-456e-bf04-9a62a2a5c49e",
"type": "image",
"inputs": {
"image": {
"id": "189c8adf-68cc-4774-a729-49da89f6fdf1",
"name": "image",
"type": "ImageField",
"fieldKind": "input",
"label": "Depth Input Image"
}
},
"outputs": {
"image": {
"id": "1a31cacd-9d19-4f32-b558-c5e4aa39ce73",
"name": "image",
"type": "ImageField",
"fieldKind": "output"
},
"width": {
"id": "12f298fd-1d11-4cca-9426-01240f7ec7cf",
"name": "width",
"type": "integer",
"fieldKind": "output"
},
"height": {
"id": "c47dabcb-44e8-40c9-992d-81dca59f598e",
"name": "height",
"type": "integer",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 225,
"position": {
"x": 3617.163483500202,
"y": 40.5529847930888
}
},
{
"id": "a33199c2-8340-401e-b8a2-42ffa875fc1c",
"type": "invocation",
"data": {
"id": "a33199c2-8340-401e-b8a2-42ffa875fc1c",
"type": "controlnet",
"inputs": {
"image": {
"id": "4e0a3172-d3c2-4005-a84c-fa12a404f8a0",
"name": "image",
"type": "ImageField",
"fieldKind": "input",
"label": ""
},
"control_model": {
"id": "8cb2d998-4086-430a-8b13-94cbc81e3ca3",
"name": "control_model",
"type": "ControlNetModelField",
"fieldKind": "input",
"label": "",
"value": {
"model_name": "sd-controlnet-depth",
"base_model": "sd-1"
}
},
"control_weight": {
"id": "5e32bd8a-9dc8-42d8-9bcc-c2b0460c0b0f",
"name": "control_weight",
"type": "FloatPolymorphic",
"fieldKind": "input",
"label": "",
"value": 1
},
"begin_step_percent": {
"id": "c258a276-352a-416c-8358-152f11005c0c",
"name": "begin_step_percent",
"type": "float",
"fieldKind": "input",
"label": "",
"value": 0
},
"end_step_percent": {
"id": "43001125-0d70-4f87-8e79-da6603ad6c33",
"name": "end_step_percent",
"type": "float",
"fieldKind": "input",
"label": "",
"value": 1
},
"control_mode": {
"id": "d2f14561-9443-4374-9270-e2f05007944e",
"name": "control_mode",
"type": "enum",
"fieldKind": "input",
"label": "",
"value": "balanced"
},
"resize_mode": {
"id": "727ee7d3-8bf6-4c7d-8b8a-43546b3b59cd",
"name": "resize_mode",
"type": "enum",
"fieldKind": "input",
"label": "",
"value": "just_resize"
}
},
"outputs": {
"control": {
"id": "b034aa0f-4d0d-46e4-b5e3-e25a9588d087",
"name": "control",
"type": "ControlField",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 508,
"position": {
"x": 4477.604342844504,
"y": -49.39005411272677
}
},
{
"id": "273e3f96-49ea-4dc5-9d5b-9660390f14e1",
"type": "invocation",
"data": {
"id": "273e3f96-49ea-4dc5-9d5b-9660390f14e1",
"type": "compel",
"inputs": {
"prompt": {
"id": "7c2c4771-2161-4d77-aced-ff8c4b3f1c15",
"name": "prompt",
"type": "string",
"fieldKind": "input",
"label": "Negative Prompt",
"value": ""
},
"clip": {
"id": "06d59e91-9cca-411d-bf05-86b099b3e8f7",
"name": "clip",
"type": "ClipField",
"fieldKind": "input",
"label": ""
}
},
"outputs": {
"conditioning": {
"id": "858bc33c-134c-4bf6-8855-f943e1d26f14",
"name": "conditioning",
"type": "ConditioningField",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 261,
"position": {
"x": 4444.706437017514,
"y": -924.0715320874991
}
},
{
"id": "54486974-835b-4d81-8f82-05f9f32ce9e9",
"type": "invocation",
"data": {
"id": "54486974-835b-4d81-8f82-05f9f32ce9e9",
"type": "main_model_loader",
"inputs": {
"model": {
"id": "f4a915a5-593e-4b6d-9198-c78eb5cefaed",
"name": "model",
"type": "MainModelField",
"fieldKind": "input",
"label": "",
"value": {
"model_name": "stable-diffusion-v1-5",
"base_model": "sd-1",
"model_type": "main"
}
}
},
"outputs": {
"unet": {
"id": "ee24fb16-da38-4c66-9fbc-e8f296ed40d2",
"name": "unet",
"type": "UNetField",
"fieldKind": "output"
},
"clip": {
"id": "f3fb0524-8803-41c1-86db-a61a13ee6a33",
"name": "clip",
"type": "ClipField",
"fieldKind": "output"
},
"vae": {
"id": "5c4878a8-b40f-44ab-b146-1c1f42c860b3",
"name": "vae",
"type": "VaeField",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 226,
"position": {
"x": 3837.096149678291,
"y": -1050.015351148365
}
},
{
"id": "7ce68934-3419-42d4-ac70-82cfc9397306",
"type": "invocation",
"data": {
"id": "7ce68934-3419-42d4-ac70-82cfc9397306",
"type": "compel",
"inputs": {
"prompt": {
"id": "7c2c4771-2161-4d77-aced-ff8c4b3f1c15",
"name": "prompt",
"type": "string",
"fieldKind": "input",
"label": "Positive Prompt",
"value": ""
},
"clip": {
"id": "06d59e91-9cca-411d-bf05-86b099b3e8f7",
"name": "clip",
"type": "ClipField",
"fieldKind": "input",
"label": ""
}
},
"outputs": {
"conditioning": {
"id": "858bc33c-134c-4bf6-8855-f943e1d26f14",
"name": "conditioning",
"type": "ConditioningField",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 261,
"position": {
"x": 4449.356038911986,
"y": -1201.659695420063
}
},
{
"id": "d204d184-f209-4fae-a0a1-d152800844e1",
"type": "invocation",
"data": {
"id": "d204d184-f209-4fae-a0a1-d152800844e1",
"type": "controlnet",
"inputs": {
"image": {
"id": "4e0a3172-d3c2-4005-a84c-fa12a404f8a0",
"name": "image",
"type": "ImageField",
"fieldKind": "input",
"label": ""
},
"control_model": {
"id": "8cb2d998-4086-430a-8b13-94cbc81e3ca3",
"name": "control_model",
"type": "ControlNetModelField",
"fieldKind": "input",
"label": "",
"value": {
"model_name": "sd-controlnet-canny",
"base_model": "sd-1"
}
},
"control_weight": {
"id": "5e32bd8a-9dc8-42d8-9bcc-c2b0460c0b0f",
"name": "control_weight",
"type": "FloatPolymorphic",
"fieldKind": "input",
"label": "",
"value": 1
},
"begin_step_percent": {
"id": "c258a276-352a-416c-8358-152f11005c0c",
"name": "begin_step_percent",
"type": "float",
"fieldKind": "input",
"label": "",
"value": 0
},
"end_step_percent": {
"id": "43001125-0d70-4f87-8e79-da6603ad6c33",
"name": "end_step_percent",
"type": "float",
"fieldKind": "input",
"label": "",
"value": 1
},
"control_mode": {
"id": "d2f14561-9443-4374-9270-e2f05007944e",
"name": "control_mode",
"type": "enum",
"fieldKind": "input",
"label": "",
"value": "balanced"
},
"resize_mode": {
"id": "727ee7d3-8bf6-4c7d-8b8a-43546b3b59cd",
"name": "resize_mode",
"type": "enum",
"fieldKind": "input",
"label": "",
"value": "just_resize"
}
},
"outputs": {
"control": {
"id": "b034aa0f-4d0d-46e4-b5e3-e25a9588d087",
"name": "control",
"type": "ControlField",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 508,
"position": {
"x": 4479.68542130465,
"y": -618.4221638099414
}
},
{
"id": "c4b23e64-7986-40c4-9cad-46327b12e204",
"type": "invocation",
"data": {
"id": "c4b23e64-7986-40c4-9cad-46327b12e204",
"type": "image",
"inputs": {
"image": {
"id": "189c8adf-68cc-4774-a729-49da89f6fdf1",
"name": "image",
"type": "ImageField",
"fieldKind": "input",
"label": "Canny Input Image"
}
},
"outputs": {
"image": {
"id": "1a31cacd-9d19-4f32-b558-c5e4aa39ce73",
"name": "image",
"type": "ImageField",
"fieldKind": "output"
},
"width": {
"id": "12f298fd-1d11-4cca-9426-01240f7ec7cf",
"name": "width",
"type": "integer",
"fieldKind": "output"
},
"height": {
"id": "c47dabcb-44e8-40c9-992d-81dca59f598e",
"name": "height",
"type": "integer",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 225,
"position": {
"x": 3593.7474460420153,
"y": -538.1200472386865
}
},
{
"id": "ca4d5059-8bfb-447f-b415-da0faba5a143",
"type": "invocation",
"data": {
"id": "ca4d5059-8bfb-447f-b415-da0faba5a143",
"type": "collect",
"inputs": {
"item": {
"id": "b16ae602-8708-4b1b-8d4f-9e0808d429ab",
"name": "item",
"type": "CollectionItem",
"fieldKind": "input",
"label": ""
}
},
"outputs": {
"collection": {
"id": "d8987dd8-dec8-4d94-816a-3e356af29884",
"name": "collection",
"type": "Collection",
"fieldKind": "output"
}
},
"label": "ControlNet Collection",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 104,
"position": {
"x": 4866.191497139488,
"y": -299.0538619537037
}
},
{
"id": "018b1214-c2af-43a7-9910-fb687c6726d7",
"type": "invocation",
"data": {
"id": "018b1214-c2af-43a7-9910-fb687c6726d7",
"type": "midas_depth_image_processor",
"inputs": {
"metadata": {
"id": "77f91980-c696-4a18-a9ea-6e2fc329a747",
"name": "metadata",
"type": "MetadataField",
"fieldKind": "input",
"label": ""
},
"image": {
"id": "50710a20-2af5-424d-9d17-aa08167829c6",
"name": "image",
"type": "ImageField",
"fieldKind": "input",
"label": ""
},
"a_mult": {
"id": "f3b26f9d-2498-415e-9c01-197a8d06c0a5",
"name": "a_mult",
"type": "float",
"fieldKind": "input",
"label": "",
"value": 2
},
"bg_th": {
"id": "4b1eb3ae-9d4a-47d6-b0ed-da62501e007f",
"name": "bg_th",
"type": "float",
"fieldKind": "input",
"label": "",
"value": 0.1
}
},
"outputs": {
"image": {
"id": "b4ed637c-c4a0-4fdd-a24e-36d6412e4ccf",
"name": "image",
"type": "ImageField",
"fieldKind": "output"
},
"width": {
"id": "6bf9b609-d72c-4239-99bd-390a73cc3a9c",
"name": "width",
"type": "integer",
"fieldKind": "output"
},
"height": {
"id": "3e8aef09-cf44-4e3e-a490-d3c9e7b23119",
"name": "height",
"type": "integer",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 339,
"position": {
"x": 4054.229311491893,
"y": -31.611411056365725
}
},
{
"id": "c826ba5e-9676-4475-b260-07b85e88753c",
"type": "invocation",
"data": {
"id": "c826ba5e-9676-4475-b260-07b85e88753c",
"type": "canny_image_processor",
"inputs": {
"metadata": {
"id": "08331ea6-99df-4e61-a919-204d9bfa8fb2",
"name": "metadata",
"type": "MetadataField",
"fieldKind": "input",
"label": ""
},
"image": {
"id": "33a37284-06ac-459c-ba93-1655e4f69b2d",
"name": "image",
"type": "ImageField",
"fieldKind": "input",
"label": ""
},
"low_threshold": {
"id": "21ec18a3-50c5-4ba1-9642-f921744d594f",
"name": "low_threshold",
"type": "integer",
"fieldKind": "input",
"label": "",
"value": 100
},
"high_threshold": {
"id": "ebeab271-a5ff-4c88-acfd-1d0271ab6ed4",
"name": "high_threshold",
"type": "integer",
"fieldKind": "input",
"label": "",
"value": 200
}
},
"outputs": {
"image": {
"id": "c0caadbf-883f-4cb4-a62d-626b9c81fc4e",
"name": "image",
"type": "ImageField",
"fieldKind": "output"
},
"width": {
"id": "df225843-8098-49c0-99d1-3b0b6600559f",
"name": "width",
"type": "integer",
"fieldKind": "output"
},
"height": {
"id": "e4abe0de-aa16-41f3-9cd7-968b49db5da3",
"name": "height",
"type": "integer",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 339,
"position": {
"x": 4095.757337055795,
"y": -455.63440891935863
}
},
{
"id": "9db25398-c869-4a63-8815-c6559341ef12",
"type": "invocation",
"data": {
"id": "9db25398-c869-4a63-8815-c6559341ef12",
"type": "l2i",
"inputs": {
"metadata": {
"id": "2f269793-72e5-4ff3-b76c-fab4f93e983f",
"name": "metadata",
"type": "MetadataField",
"fieldKind": "input",
"label": ""
},
"latents": {
"id": "4aaedd3b-cc77-420c-806e-c7fa74ec4cdf",
"name": "latents",
"type": "LatentsField",
"fieldKind": "input",
"label": ""
},
"vae": {
"id": "432b066a-2462-4d18-83d9-64620b72df45",
"name": "vae",
"type": "VaeField",
"fieldKind": "input",
"label": ""
},
"tiled": {
"id": "61f86e0f-7c46-40f8-b3f5-fe2f693595ca",
"name": "tiled",
"type": "boolean",
"fieldKind": "input",
"label": "",
"value": false
},
"fp32": {
"id": "39b6c89a-37ef-4a7e-9509-daeca49d5092",
"name": "fp32",
"type": "boolean",
"fieldKind": "input",
"label": "",
"value": false
}
},
"outputs": {
"image": {
"id": "6204e9b0-61dd-4250-b685-2092ba0e28e6",
"name": "image",
"type": "ImageField",
"fieldKind": "output"
},
"width": {
"id": "b4140649-8d5d-4d2d-bfa6-09e389ede5f9",
"name": "width",
"type": "integer",
"fieldKind": "output"
},
"height": {
"id": "f3a0c0c8-fc24-4646-8be1-ed8cdd140828",
"name": "height",
"type": "integer",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": false,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 267,
"position": {
"x": 5678.726701377887,
"y": -351.6792416734579
}
},
{
"id": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce",
"type": "invocation",
"data": {
"id": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce",
"type": "denoise_latents",
"inputs": {
"positive_conditioning": {
"id": "869cd309-c238-444b-a1a0-5021f99785ba",
"name": "positive_conditioning",
"type": "ConditioningField",
"fieldKind": "input",
"label": ""
},
"negative_conditioning": {
"id": "343447b4-1e37-4e9e-8ac7-4d04864066af",
"name": "negative_conditioning",
"type": "ConditioningField",
"fieldKind": "input",
"label": ""
},
"noise": {
"id": "b556571e-0cf9-4e03-8cfc-5caad937d957",
"name": "noise",
"type": "LatentsField",
"fieldKind": "input",
"label": ""
},
"steps": {
"id": "a3b3d2de-9308-423e-b00d-c209c3e6e808",
"name": "steps",
"type": "integer",
"fieldKind": "input",
"label": "",
"value": 10
},
"cfg_scale": {
"id": "b13c50a4-ec7e-4579-b0ef-2fe5df2605ea",
"name": "cfg_scale",
"type": "FloatPolymorphic",
"fieldKind": "input",
"label": "",
"value": 7.5
},
"denoising_start": {
"id": "57d5d755-f58f-4347-b991-f0bca4a0ab29",
"name": "denoising_start",
"type": "float",
"fieldKind": "input",
"label": "",
"value": 0
},
"denoising_end": {
"id": "323e78a6-880a-4d73-a62c-70faff965aa6",
"name": "denoising_end",
"type": "float",
"fieldKind": "input",
"label": "",
"value": 1
},
"scheduler": {
"id": "c25fdc17-a089-43ac-953e-067c45d5c76b",
"name": "scheduler",
"type": "Scheduler",
"fieldKind": "input",
"label": "",
"value": "euler"
},
"unet": {
"id": "6cde662b-e633-4569-b6b4-ec87c52c9c11",
"name": "unet",
"type": "UNetField",
"fieldKind": "input",
"label": ""
},
"control": {
"id": "276a4df9-bb26-4505-a4d3-a94e18c7b541",
"name": "control",
"type": "ControlPolymorphic",
"fieldKind": "input",
"label": ""
},
"ip_adapter": {
"id": "48d40c51-b5e2-4457-a428-eef0696695e8",
"name": "ip_adapter",
"type": "IPAdapterPolymorphic",
"fieldKind": "input",
"label": ""
},
"t2i_adapter": {
"id": "75dd8af2-e7d7-48b4-a574-edd9f6e686ad",
"name": "t2i_adapter",
"type": "T2IAdapterPolymorphic",
"fieldKind": "input",
"label": ""
},
"latents": {
"id": "9223d67b-1dd7-4b34-a45f-ed0a725d9702",
"name": "latents",
"type": "LatentsField",
"fieldKind": "input",
"label": ""
},
"denoise_mask": {
"id": "4ee99177-6923-4b7f-8fe0-d721dd7cb05b",
"name": "denoise_mask",
"type": "DenoiseMaskField",
"fieldKind": "input",
"label": ""
}
},
"outputs": {
"latents": {
"id": "7fb4e326-a974-43e8-9ee7-2e3ab235819d",
"name": "latents",
"type": "LatentsField",
"fieldKind": "output"
},
"width": {
"id": "6bb8acd0-8973-4195-a095-e376385dc705",
"name": "width",
"type": "integer",
"fieldKind": "output"
},
"height": {
"id": "795dea52-1c7d-4e64-99f7-2f60ec6e3ab9",
"name": "height",
"type": "integer",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.4.0"
},
"width": 320,
"height": 646,
"position": {
"x": 5274.672987098195,
"y": -823.0752416664332
}
}
],
"edges": [
{
"source": "54486974-835b-4d81-8f82-05f9f32ce9e9",
"sourceHandle": "clip",
"target": "7ce68934-3419-42d4-ac70-82cfc9397306",
"targetHandle": "clip",
"id": "reactflow__edge-54486974-835b-4d81-8f82-05f9f32ce9e9clip-7ce68934-3419-42d4-ac70-82cfc9397306clip",
"type": "default"
},
{
"source": "54486974-835b-4d81-8f82-05f9f32ce9e9",
"sourceHandle": "clip",
"target": "273e3f96-49ea-4dc5-9d5b-9660390f14e1",
"targetHandle": "clip",
"id": "reactflow__edge-54486974-835b-4d81-8f82-05f9f32ce9e9clip-273e3f96-49ea-4dc5-9d5b-9660390f14e1clip",
"type": "default"
},
{
"source": "a33199c2-8340-401e-b8a2-42ffa875fc1c",
"sourceHandle": "control",
"target": "ca4d5059-8bfb-447f-b415-da0faba5a143",
"targetHandle": "item",
"id": "reactflow__edge-a33199c2-8340-401e-b8a2-42ffa875fc1ccontrol-ca4d5059-8bfb-447f-b415-da0faba5a143item",
"type": "default"
},
{
"source": "d204d184-f209-4fae-a0a1-d152800844e1",
"sourceHandle": "control",
"target": "ca4d5059-8bfb-447f-b415-da0faba5a143",
"targetHandle": "item",
"id": "reactflow__edge-d204d184-f209-4fae-a0a1-d152800844e1control-ca4d5059-8bfb-447f-b415-da0faba5a143item",
"type": "default"
},
{
"source": "8e860e51-5045-456e-bf04-9a62a2a5c49e",
"sourceHandle": "image",
"target": "018b1214-c2af-43a7-9910-fb687c6726d7",
"targetHandle": "image",
"id": "reactflow__edge-8e860e51-5045-456e-bf04-9a62a2a5c49eimage-018b1214-c2af-43a7-9910-fb687c6726d7image",
"type": "default"
},
{
"source": "018b1214-c2af-43a7-9910-fb687c6726d7",
"sourceHandle": "image",
"target": "a33199c2-8340-401e-b8a2-42ffa875fc1c",
"targetHandle": "image",
"id": "reactflow__edge-018b1214-c2af-43a7-9910-fb687c6726d7image-a33199c2-8340-401e-b8a2-42ffa875fc1cimage",
"type": "default"
},
{
"source": "c4b23e64-7986-40c4-9cad-46327b12e204",
"sourceHandle": "image",
"target": "c826ba5e-9676-4475-b260-07b85e88753c",
"targetHandle": "image",
"id": "reactflow__edge-c4b23e64-7986-40c4-9cad-46327b12e204image-c826ba5e-9676-4475-b260-07b85e88753cimage",
"type": "default"
},
{
"source": "c826ba5e-9676-4475-b260-07b85e88753c",
"sourceHandle": "image",
"target": "d204d184-f209-4fae-a0a1-d152800844e1",
"targetHandle": "image",
"id": "reactflow__edge-c826ba5e-9676-4475-b260-07b85e88753cimage-d204d184-f209-4fae-a0a1-d152800844e1image",
"type": "default"
},
{
"source": "54486974-835b-4d81-8f82-05f9f32ce9e9",
"sourceHandle": "vae",
"target": "9db25398-c869-4a63-8815-c6559341ef12",
"targetHandle": "vae",
"id": "reactflow__edge-54486974-835b-4d81-8f82-05f9f32ce9e9vae-9db25398-c869-4a63-8815-c6559341ef12vae",
"type": "default"
},
{
"source": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce",
"sourceHandle": "latents",
"target": "9db25398-c869-4a63-8815-c6559341ef12",
"targetHandle": "latents",
"id": "reactflow__edge-ac481b7f-08bf-4a9d-9e0c-3a82ea5243celatents-9db25398-c869-4a63-8815-c6559341ef12latents",
"type": "default"
},
{
"source": "ca4d5059-8bfb-447f-b415-da0faba5a143",
"sourceHandle": "collection",
"target": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce",
"targetHandle": "control",
"id": "reactflow__edge-ca4d5059-8bfb-447f-b415-da0faba5a143collection-ac481b7f-08bf-4a9d-9e0c-3a82ea5243cecontrol",
"type": "default"
},
{
"source": "54486974-835b-4d81-8f82-05f9f32ce9e9",
"sourceHandle": "unet",
"target": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce",
"targetHandle": "unet",
"id": "reactflow__edge-54486974-835b-4d81-8f82-05f9f32ce9e9unet-ac481b7f-08bf-4a9d-9e0c-3a82ea5243ceunet",
"type": "default"
},
{
"source": "273e3f96-49ea-4dc5-9d5b-9660390f14e1",
"sourceHandle": "conditioning",
"target": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce",
"targetHandle": "negative_conditioning",
"id": "reactflow__edge-273e3f96-49ea-4dc5-9d5b-9660390f14e1conditioning-ac481b7f-08bf-4a9d-9e0c-3a82ea5243cenegative_conditioning",
"type": "default"
},
{
"source": "7ce68934-3419-42d4-ac70-82cfc9397306",
"sourceHandle": "conditioning",
"target": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce",
"targetHandle": "positive_conditioning",
"id": "reactflow__edge-7ce68934-3419-42d4-ac70-82cfc9397306conditioning-ac481b7f-08bf-4a9d-9e0c-3a82ea5243cepositive_conditioning",
"type": "default"
}
]
}

View File

@ -1,719 +0,0 @@
{
"name": "Prompt from File",
"author": "InvokeAI",
"description": "Sample workflow using prompt from file capabilities of InvokeAI ",
"version": "0.1.0",
"contact": "millun@invoke.ai",
"tags": "text2image, prompt from file, default",
"notes": "",
"exposedFields": [
{
"nodeId": "d6353b7f-b447-4e17-8f2e-80a88c91d426",
"fieldName": "model"
},
{
"nodeId": "1b7e0df8-8589-4915-a4ea-c0088f15d642",
"fieldName": "file_path"
}
],
"meta": {
"version": "1.0.0"
},
"nodes": [
{
"id": "c2eaf1ba-5708-4679-9e15-945b8b432692",
"type": "invocation",
"data": {
"id": "c2eaf1ba-5708-4679-9e15-945b8b432692",
"type": "compel",
"inputs": {
"prompt": {
"id": "dcdf3f6d-9b96-4bcd-9b8d-f992fefe4f62",
"name": "prompt",
"type": "string",
"fieldKind": "input",
"label": "",
"value": ""
},
"clip": {
"id": "3f1981c9-d8a9-42eb-a739-4f120eb80745",
"name": "clip",
"type": "ClipField",
"fieldKind": "input",
"label": ""
}
},
"outputs": {
"conditioning": {
"id": "46205e6c-c5e2-44cb-9c82-1cd20b95674a",
"name": "conditioning",
"type": "ConditioningField",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 261,
"position": {
"x": 1177.3417789657444,
"y": -102.0924766641035
}
},
{
"id": "1b7e0df8-8589-4915-a4ea-c0088f15d642",
"type": "invocation",
"data": {
"id": "1b7e0df8-8589-4915-a4ea-c0088f15d642",
"type": "prompt_from_file",
"inputs": {
"file_path": {
"id": "37e37684-4f30-4ec8-beae-b333e550f904",
"name": "file_path",
"type": "string",
"fieldKind": "input",
"label": "Prompts File Path",
"value": ""
},
"pre_prompt": {
"id": "7de02feb-819a-4992-bad3-72a30920ddea",
"name": "pre_prompt",
"type": "string",
"fieldKind": "input",
"label": "",
"value": ""
},
"post_prompt": {
"id": "95f191d8-a282-428e-bd65-de8cb9b7513a",
"name": "post_prompt",
"type": "string",
"fieldKind": "input",
"label": "",
"value": ""
},
"start_line": {
"id": "efee9a48-05ab-4829-8429-becfa64a0782",
"name": "start_line",
"type": "integer",
"fieldKind": "input",
"label": "",
"value": 1
},
"max_prompts": {
"id": "abebb428-3d3d-49fd-a482-4e96a16fff08",
"name": "max_prompts",
"type": "integer",
"fieldKind": "input",
"label": "",
"value": 1
}
},
"outputs": {
"collection": {
"id": "77d5d7f1-9877-4ab1-9a8c-33e9ffa9abf3",
"name": "collection",
"type": "StringCollection",
"fieldKind": "output"
}
},
"label": "Prompts from File",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 589,
"position": {
"x": 394.181884547075,
"y": -423.5345157864633
}
},
{
"id": "1b89067c-3f6b-42c8-991f-e3055789b251",
"type": "invocation",
"data": {
"id": "1b89067c-3f6b-42c8-991f-e3055789b251",
"type": "iterate",
"inputs": {
"collection": {
"id": "4c564bf8-5ed6-441e-ad2c-dda265d5785f",
"name": "collection",
"type": "Collection",
"fieldKind": "input",
"label": "",
"value": []
}
},
"outputs": {
"item": {
"id": "36340f9a-e7a5-4afa-b4b5-313f4e292380",
"name": "item",
"type": "CollectionItem",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 104,
"position": {
"x": 792.8735298060233,
"y": -432.6964953027252
}
},
{
"id": "d6353b7f-b447-4e17-8f2e-80a88c91d426",
"type": "invocation",
"data": {
"id": "d6353b7f-b447-4e17-8f2e-80a88c91d426",
"type": "main_model_loader",
"inputs": {
"model": {
"id": "3f264259-3418-47d5-b90d-b6600e36ae46",
"name": "model",
"type": "MainModelField",
"fieldKind": "input",
"label": "",
"value": {
"model_name": "stable-diffusion-v1-5",
"base_model": "sd-1",
"model_type": "main"
}
}
},
"outputs": {
"unet": {
"id": "8e182ea2-9d0a-4c02-9407-27819288d4b5",
"name": "unet",
"type": "UNetField",
"fieldKind": "output"
},
"clip": {
"id": "d67d9d30-058c-46d5-bded-3d09d6d1aa39",
"name": "clip",
"type": "ClipField",
"fieldKind": "output"
},
"vae": {
"id": "89641601-0429-4448-98d5-190822d920d8",
"name": "vae",
"type": "VaeField",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 226,
"position": {
"x": -47.66201354137797,
"y": -299.218193067033
}
},
{
"id": "fc9d0e35-a6de-4a19-84e1-c72497c823f6",
"type": "invocation",
"data": {
"id": "fc9d0e35-a6de-4a19-84e1-c72497c823f6",
"type": "compel",
"inputs": {
"prompt": {
"id": "dcdf3f6d-9b96-4bcd-9b8d-f992fefe4f62",
"name": "prompt",
"type": "string",
"fieldKind": "input",
"label": "",
"value": ""
},
"clip": {
"id": "3f1981c9-d8a9-42eb-a739-4f120eb80745",
"name": "clip",
"type": "ClipField",
"fieldKind": "input",
"label": ""
}
},
"outputs": {
"conditioning": {
"id": "46205e6c-c5e2-44cb-9c82-1cd20b95674a",
"name": "conditioning",
"type": "ConditioningField",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 261,
"position": {
"x": 1175.0187896425462,
"y": -420.64289413577114
}
},
{
"id": "0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77",
"type": "invocation",
"data": {
"id": "0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77",
"type": "noise",
"inputs": {
"seed": {
"id": "b722d84a-eeee-484f-bef2-0250c027cb67",
"name": "seed",
"type": "integer",
"fieldKind": "input",
"label": "",
"value": 0
},
"width": {
"id": "d5f8ce11-0502-4bfc-9a30-5757dddf1f94",
"name": "width",
"type": "integer",
"fieldKind": "input",
"label": "",
"value": 512
},
"height": {
"id": "f187d5ff-38a5-4c3f-b780-fc5801ef34af",
"name": "height",
"type": "integer",
"fieldKind": "input",
"label": "",
"value": 512
},
"use_cpu": {
"id": "12f112b8-8b76-4816-b79e-662edc9f9aa5",
"name": "use_cpu",
"type": "boolean",
"fieldKind": "input",
"label": "",
"value": true
}
},
"outputs": {
"noise": {
"id": "08576ad1-96d9-42d2-96ef-6f5c1961933f",
"name": "noise",
"type": "LatentsField",
"fieldKind": "output"
},
"width": {
"id": "f3e1f94a-258d-41ff-9789-bd999bd9f40d",
"name": "width",
"type": "integer",
"fieldKind": "output"
},
"height": {
"id": "6cefc357-4339-415e-a951-49b9c2be32f4",
"name": "height",
"type": "integer",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 389,
"position": {
"x": 809.1964864135837,
"y": 183.2735123359796
}
},
{
"id": "dfc20e07-7aef-4fc0-a3a1-7bf68ec6a4e5",
"type": "invocation",
"data": {
"id": "dfc20e07-7aef-4fc0-a3a1-7bf68ec6a4e5",
"type": "rand_int",
"inputs": {
"low": {
"id": "b9fc6cf1-469c-4037-9bf0-04836965826f",
"name": "low",
"type": "integer",
"fieldKind": "input",
"label": "",
"value": 0
},
"high": {
"id": "06eac725-0f60-4ba2-b8cd-7ad9f757488c",
"name": "high",
"type": "integer",
"fieldKind": "input",
"label": "",
"value": 2147483647
}
},
"outputs": {
"value": {
"id": "df08c84e-7346-4e92-9042-9e5cb773aaff",
"name": "value",
"type": "integer",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": false,
"version": "1.0.0"
},
"width": 320,
"height": 218,
"position": {
"x": 354.19913145404166,
"y": 301.86324846905165
}
},
{
"id": "491ec988-3c77-4c37-af8a-39a0c4e7a2a1",
"type": "invocation",
"data": {
"id": "491ec988-3c77-4c37-af8a-39a0c4e7a2a1",
"type": "l2i",
"inputs": {
"metadata": {
"id": "022e4b33-562b-438d-b7df-41c3fd931f40",
"name": "metadata",
"type": "MetadataField",
"fieldKind": "input",
"label": ""
},
"latents": {
"id": "67cb6c77-a394-4a66-a6a9-a0a7dcca69ec",
"name": "latents",
"type": "LatentsField",
"fieldKind": "input",
"label": ""
},
"vae": {
"id": "7b3fd9ad-a4ef-4e04-89fa-3832a9902dbd",
"name": "vae",
"type": "VaeField",
"fieldKind": "input",
"label": ""
},
"tiled": {
"id": "5ac5680d-3add-4115-8ec0-9ef5bb87493b",
"name": "tiled",
"type": "boolean",
"fieldKind": "input",
"label": "",
"value": false
},
"fp32": {
"id": "db8297f5-55f8-452f-98cf-6572c2582152",
"name": "fp32",
"type": "boolean",
"fieldKind": "input",
"label": "",
"value": false
}
},
"outputs": {
"image": {
"id": "d8778d0c-592a-4960-9280-4e77e00a7f33",
"name": "image",
"type": "ImageField",
"fieldKind": "output"
},
"width": {
"id": "c8b0a75a-f5de-4ff2-9227-f25bb2b97bec",
"name": "width",
"type": "integer",
"fieldKind": "output"
},
"height": {
"id": "83c05fbf-76b9-49ab-93c4-fa4b10e793e4",
"name": "height",
"type": "integer",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 267,
"position": {
"x": 2037.861329274915,
"y": -329.8393457509562
}
},
{
"id": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e",
"type": "invocation",
"data": {
"id": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e",
"type": "denoise_latents",
"inputs": {
"positive_conditioning": {
"id": "751fb35b-3f23-45ce-af1c-053e74251337",
"name": "positive_conditioning",
"type": "ConditioningField",
"fieldKind": "input",
"label": ""
},
"negative_conditioning": {
"id": "b9dc06b6-7481-4db1-a8c2-39d22a5eacff",
"name": "negative_conditioning",
"type": "ConditioningField",
"fieldKind": "input",
"label": ""
},
"noise": {
"id": "6e15e439-3390-48a4-8031-01e0e19f0e1d",
"name": "noise",
"type": "LatentsField",
"fieldKind": "input",
"label": ""
},
"steps": {
"id": "bfdfb3df-760b-4d51-b17b-0abb38b976c2",
"name": "steps",
"type": "integer",
"fieldKind": "input",
"label": "",
"value": 10
},
"cfg_scale": {
"id": "47770858-322e-41af-8494-d8b63ed735f3",
"name": "cfg_scale",
"type": "FloatPolymorphic",
"fieldKind": "input",
"label": "",
"value": 7.5
},
"denoising_start": {
"id": "2ba78720-ee02-4130-a348-7bc3531f790b",
"name": "denoising_start",
"type": "float",
"fieldKind": "input",
"label": "",
"value": 0
},
"denoising_end": {
"id": "a874dffb-d433-4d1a-9f59-af4367bb05e4",
"name": "denoising_end",
"type": "float",
"fieldKind": "input",
"label": "",
"value": 1
},
"scheduler": {
"id": "36e021ad-b762-4fe4-ad4d-17f0291c40b2",
"name": "scheduler",
"type": "Scheduler",
"fieldKind": "input",
"label": "",
"value": "euler"
},
"unet": {
"id": "98d3282d-f9f6-4b5e-b9e8-58658f1cac78",
"name": "unet",
"type": "UNetField",
"fieldKind": "input",
"label": ""
},
"control": {
"id": "f2ea3216-43d5-42b4-887f-36e8f7166d53",
"name": "control",
"type": "ControlPolymorphic",
"fieldKind": "input",
"label": ""
},
"ip_adapter": {
"id": "d0780610-a298-47c8-a54e-70e769e0dfe2",
"name": "ip_adapter",
"type": "IPAdapterPolymorphic",
"fieldKind": "input",
"label": ""
},
"t2i_adapter": {
"id": "fdb40970-185e-4ea8-8bb5-88f06f91f46a",
"name": "t2i_adapter",
"type": "T2IAdapterPolymorphic",
"fieldKind": "input",
"label": ""
},
"latents": {
"id": "e05b538a-1b5a-4aa5-84b1-fd2361289a81",
"name": "latents",
"type": "LatentsField",
"fieldKind": "input",
"label": ""
},
"denoise_mask": {
"id": "463a419e-df30-4382-8ffb-b25b25abe425",
"name": "denoise_mask",
"type": "DenoiseMaskField",
"fieldKind": "input",
"label": ""
}
},
"outputs": {
"latents": {
"id": "559ee688-66cf-4139-8b82-3d3aa69995ce",
"name": "latents",
"type": "LatentsField",
"fieldKind": "output"
},
"width": {
"id": "0b4285c2-e8b9-48e5-98f6-0a49d3f98fd2",
"name": "width",
"type": "integer",
"fieldKind": "output"
},
"height": {
"id": "8b0881b9-45e5-47d5-b526-24b6661de0ee",
"name": "height",
"type": "integer",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.4.0"
},
"width": 320,
"height": 646,
"position": {
"x": 1570.9941088179146,
"y": -407.6505491604564
}
}
],
"edges": [
{
"source": "1b7e0df8-8589-4915-a4ea-c0088f15d642",
"sourceHandle": "collection",
"target": "1b89067c-3f6b-42c8-991f-e3055789b251",
"targetHandle": "collection",
"id": "reactflow__edge-1b7e0df8-8589-4915-a4ea-c0088f15d642collection-1b89067c-3f6b-42c8-991f-e3055789b251collection",
"type": "default"
},
{
"source": "d6353b7f-b447-4e17-8f2e-80a88c91d426",
"sourceHandle": "clip",
"target": "fc9d0e35-a6de-4a19-84e1-c72497c823f6",
"targetHandle": "clip",
"id": "reactflow__edge-d6353b7f-b447-4e17-8f2e-80a88c91d426clip-fc9d0e35-a6de-4a19-84e1-c72497c823f6clip",
"type": "default"
},
{
"source": "1b89067c-3f6b-42c8-991f-e3055789b251",
"sourceHandle": "item",
"target": "fc9d0e35-a6de-4a19-84e1-c72497c823f6",
"targetHandle": "prompt",
"id": "reactflow__edge-1b89067c-3f6b-42c8-991f-e3055789b251item-fc9d0e35-a6de-4a19-84e1-c72497c823f6prompt",
"type": "default"
},
{
"source": "d6353b7f-b447-4e17-8f2e-80a88c91d426",
"sourceHandle": "clip",
"target": "c2eaf1ba-5708-4679-9e15-945b8b432692",
"targetHandle": "clip",
"id": "reactflow__edge-d6353b7f-b447-4e17-8f2e-80a88c91d426clip-c2eaf1ba-5708-4679-9e15-945b8b432692clip",
"type": "default"
},
{
"source": "dfc20e07-7aef-4fc0-a3a1-7bf68ec6a4e5",
"sourceHandle": "value",
"target": "0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77",
"targetHandle": "seed",
"id": "reactflow__edge-dfc20e07-7aef-4fc0-a3a1-7bf68ec6a4e5value-0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77seed",
"type": "default"
},
{
"source": "fc9d0e35-a6de-4a19-84e1-c72497c823f6",
"sourceHandle": "conditioning",
"target": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e",
"targetHandle": "positive_conditioning",
"id": "reactflow__edge-fc9d0e35-a6de-4a19-84e1-c72497c823f6conditioning-2fb1577f-0a56-4f12-8711-8afcaaaf1d5epositive_conditioning",
"type": "default"
},
{
"source": "c2eaf1ba-5708-4679-9e15-945b8b432692",
"sourceHandle": "conditioning",
"target": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e",
"targetHandle": "negative_conditioning",
"id": "reactflow__edge-c2eaf1ba-5708-4679-9e15-945b8b432692conditioning-2fb1577f-0a56-4f12-8711-8afcaaaf1d5enegative_conditioning",
"type": "default"
},
{
"source": "0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77",
"sourceHandle": "noise",
"target": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e",
"targetHandle": "noise",
"id": "reactflow__edge-0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77noise-2fb1577f-0a56-4f12-8711-8afcaaaf1d5enoise",
"type": "default"
},
{
"source": "d6353b7f-b447-4e17-8f2e-80a88c91d426",
"sourceHandle": "unet",
"target": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e",
"targetHandle": "unet",
"id": "reactflow__edge-d6353b7f-b447-4e17-8f2e-80a88c91d426unet-2fb1577f-0a56-4f12-8711-8afcaaaf1d5eunet",
"type": "default"
},
{
"source": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e",
"sourceHandle": "latents",
"target": "491ec988-3c77-4c37-af8a-39a0c4e7a2a1",
"targetHandle": "latents",
"id": "reactflow__edge-2fb1577f-0a56-4f12-8711-8afcaaaf1d5elatents-491ec988-3c77-4c37-af8a-39a0c4e7a2a1latents",
"type": "default"
},
{
"source": "d6353b7f-b447-4e17-8f2e-80a88c91d426",
"sourceHandle": "vae",
"target": "491ec988-3c77-4c37-af8a-39a0c4e7a2a1",
"targetHandle": "vae",
"id": "reactflow__edge-d6353b7f-b447-4e17-8f2e-80a88c91d426vae-491ec988-3c77-4c37-af8a-39a0c4e7a2a1vae",
"type": "default"
}
]
}

View File

@ -1,758 +0,0 @@
{
"name": "QR Code Monster",
"author": "InvokeAI",
"description": "Sample workflow for create images with QR code Monster ControlNet",
"version": "1.0.1",
"contact": "invoke@invoke.ai",
"tags": "qrcode, controlnet, default",
"notes": "",
"exposedFields": [
{
"nodeId": "a6cc0986-f928-4a7e-8d44-ba2d4b36f54a",
"fieldName": "image"
},
{
"nodeId": "aca3b054-bfba-4392-bd20-6476f59504df",
"fieldName": "prompt"
},
{
"nodeId": "3db7cee0-31e2-4a3d-94a1-268cb16177dd",
"fieldName": "prompt"
}
],
"meta": {
"version": "1.0.0"
},
"nodes": [
{
"id": "3db7cee0-31e2-4a3d-94a1-268cb16177dd",
"type": "invocation",
"data": {
"id": "3db7cee0-31e2-4a3d-94a1-268cb16177dd",
"type": "compel",
"inputs": {
"prompt": {
"id": "6a1fe244-5656-4f8c-91d1-1fb474e28807",
"name": "prompt",
"type": "string",
"fieldKind": "input",
"label": "Negative Prompt",
"value": ""
},
"clip": {
"id": "f24688f3-29b8-4a2d-8603-046e5a5c7250",
"name": "clip",
"type": "ClipField",
"fieldKind": "input",
"label": ""
}
},
"outputs": {
"conditioning": {
"id": "700528eb-3f8b-4745-b540-34f919b5b228",
"name": "conditioning",
"type": "ConditioningField",
"fieldKind": "output"
}
},
"label": "Prompt",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 261,
"position": {
"x": 773.0502679628016,
"y": 1622.4836086770556
}
},
{
"id": "610384f1-6f0c-4847-a9a2-37ce7f456ed1",
"type": "invocation",
"data": {
"id": "610384f1-6f0c-4847-a9a2-37ce7f456ed1",
"type": "main_model_loader",
"inputs": {
"model": {
"id": "cb36b6d3-6c1f-4911-a200-646745b0ff74",
"name": "model",
"type": "MainModelField",
"fieldKind": "input",
"label": "",
"value": {
"model_name": "stable-diffusion-v1-5",
"base_model": "sd-1",
"model_type": "main"
}
}
},
"outputs": {
"unet": {
"id": "7246895b-b252-49bc-b952-8d801b4672f7",
"name": "unet",
"type": "UNetField",
"fieldKind": "output"
},
"clip": {
"id": "3c2aedb8-30d5-4d4b-99df-d06a0d7bedc6",
"name": "clip",
"type": "ClipField",
"fieldKind": "output"
},
"vae": {
"id": "b9743815-5501-4bbb-8bde-8bd6ba298a4e",
"name": "vae",
"type": "VaeField",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 226,
"position": {
"x": 211.58866462619744,
"y": 1376.0542388105248
}
},
{
"id": "aca3b054-bfba-4392-bd20-6476f59504df",
"type": "invocation",
"data": {
"id": "aca3b054-bfba-4392-bd20-6476f59504df",
"type": "compel",
"inputs": {
"prompt": {
"id": "6a1fe244-5656-4f8c-91d1-1fb474e28807",
"name": "prompt",
"type": "string",
"fieldKind": "input",
"label": "Positive Prompt",
"value": ""
},
"clip": {
"id": "f24688f3-29b8-4a2d-8603-046e5a5c7250",
"name": "clip",
"type": "ClipField",
"fieldKind": "input",
"label": ""
}
},
"outputs": {
"conditioning": {
"id": "700528eb-3f8b-4745-b540-34f919b5b228",
"name": "conditioning",
"type": "ConditioningField",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 261,
"position": {
"x": 770.6491131680111,
"y": 1316.379247112241
}
},
{
"id": "a6cc0986-f928-4a7e-8d44-ba2d4b36f54a",
"type": "invocation",
"data": {
"id": "a6cc0986-f928-4a7e-8d44-ba2d4b36f54a",
"type": "image",
"inputs": {
"image": {
"id": "89ba5d58-28c9-4e04-a5df-79fb7a6f3531",
"name": "image",
"type": "ImageField",
"fieldKind": "input",
"label": "QR Code / Hidden Image"
}
},
"outputs": {
"image": {
"id": "54335653-0e17-42da-b9e8-83c5fb5af670",
"name": "image",
"type": "ImageField",
"fieldKind": "output"
},
"width": {
"id": "a3c65953-39ea-4d97-8858-d65154ff9d11",
"name": "width",
"type": "integer",
"fieldKind": "output"
},
"height": {
"id": "2c7db511-ebc9-4286-a46b-bc11e0fd779f",
"name": "height",
"type": "integer",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 225,
"position": {
"x": 700.5034176864369,
"y": 1981.749600549388
}
},
{
"id": "280fd8a7-3b0c-49fe-8be4-6246e08b6c9a",
"type": "invocation",
"data": {
"id": "280fd8a7-3b0c-49fe-8be4-6246e08b6c9a",
"type": "noise",
"inputs": {
"seed": {
"id": "7c6c76dd-127b-4829-b1ec-430790cb7ed7",
"name": "seed",
"type": "integer",
"fieldKind": "input",
"label": "",
"value": 0
},
"width": {
"id": "8ec6a525-a421-40d8-a17e-39e7b6836438",
"name": "width",
"type": "integer",
"fieldKind": "input",
"label": "",
"value": 512
},
"height": {
"id": "6af1e58a-e2ee-4ec4-9f06-d8d0412922ca",
"name": "height",
"type": "integer",
"fieldKind": "input",
"label": "",
"value": 512
},
"use_cpu": {
"id": "26662e99-5720-43a6-a5d8-06c9dab0e261",
"name": "use_cpu",
"type": "boolean",
"fieldKind": "input",
"label": "",
"value": true
}
},
"outputs": {
"noise": {
"id": "cb4c4dfc-a744-49eb-af4f-677448e28407",
"name": "noise",
"type": "LatentsField",
"fieldKind": "output"
},
"width": {
"id": "97e87be6-e81f-40a3-a522-28ebe4aad0ac",
"name": "width",
"type": "integer",
"fieldKind": "output"
},
"height": {
"id": "80784420-f1e1-47b0-bd1d-1d381a15e22d",
"name": "height",
"type": "integer",
"fieldKind": "output"
}
},
"label": "",
"isOpen": false,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 32,
"position": {
"x": 1182.460291960481,
"y": 1759.592972960265
}
},
{
"id": "2ac03cf6-0326-454a-bed0-d8baef2bf30d",
"type": "invocation",
"data": {
"id": "2ac03cf6-0326-454a-bed0-d8baef2bf30d",
"type": "controlnet",
"inputs": {
"image": {
"id": "1f683889-9f14-40c8-af29-4b991b211a3a",
"name": "image",
"type": "ImageField",
"fieldKind": "input",
"label": ""
},
"control_model": {
"id": "a933b21d-22c1-4e06-818f-15416b971282",
"name": "control_model",
"type": "ControlNetModelField",
"fieldKind": "input",
"label": "",
"value": {
"model_name": "qrcode_monster",
"base_model": "sd-1"
}
},
"control_weight": {
"id": "198a0825-e55e-4496-bc54-c3d7b02f3d75",
"name": "control_weight",
"type": "FloatPolymorphic",
"fieldKind": "input",
"label": "",
"value": 1.4
},
"begin_step_percent": {
"id": "c85ce42f-22af-42a0-8993-676002fb275e",
"name": "begin_step_percent",
"type": "float",
"fieldKind": "input",
"label": "",
"value": 0
},
"end_step_percent": {
"id": "a61a65c4-9e6f-4fe2-96a5-1294d17ec6e4",
"name": "end_step_percent",
"type": "float",
"fieldKind": "input",
"label": "",
"value": 1
},
"control_mode": {
"id": "1aa45cfa-0249-46b7-bf24-3e38e92f5fa0",
"name": "control_mode",
"type": "enum",
"fieldKind": "input",
"label": "",
"value": "balanced"
},
"resize_mode": {
"id": "a89d3cb9-a141-4cea-bb49-977bf267377b",
"name": "resize_mode",
"type": "enum",
"fieldKind": "input",
"label": "",
"value": "just_resize"
}
},
"outputs": {
"control": {
"id": "c9a1fc7e-cb25-45a9-adff-1a97c9ff04d6",
"name": "control",
"type": "ControlField",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 508,
"position": {
"x": 1165.434407461108,
"y": 1862.916856351665
}
},
{
"id": "28542b66-5a00-4780-a318-0a036d2df914",
"type": "invocation",
"data": {
"id": "28542b66-5a00-4780-a318-0a036d2df914",
"type": "l2i",
"inputs": {
"metadata": {
"id": "a38e8f55-7f2c-4fcc-a71f-d51e2eb0374a",
"name": "metadata",
"type": "MetadataField",
"fieldKind": "input",
"label": ""
},
"latents": {
"id": "80e97bc8-e716-4175-9115-5b58495aa30c",
"name": "latents",
"type": "LatentsField",
"fieldKind": "input",
"label": ""
},
"vae": {
"id": "5641bce6-ac2b-47eb-bb32-2f290026b7e1",
"name": "vae",
"type": "VaeField",
"fieldKind": "input",
"label": ""
},
"tiled": {
"id": "9e75eb16-ae48-47ed-b180-e0409d377436",
"name": "tiled",
"type": "boolean",
"fieldKind": "input",
"label": "",
"value": false
},
"fp32": {
"id": "0518b0ce-ee37-437b-8437-cc2976a3279f",
"name": "fp32",
"type": "boolean",
"fieldKind": "input",
"label": "",
"value": false
}
},
"outputs": {
"image": {
"id": "ec2ff985-a7eb-401f-92c4-1217cddad6a2",
"name": "image",
"type": "ImageField",
"fieldKind": "output"
},
"width": {
"id": "ba1d1720-6d67-4eca-9e9d-b97d08636774",
"name": "width",
"type": "integer",
"fieldKind": "output"
},
"height": {
"id": "10bcf8f4-6394-422f-b0c0-51680f3bfb25",
"name": "height",
"type": "integer",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 267,
"position": {
"x": 2110.8415693683014,
"y": 1487.253341116115
}
},
{
"id": "9755ae4c-ef30-4db3-80f6-a31f98979a11",
"type": "invocation",
"data": {
"id": "9755ae4c-ef30-4db3-80f6-a31f98979a11",
"type": "denoise_latents",
"inputs": {
"positive_conditioning": {
"id": "8e6aceaa-a986-4ab2-9c04-5b1027b3daf6",
"name": "positive_conditioning",
"type": "ConditioningField",
"fieldKind": "input",
"label": ""
},
"negative_conditioning": {
"id": "fbbaa712-ca1a-420b-9016-763f2a29d68c",
"name": "negative_conditioning",
"type": "ConditioningField",
"fieldKind": "input",
"label": ""
},
"noise": {
"id": "a3b3d5d2-c0f9-4b89-a9b3-8de9418f7bb5",
"name": "noise",
"type": "LatentsField",
"fieldKind": "input",
"label": ""
},
"steps": {
"id": "e491e664-2f8c-4f49-b3e4-57b051fbb9c5",
"name": "steps",
"type": "integer",
"fieldKind": "input",
"label": "",
"value": 10
},
"cfg_scale": {
"id": "f0318abd-ed65-4cad-86a7-48d1c19a6d14",
"name": "cfg_scale",
"type": "FloatPolymorphic",
"fieldKind": "input",
"label": "",
"value": 7.5
},
"denoising_start": {
"id": "f7c24c51-496f-44c4-836a-c734e529fec0",
"name": "denoising_start",
"type": "float",
"fieldKind": "input",
"label": "",
"value": 0
},
"denoising_end": {
"id": "54f7656a-fb0d-4d9e-a459-f700f7dccd2e",
"name": "denoising_end",
"type": "float",
"fieldKind": "input",
"label": "",
"value": 1
},
"scheduler": {
"id": "363ee440-040d-499b-bf84-bf5391b08681",
"name": "scheduler",
"type": "Scheduler",
"fieldKind": "input",
"label": "",
"value": "euler"
},
"unet": {
"id": "5c93d4e5-1064-4700-ab1d-d12e1e9b5ba7",
"name": "unet",
"type": "UNetField",
"fieldKind": "input",
"label": ""
},
"control": {
"id": "e1948eb3-7407-43b0-93e3-139470f186b7",
"name": "control",
"type": "ControlPolymorphic",
"fieldKind": "input",
"label": ""
},
"ip_adapter": {
"id": "5675b2c3-adfb-49ee-b33c-26bdbfab1fed",
"name": "ip_adapter",
"type": "IPAdapterPolymorphic",
"fieldKind": "input",
"label": ""
},
"t2i_adapter": {
"id": "89cd4ab3-3bfc-4063-9de5-91d42305c651",
"name": "t2i_adapter",
"type": "T2IAdapterPolymorphic",
"fieldKind": "input",
"label": ""
},
"latents": {
"id": "ec01df90-5042-418d-b6d6-86b251c13770",
"name": "latents",
"type": "LatentsField",
"fieldKind": "input",
"label": ""
},
"denoise_mask": {
"id": "561cde00-cb20-42ae-9bd3-4f477f73fbe1",
"name": "denoise_mask",
"type": "DenoiseMaskField",
"fieldKind": "input",
"label": ""
}
},
"outputs": {
"latents": {
"id": "f9addefe-efcc-4e01-8945-6ebbc934b002",
"name": "latents",
"type": "LatentsField",
"fieldKind": "output"
},
"width": {
"id": "6d48f78b-d681-422a-8677-0111bd0625f1",
"name": "width",
"type": "integer",
"fieldKind": "output"
},
"height": {
"id": "f25997b8-6316-44ce-b696-b82e4ed51ae5",
"name": "height",
"type": "integer",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": true,
"version": "1.4.0"
},
"width": 320,
"height": 646,
"position": {
"x": 1597.9598293300219,
"y": 1420.4637727891632
}
},
{
"id": "59349822-af20-4e0e-a53f-3ba135d00c3f",
"type": "invocation",
"data": {
"id": "59349822-af20-4e0e-a53f-3ba135d00c3f",
"type": "rand_int",
"inputs": {
"low": {
"id": "051f22f9-2d4f-414f-bc51-84af2d626efa",
"name": "low",
"type": "integer",
"fieldKind": "input",
"label": "",
"value": 0
},
"high": {
"id": "77206186-f264-4224-9589-f925cf903dc9",
"name": "high",
"type": "integer",
"fieldKind": "input",
"label": "",
"value": 2147483647
}
},
"outputs": {
"value": {
"id": "a7ed9387-3a24-4d34-b7c5-f713bd544ab1",
"name": "value",
"type": "integer",
"fieldKind": "output"
}
},
"label": "",
"isOpen": false,
"notes": "",
"embedWorkflow": false,
"isIntermediate": true,
"useCache": false,
"version": "1.0.0"
},
"width": 320,
"height": 32,
"position": {
"x": 1178.16746986153,
"y": 1663.9433412808876
}
}
],
"edges": [
{
"source": "59349822-af20-4e0e-a53f-3ba135d00c3f",
"target": "280fd8a7-3b0c-49fe-8be4-6246e08b6c9a",
"id": "59349822-af20-4e0e-a53f-3ba135d00c3f-280fd8a7-3b0c-49fe-8be4-6246e08b6c9a-collapsed",
"type": "collapsed"
},
{
"source": "610384f1-6f0c-4847-a9a2-37ce7f456ed1",
"sourceHandle": "clip",
"target": "aca3b054-bfba-4392-bd20-6476f59504df",
"targetHandle": "clip",
"id": "reactflow__edge-610384f1-6f0c-4847-a9a2-37ce7f456ed1clip-aca3b054-bfba-4392-bd20-6476f59504dfclip",
"type": "default"
},
{
"source": "610384f1-6f0c-4847-a9a2-37ce7f456ed1",
"sourceHandle": "clip",
"target": "3db7cee0-31e2-4a3d-94a1-268cb16177dd",
"targetHandle": "clip",
"id": "reactflow__edge-610384f1-6f0c-4847-a9a2-37ce7f456ed1clip-3db7cee0-31e2-4a3d-94a1-268cb16177ddclip",
"type": "default"
},
{
"source": "a6cc0986-f928-4a7e-8d44-ba2d4b36f54a",
"sourceHandle": "image",
"target": "2ac03cf6-0326-454a-bed0-d8baef2bf30d",
"targetHandle": "image",
"id": "reactflow__edge-a6cc0986-f928-4a7e-8d44-ba2d4b36f54aimage-2ac03cf6-0326-454a-bed0-d8baef2bf30dimage",
"type": "default"
},
{
"source": "610384f1-6f0c-4847-a9a2-37ce7f456ed1",
"sourceHandle": "vae",
"target": "28542b66-5a00-4780-a318-0a036d2df914",
"targetHandle": "vae",
"id": "reactflow__edge-610384f1-6f0c-4847-a9a2-37ce7f456ed1vae-28542b66-5a00-4780-a318-0a036d2df914vae",
"type": "default"
},
{
"source": "280fd8a7-3b0c-49fe-8be4-6246e08b6c9a",
"sourceHandle": "noise",
"target": "9755ae4c-ef30-4db3-80f6-a31f98979a11",
"targetHandle": "noise",
"id": "reactflow__edge-280fd8a7-3b0c-49fe-8be4-6246e08b6c9anoise-9755ae4c-ef30-4db3-80f6-a31f98979a11noise",
"type": "default"
},
{
"source": "3db7cee0-31e2-4a3d-94a1-268cb16177dd",
"sourceHandle": "conditioning",
"target": "9755ae4c-ef30-4db3-80f6-a31f98979a11",
"targetHandle": "negative_conditioning",
"id": "reactflow__edge-3db7cee0-31e2-4a3d-94a1-268cb16177ddconditioning-9755ae4c-ef30-4db3-80f6-a31f98979a11negative_conditioning",
"type": "default"
},
{
"source": "aca3b054-bfba-4392-bd20-6476f59504df",
"sourceHandle": "conditioning",
"target": "9755ae4c-ef30-4db3-80f6-a31f98979a11",
"targetHandle": "positive_conditioning",
"id": "reactflow__edge-aca3b054-bfba-4392-bd20-6476f59504dfconditioning-9755ae4c-ef30-4db3-80f6-a31f98979a11positive_conditioning",
"type": "default"
},
{
"source": "610384f1-6f0c-4847-a9a2-37ce7f456ed1",
"sourceHandle": "unet",
"target": "9755ae4c-ef30-4db3-80f6-a31f98979a11",
"targetHandle": "unet",
"id": "reactflow__edge-610384f1-6f0c-4847-a9a2-37ce7f456ed1unet-9755ae4c-ef30-4db3-80f6-a31f98979a11unet",
"type": "default"
},
{
"source": "2ac03cf6-0326-454a-bed0-d8baef2bf30d",
"sourceHandle": "control",
"target": "9755ae4c-ef30-4db3-80f6-a31f98979a11",
"targetHandle": "control",
"id": "reactflow__edge-2ac03cf6-0326-454a-bed0-d8baef2bf30dcontrol-9755ae4c-ef30-4db3-80f6-a31f98979a11control",
"type": "default"
},
{
"source": "9755ae4c-ef30-4db3-80f6-a31f98979a11",
"sourceHandle": "latents",
"target": "28542b66-5a00-4780-a318-0a036d2df914",
"targetHandle": "latents",
"id": "reactflow__edge-9755ae4c-ef30-4db3-80f6-a31f98979a11latents-28542b66-5a00-4780-a318-0a036d2df914latents",
"type": "default"
},
{
"source": "59349822-af20-4e0e-a53f-3ba135d00c3f",
"sourceHandle": "value",
"target": "280fd8a7-3b0c-49fe-8be4-6246e08b6c9a",
"targetHandle": "seed",
"id": "reactflow__edge-59349822-af20-4e0e-a53f-3ba135d00c3fvalue-280fd8a7-3b0c-49fe-8be4-6246e08b6c9aseed",
"type": "default"
}
]
}

View File

@ -26,6 +26,10 @@
{ {
"nodeId": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", "nodeId": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204",
"fieldName": "style" "fieldName": "style"
},
{
"nodeId": "87ee6243-fb0d-4f77-ad5f-56591659339e",
"fieldName": "steps"
} }
], ],
"meta": { "meta": {
@ -36,6 +40,7 @@
"id": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", "id": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204",
"type": "invocation", "type": "invocation",
"data": { "data": {
"version": "1.0.0",
"id": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", "id": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204",
"type": "sdxl_compel_prompt", "type": "sdxl_compel_prompt",
"inputs": { "inputs": {
@ -130,12 +135,10 @@
"isOpen": true, "isOpen": true,
"notes": "", "notes": "",
"embedWorkflow": false, "embedWorkflow": false,
"isIntermediate": true, "isIntermediate": true
"useCache": true,
"version": "1.0.0"
}, },
"width": 320, "width": 320,
"height": 793, "height": 764,
"position": { "position": {
"x": 1275, "x": 1275,
"y": -350 "y": -350
@ -145,6 +148,7 @@
"id": "55705012-79b9-4aac-9f26-c0b10309785b", "id": "55705012-79b9-4aac-9f26-c0b10309785b",
"type": "invocation", "type": "invocation",
"data": { "data": {
"version": "1.0.0",
"id": "55705012-79b9-4aac-9f26-c0b10309785b", "id": "55705012-79b9-4aac-9f26-c0b10309785b",
"type": "noise", "type": "noise",
"inputs": { "inputs": {
@ -205,9 +209,7 @@
"isOpen": false, "isOpen": false,
"notes": "", "notes": "",
"embedWorkflow": false, "embedWorkflow": false,
"isIntermediate": true, "isIntermediate": true
"useCache": true,
"version": "1.0.0"
}, },
"width": 320, "width": 320,
"height": 32, "height": 32,
@ -216,10 +218,83 @@
"y": -300 "y": -300
} }
}, },
{
"id": "dbcd2f98-d809-48c8-bf64-2635f88a2fe9",
"type": "invocation",
"data": {
"version": "1.0.0",
"id": "dbcd2f98-d809-48c8-bf64-2635f88a2fe9",
"type": "l2i",
"inputs": {
"tiled": {
"id": "24f5bc7b-f6a1-425d-8ab1-f50b4db5d0df",
"name": "tiled",
"type": "boolean",
"fieldKind": "input",
"label": "",
"value": false
},
"fp32": {
"id": "b146d873-ffb9-4767-986a-5360504841a2",
"name": "fp32",
"type": "boolean",
"fieldKind": "input",
"label": "",
"value": true
},
"latents": {
"id": "65441abd-7713-4b00-9d8d-3771404002e8",
"name": "latents",
"type": "LatentsField",
"fieldKind": "input",
"label": ""
},
"vae": {
"id": "a478b833-6e13-4611-9a10-842c89603c74",
"name": "vae",
"type": "VaeField",
"fieldKind": "input",
"label": ""
}
},
"outputs": {
"image": {
"id": "c87ae925-f858-417a-8940-8708ba9b4b53",
"name": "image",
"type": "ImageField",
"fieldKind": "output"
},
"width": {
"id": "4bcb8512-b5a1-45f1-9e52-6e92849f9d6c",
"name": "width",
"type": "integer",
"fieldKind": "output"
},
"height": {
"id": "23e41c00-a354-48e8-8f59-5875679c27ab",
"name": "height",
"type": "integer",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": true,
"isIntermediate": false
},
"width": 320,
"height": 224,
"position": {
"x": 2025,
"y": -250
}
},
{ {
"id": "ea94bc37-d995-4a83-aa99-4af42479f2f2", "id": "ea94bc37-d995-4a83-aa99-4af42479f2f2",
"type": "invocation", "type": "invocation",
"data": { "data": {
"version": "1.0.0",
"id": "ea94bc37-d995-4a83-aa99-4af42479f2f2", "id": "ea94bc37-d995-4a83-aa99-4af42479f2f2",
"type": "rand_int", "type": "rand_int",
"inputs": { "inputs": {
@ -252,9 +327,7 @@
"isOpen": false, "isOpen": false,
"notes": "", "notes": "",
"embedWorkflow": false, "embedWorkflow": false,
"isIntermediate": true, "isIntermediate": true
"useCache": false,
"version": "1.0.0"
}, },
"width": 320, "width": 320,
"height": 32, "height": 32,
@ -267,6 +340,7 @@
"id": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", "id": "30d3289c-773c-4152-a9d2-bd8a99c8fd22",
"type": "invocation", "type": "invocation",
"data": { "data": {
"version": "1.0.0",
"id": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", "id": "30d3289c-773c-4152-a9d2-bd8a99c8fd22",
"type": "sdxl_model_loader", "type": "sdxl_model_loader",
"inputs": { "inputs": {
@ -277,7 +351,7 @@
"fieldKind": "input", "fieldKind": "input",
"label": "", "label": "",
"value": { "value": {
"model_name": "stable-diffusion-xl-base-1-0", "model_name": "stable-diffusion-xl-base-1.0",
"base_model": "sdxl", "base_model": "sdxl",
"model_type": "main" "model_type": "main"
} }
@ -313,12 +387,10 @@
"isOpen": true, "isOpen": true,
"notes": "", "notes": "",
"embedWorkflow": false, "embedWorkflow": false,
"isIntermediate": true, "isIntermediate": true
"useCache": true,
"version": "1.0.0"
}, },
"width": 320, "width": 320,
"height": 258, "height": 234,
"position": { "position": {
"x": 475, "x": 475,
"y": 25 "y": 25
@ -328,6 +400,7 @@
"id": "faf965a4-7530-427b-b1f3-4ba6505c2a08", "id": "faf965a4-7530-427b-b1f3-4ba6505c2a08",
"type": "invocation", "type": "invocation",
"data": { "data": {
"version": "1.0.0",
"id": "faf965a4-7530-427b-b1f3-4ba6505c2a08", "id": "faf965a4-7530-427b-b1f3-4ba6505c2a08",
"type": "sdxl_compel_prompt", "type": "sdxl_compel_prompt",
"inputs": { "inputs": {
@ -422,143 +495,48 @@
"isOpen": true, "isOpen": true,
"notes": "", "notes": "",
"embedWorkflow": false, "embedWorkflow": false,
"isIntermediate": true, "isIntermediate": true
"useCache": true,
"version": "1.0.0"
}, },
"width": 320, "width": 320,
"height": 793, "height": 764,
"position": { "position": {
"x": 900, "x": 900,
"y": -350 "y": -350
} }
}, },
{ {
"id": "63e91020-83b2-4f35-b174-ad9692aabb48", "id": "87ee6243-fb0d-4f77-ad5f-56591659339e",
"type": "invocation", "type": "invocation",
"data": { "data": {
"id": "63e91020-83b2-4f35-b174-ad9692aabb48", "version": "1.0.0",
"type": "l2i", "id": "87ee6243-fb0d-4f77-ad5f-56591659339e",
"inputs": {
"metadata": {
"id": "88971324-3fdb-442d-b8b7-7612478a8622",
"name": "metadata",
"type": "MetadataField",
"fieldKind": "input",
"label": ""
},
"latents": {
"id": "da0e40cb-c49f-4fa5-9856-338b91a65f6b",
"name": "latents",
"type": "LatentsField",
"fieldKind": "input",
"label": ""
},
"vae": {
"id": "ae5164ce-1710-4ec5-a83a-6113a0d1b5c0",
"name": "vae",
"type": "VaeField",
"fieldKind": "input",
"label": ""
},
"tiled": {
"id": "2ccfd535-1a7b-4ecf-84db-9430a64fb3d7",
"name": "tiled",
"type": "boolean",
"fieldKind": "input",
"label": "",
"value": false
},
"fp32": {
"id": "64f07d5a-54a2-429c-8c5b-0c2a3a8e5cd5",
"name": "fp32",
"type": "boolean",
"fieldKind": "input",
"label": "",
"value": false
}
},
"outputs": {
"image": {
"id": "9b281eaa-6504-407d-a5ca-1e5e8020a4bf",
"name": "image",
"type": "ImageField",
"fieldKind": "output"
},
"width": {
"id": "98e545f3-b53b-490d-b94d-bed9418ccc75",
"name": "width",
"type": "integer",
"fieldKind": "output"
},
"height": {
"id": "4a74bd43-d7f7-4c7f-bb3b-d09bb2992c46",
"name": "height",
"type": "integer",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": false,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 267,
"position": {
"x": 2112.5626808057173,
"y": -174.24042139280238
}
},
{
"id": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb",
"type": "invocation",
"data": {
"id": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb",
"type": "denoise_latents", "type": "denoise_latents",
"inputs": { "inputs": {
"positive_conditioning": {
"id": "29b73dfa-a06e-4b4a-a844-515b9eb93a81",
"name": "positive_conditioning",
"type": "ConditioningField",
"fieldKind": "input",
"label": ""
},
"negative_conditioning": {
"id": "a81e6f5b-f4de-4919-b483-b6e2f067465a",
"name": "negative_conditioning",
"type": "ConditioningField",
"fieldKind": "input",
"label": ""
},
"noise": { "noise": {
"id": "4ba06bb7-eb45-4fb9-9984-31001b545587", "id": "4884a4b7-cc19-4fea-83c7-1f940e6edd24",
"name": "noise", "name": "noise",
"type": "LatentsField", "type": "LatentsField",
"fieldKind": "input", "fieldKind": "input",
"label": "" "label": ""
}, },
"steps": { "steps": {
"id": "36ee8a45-ca69-44bc-9bc3-aa881e6045c0", "id": "4c61675c-b6b9-41ac-b187-b5c13b587039",
"name": "steps", "name": "steps",
"type": "integer", "type": "integer",
"fieldKind": "input", "fieldKind": "input",
"label": "", "label": "",
"value": 10 "value": 36
}, },
"cfg_scale": { "cfg_scale": {
"id": "2a2024e0-a736-46ec-933c-c1c1ebe96943", "id": "f8213f35-4637-4a1a-83f4-1f8cfb9ccd2c",
"name": "cfg_scale", "name": "cfg_scale",
"type": "FloatPolymorphic", "type": "float",
"fieldKind": "input", "fieldKind": "input",
"label": "", "label": "",
"value": 7.5 "value": 7.5
}, },
"denoising_start": { "denoising_start": {
"id": "be219d5e-41b7-430a-8fb5-bc21a31ad219", "id": "01e2f30d-0acd-4e21-98b9-a9b8e24c6db2",
"name": "denoising_start", "name": "denoising_start",
"type": "float", "type": "float",
"fieldKind": "input", "fieldKind": "input",
@ -566,7 +544,7 @@
"value": 0 "value": 0
}, },
"denoising_end": { "denoising_end": {
"id": "3adfb7ae-c9f7-4a40-b6e0-4c2050bd1a99", "id": "3db95479-a73b-4c75-9b44-08daec16b224",
"name": "denoising_end", "name": "denoising_end",
"type": "float", "type": "float",
"fieldKind": "input", "fieldKind": "input",
@ -574,71 +552,71 @@
"value": 1 "value": 1
}, },
"scheduler": { "scheduler": {
"id": "14423e0d-7215-4ee0-b065-f9e95eaa8d7d", "id": "db8430a9-64c3-4c54-ae38-9f597cf7b6d5",
"name": "scheduler", "name": "scheduler",
"type": "Scheduler", "type": "Scheduler",
"fieldKind": "input", "fieldKind": "input",
"label": "", "label": "",
"value": "euler" "value": "euler"
}, },
"unet": {
"id": "e73bbf98-6489-492b-b83c-faed215febac",
"name": "unet",
"type": "UNetField",
"fieldKind": "input",
"label": ""
},
"control": { "control": {
"id": "dab351b3-0c86-4ea5-9782-4e8edbfb0607", "id": "599b49e8-6435-4576-be41-a5155f3a17e3",
"name": "control", "name": "control",
"type": "ControlPolymorphic", "type": "ControlField",
"fieldKind": "input",
"label": ""
},
"ip_adapter": {
"id": "192daea0-a90a-43cc-a2ee-0114a8e90318",
"name": "ip_adapter",
"type": "IPAdapterPolymorphic",
"fieldKind": "input",
"label": ""
},
"t2i_adapter": {
"id": "ee386a55-d4c7-48c1-ac57-7bc4e3aada7a",
"name": "t2i_adapter",
"type": "T2IAdapterPolymorphic",
"fieldKind": "input", "fieldKind": "input",
"label": "" "label": ""
}, },
"latents": { "latents": {
"id": "3a922c6a-3d8c-4c9e-b3ec-2f4d81cda077", "id": "226f9e91-454e-4159-9fa6-019c0cf29277",
"name": "latents", "name": "latents",
"type": "LatentsField", "type": "LatentsField",
"fieldKind": "input", "fieldKind": "input",
"label": "" "label": ""
}, },
"denoise_mask": { "denoise_mask": {
"id": "cd7ce032-835f-495f-8b45-d57272f33132", "id": "de019cb6-7fb5-45bf-a266-22e20889893f",
"name": "denoise_mask", "name": "denoise_mask",
"type": "DenoiseMaskField", "type": "DenoiseMaskField",
"fieldKind": "input", "fieldKind": "input",
"label": "" "label": ""
},
"positive_conditioning": {
"id": "02fc400a-110d-470e-8411-f404f966a949",
"name": "positive_conditioning",
"type": "ConditioningField",
"fieldKind": "input",
"label": ""
},
"negative_conditioning": {
"id": "4bd3bdfa-fcf4-42be-8e47-1e314255798f",
"name": "negative_conditioning",
"type": "ConditioningField",
"fieldKind": "input",
"label": ""
},
"unet": {
"id": "7c2d58a8-b5f1-4e63-8ffd-8ada52c35832",
"name": "unet",
"type": "UNetField",
"fieldKind": "input",
"label": ""
} }
}, },
"outputs": { "outputs": {
"latents": { "latents": {
"id": "6260b84f-8361-470a-98d8-5b22a45c2d8c", "id": "6a6fa492-de26-4e95-b1d9-a322fe37eb13",
"name": "latents", "name": "latents",
"type": "LatentsField", "type": "LatentsField",
"fieldKind": "output" "fieldKind": "output"
}, },
"width": { "width": {
"id": "aede0ecf-25b6-46be-aa30-b77f79715deb", "id": "a9790729-7d6c-4418-903d-4da961fccf56",
"name": "width", "name": "width",
"type": "integer", "type": "integer",
"fieldKind": "output" "fieldKind": "output"
}, },
"height": { "height": {
"id": "519abf62-d475-48ef-ab8f-66136bc0e499", "id": "fa74efe5-7330-4a3c-b256-c82a544585b4",
"name": "height", "name": "height",
"type": "integer", "type": "integer",
"fieldKind": "output" "fieldKind": "output"
@ -648,15 +626,13 @@
"isOpen": true, "isOpen": true,
"notes": "", "notes": "",
"embedWorkflow": false, "embedWorkflow": false,
"isIntermediate": true, "isIntermediate": true
"useCache": true,
"version": "1.4.0"
}, },
"width": 320, "width": 320,
"height": 646, "height": 558,
"position": { "position": {
"x": 1642.955772577545, "x": 1650,
"y": -230.2485847594651 "y": -250
} }
} }
], ],
@ -710,42 +686,50 @@
{ {
"source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", "source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22",
"sourceHandle": "vae", "sourceHandle": "vae",
"target": "63e91020-83b2-4f35-b174-ad9692aabb48", "target": "dbcd2f98-d809-48c8-bf64-2635f88a2fe9",
"targetHandle": "vae", "targetHandle": "vae",
"id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22vae-63e91020-83b2-4f35-b174-ad9692aabb48vae", "id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22vae-dbcd2f98-d809-48c8-bf64-2635f88a2fe9vae",
"type": "default" "type": "default"
}, },
{ {
"source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", "source": "87ee6243-fb0d-4f77-ad5f-56591659339e",
"sourceHandle": "unet", "sourceHandle": "latents",
"target": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb", "target": "dbcd2f98-d809-48c8-bf64-2635f88a2fe9",
"targetHandle": "unet", "targetHandle": "latents",
"id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22unet-50a36525-3c0a-4cc5-977c-e4bfc3fd6dfbunet", "id": "reactflow__edge-87ee6243-fb0d-4f77-ad5f-56591659339elatents-dbcd2f98-d809-48c8-bf64-2635f88a2fe9latents",
"type": "default" "type": "default"
}, },
{ {
"source": "faf965a4-7530-427b-b1f3-4ba6505c2a08", "source": "faf965a4-7530-427b-b1f3-4ba6505c2a08",
"sourceHandle": "conditioning", "sourceHandle": "conditioning",
"target": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb", "target": "87ee6243-fb0d-4f77-ad5f-56591659339e",
"targetHandle": "positive_conditioning", "targetHandle": "positive_conditioning",
"id": "reactflow__edge-faf965a4-7530-427b-b1f3-4ba6505c2a08conditioning-50a36525-3c0a-4cc5-977c-e4bfc3fd6dfbpositive_conditioning", "id": "reactflow__edge-faf965a4-7530-427b-b1f3-4ba6505c2a08conditioning-87ee6243-fb0d-4f77-ad5f-56591659339epositive_conditioning",
"type": "default" "type": "default"
}, },
{ {
"source": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", "source": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204",
"sourceHandle": "conditioning", "sourceHandle": "conditioning",
"target": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb", "target": "87ee6243-fb0d-4f77-ad5f-56591659339e",
"targetHandle": "negative_conditioning", "targetHandle": "negative_conditioning",
"id": "reactflow__edge-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204conditioning-50a36525-3c0a-4cc5-977c-e4bfc3fd6dfbnegative_conditioning", "id": "reactflow__edge-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204conditioning-87ee6243-fb0d-4f77-ad5f-56591659339enegative_conditioning",
"type": "default"
},
{
"source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22",
"sourceHandle": "unet",
"target": "87ee6243-fb0d-4f77-ad5f-56591659339e",
"targetHandle": "unet",
"id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22unet-87ee6243-fb0d-4f77-ad5f-56591659339eunet",
"type": "default" "type": "default"
}, },
{ {
"source": "55705012-79b9-4aac-9f26-c0b10309785b", "source": "55705012-79b9-4aac-9f26-c0b10309785b",
"sourceHandle": "noise", "sourceHandle": "noise",
"target": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb", "target": "87ee6243-fb0d-4f77-ad5f-56591659339e",
"targetHandle": "noise", "targetHandle": "noise",
"id": "reactflow__edge-55705012-79b9-4aac-9f26-c0b10309785bnoise-50a36525-3c0a-4cc5-977c-e4bfc3fd6dfbnoise", "id": "reactflow__edge-55705012-79b9-4aac-9f26-c0b10309785bnoise-87ee6243-fb0d-4f77-ad5f-56591659339enoise",
"type": "default" "type": "default"
} }
] ]
} }

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,10 @@
{ {
"nodeId": "93dc02a4-d05b-48ed-b99c-c9b616af3402", "nodeId": "93dc02a4-d05b-48ed-b99c-c9b616af3402",
"fieldName": "prompt" "fieldName": "prompt"
},
{
"nodeId": "75899702-fa44-46d2-b2d5-3e17f234c3e7",
"fieldName": "steps"
} }
], ],
"meta": { "meta": {
@ -28,6 +32,7 @@
"id": "93dc02a4-d05b-48ed-b99c-c9b616af3402", "id": "93dc02a4-d05b-48ed-b99c-c9b616af3402",
"type": "invocation", "type": "invocation",
"data": { "data": {
"version": "1.0.0",
"id": "93dc02a4-d05b-48ed-b99c-c9b616af3402", "id": "93dc02a4-d05b-48ed-b99c-c9b616af3402",
"type": "compel", "type": "compel",
"inputs": { "inputs": {
@ -59,21 +64,20 @@
"isOpen": true, "isOpen": true,
"notes": "", "notes": "",
"embedWorkflow": false, "embedWorkflow": false,
"isIntermediate": true, "isIntermediate": true
"useCache": true,
"version": "1.0.0"
}, },
"width": 320, "width": 320,
"height": 261, "height": 235,
"position": { "position": {
"x": 995.7263915923627, "x": 1400,
"y": 239.67783573351227 "y": -75
} }
}, },
{ {
"id": "55705012-79b9-4aac-9f26-c0b10309785b", "id": "55705012-79b9-4aac-9f26-c0b10309785b",
"type": "invocation", "type": "invocation",
"data": { "data": {
"version": "1.0.0",
"id": "55705012-79b9-4aac-9f26-c0b10309785b", "id": "55705012-79b9-4aac-9f26-c0b10309785b",
"type": "noise", "type": "noise",
"inputs": { "inputs": {
@ -134,21 +138,92 @@
"isOpen": true, "isOpen": true,
"notes": "", "notes": "",
"embedWorkflow": false, "embedWorkflow": false,
"isIntermediate": true, "isIntermediate": true
"useCache": true,
"version": "1.0.0"
}, },
"width": 320, "width": 320,
"height": 389, "height": 364,
"position": { "position": {
"x": 993.4442117555518, "x": 1000,
"y": 605.6757415334787 "y": 350
}
},
{
"id": "dbcd2f98-d809-48c8-bf64-2635f88a2fe9",
"type": "invocation",
"data": {
"version": "1.0.0",
"id": "dbcd2f98-d809-48c8-bf64-2635f88a2fe9",
"type": "l2i",
"inputs": {
"tiled": {
"id": "24f5bc7b-f6a1-425d-8ab1-f50b4db5d0df",
"name": "tiled",
"type": "boolean",
"fieldKind": "input",
"label": "",
"value": false
},
"fp32": {
"id": "b146d873-ffb9-4767-986a-5360504841a2",
"name": "fp32",
"type": "boolean",
"fieldKind": "input",
"label": "",
"value": false
},
"latents": {
"id": "65441abd-7713-4b00-9d8d-3771404002e8",
"name": "latents",
"type": "LatentsField",
"fieldKind": "input",
"label": ""
},
"vae": {
"id": "a478b833-6e13-4611-9a10-842c89603c74",
"name": "vae",
"type": "VaeField",
"fieldKind": "input",
"label": ""
}
},
"outputs": {
"image": {
"id": "c87ae925-f858-417a-8940-8708ba9b4b53",
"name": "image",
"type": "ImageField",
"fieldKind": "output"
},
"width": {
"id": "4bcb8512-b5a1-45f1-9e52-6e92849f9d6c",
"name": "width",
"type": "integer",
"fieldKind": "output"
},
"height": {
"id": "23e41c00-a354-48e8-8f59-5875679c27ab",
"name": "height",
"type": "integer",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": true,
"isIntermediate": false
},
"width": 320,
"height": 266,
"position": {
"x": 1800,
"y": 200
} }
}, },
{ {
"id": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8", "id": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8",
"type": "invocation", "type": "invocation",
"data": { "data": {
"version": "1.0.0",
"id": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8", "id": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8",
"type": "main_model_loader", "type": "main_model_loader",
"inputs": { "inputs": {
@ -186,24 +261,23 @@
} }
}, },
"label": "", "label": "",
"isOpen": true, "isOpen": false,
"notes": "", "notes": "",
"embedWorkflow": false, "embedWorkflow": false,
"isIntermediate": true, "isIntermediate": true
"useCache": true,
"version": "1.0.0"
}, },
"width": 320, "width": 320,
"height": 226, "height": 32,
"position": { "position": {
"x": 163.04436745878343, "x": 1000,
"y": 254.63156870373479 "y": 200
} }
}, },
{ {
"id": "7d8bf987-284f-413a-b2fd-d825445a5d6c", "id": "7d8bf987-284f-413a-b2fd-d825445a5d6c",
"type": "invocation", "type": "invocation",
"data": { "data": {
"version": "1.0.0",
"id": "7d8bf987-284f-413a-b2fd-d825445a5d6c", "id": "7d8bf987-284f-413a-b2fd-d825445a5d6c",
"type": "compel", "type": "compel",
"inputs": { "inputs": {
@ -235,21 +309,20 @@
"isOpen": true, "isOpen": true,
"notes": "", "notes": "",
"embedWorkflow": false, "embedWorkflow": false,
"isIntermediate": true, "isIntermediate": true
"useCache": true,
"version": "1.0.0"
}, },
"width": 320, "width": 320,
"height": 261, "height": 235,
"position": { "position": {
"x": 595.7263915923627, "x": 1000,
"y": 239.67783573351227 "y": -75
} }
}, },
{ {
"id": "ea94bc37-d995-4a83-aa99-4af42479f2f2", "id": "ea94bc37-d995-4a83-aa99-4af42479f2f2",
"type": "invocation", "type": "invocation",
"data": { "data": {
"version": "1.0.0",
"id": "ea94bc37-d995-4a83-aa99-4af42479f2f2", "id": "ea94bc37-d995-4a83-aa99-4af42479f2f2",
"type": "rand_int", "type": "rand_int",
"inputs": { "inputs": {
@ -279,66 +352,51 @@
} }
}, },
"label": "Random Seed", "label": "Random Seed",
"isOpen": true, "isOpen": false,
"notes": "", "notes": "",
"embedWorkflow": false, "embedWorkflow": false,
"isIntermediate": true, "isIntermediate": true
"useCache": false,
"version": "1.0.0"
}, },
"width": 320, "width": 320,
"height": 218, "height": 32,
"position": { "position": {
"x": 541.094822888628, "x": 1000,
"y": 694.5704476446829 "y": 275
} }
}, },
{ {
"id": "eea2702a-19fb-45b5-9d75-56b4211ec03c", "id": "75899702-fa44-46d2-b2d5-3e17f234c3e7",
"type": "invocation", "type": "invocation",
"data": { "data": {
"id": "eea2702a-19fb-45b5-9d75-56b4211ec03c", "version": "1.0.0",
"id": "75899702-fa44-46d2-b2d5-3e17f234c3e7",
"type": "denoise_latents", "type": "denoise_latents",
"inputs": { "inputs": {
"positive_conditioning": {
"id": "90b7f4f8-ada7-4028-8100-d2e54f192052",
"name": "positive_conditioning",
"type": "ConditioningField",
"fieldKind": "input",
"label": ""
},
"negative_conditioning": {
"id": "9393779e-796c-4f64-b740-902a1177bf53",
"name": "negative_conditioning",
"type": "ConditioningField",
"fieldKind": "input",
"label": ""
},
"noise": { "noise": {
"id": "8e17f1e5-4f98-40b1-b7f4-86aeeb4554c1", "id": "8b18f3eb-40d2-45c1-9a9d-28d6af0dce2b",
"name": "noise", "name": "noise",
"type": "LatentsField", "type": "LatentsField",
"fieldKind": "input", "fieldKind": "input",
"label": "" "label": ""
}, },
"steps": { "steps": {
"id": "9b63302d-6bd2-42c9-ac13-9b1afb51af88", "id": "0be4373c-46f3-441c-80a7-a4bb6ceb498c",
"name": "steps", "name": "steps",
"type": "integer", "type": "integer",
"fieldKind": "input", "fieldKind": "input",
"label": "", "label": "",
"value": 10 "value": 36
}, },
"cfg_scale": { "cfg_scale": {
"id": "87dd04d3-870e-49e1-98bf-af003a810109", "id": "107267ce-4666-4cd7-94b3-7476b7973ae9",
"name": "cfg_scale", "name": "cfg_scale",
"type": "FloatPolymorphic", "type": "float",
"fieldKind": "input", "fieldKind": "input",
"label": "", "label": "",
"value": 7.5 "value": 7.5
}, },
"denoising_start": { "denoising_start": {
"id": "f369d80f-4931-4740-9bcd-9f0620719fab", "id": "d2ce9f0f-5fc2-48b2-b917-53442941e9a1",
"name": "denoising_start", "name": "denoising_start",
"type": "float", "type": "float",
"fieldKind": "input", "fieldKind": "input",
@ -346,7 +404,7 @@
"value": 0 "value": 0
}, },
"denoising_end": { "denoising_end": {
"id": "747d10e5-6f02-445c-994c-0604d814de8c", "id": "8ad51505-b8d0-422a-beb8-96fc6fc6b65f",
"name": "denoising_end", "name": "denoising_end",
"type": "float", "type": "float",
"fieldKind": "input", "fieldKind": "input",
@ -354,71 +412,71 @@
"value": 1 "value": 1
}, },
"scheduler": { "scheduler": {
"id": "1de84a4e-3a24-4ec8-862b-16ce49633b9b", "id": "53092874-a43b-4623-91a2-76e62fdb1f2e",
"name": "scheduler", "name": "scheduler",
"type": "Scheduler", "type": "Scheduler",
"fieldKind": "input", "fieldKind": "input",
"label": "", "label": "",
"value": "euler" "value": "euler"
}, },
"unet": {
"id": "ffa6fef4-3ce2-4bdb-9296-9a834849489b",
"name": "unet",
"type": "UNetField",
"fieldKind": "input",
"label": ""
},
"control": { "control": {
"id": "077b64cb-34be-4fcc-83f2-e399807a02bd", "id": "7abe57cc-469d-437e-ad72-a18efa28215f",
"name": "control", "name": "control",
"type": "ControlPolymorphic", "type": "ControlField",
"fieldKind": "input",
"label": ""
},
"ip_adapter": {
"id": "1d6948f7-3a65-4a65-a20c-768b287251aa",
"name": "ip_adapter",
"type": "IPAdapterPolymorphic",
"fieldKind": "input",
"label": ""
},
"t2i_adapter": {
"id": "75e67b09-952f-4083-aaf4-6b804d690412",
"name": "t2i_adapter",
"type": "T2IAdapterPolymorphic",
"fieldKind": "input", "fieldKind": "input",
"label": "" "label": ""
}, },
"latents": { "latents": {
"id": "334d4ba3-5a99-4195-82c5-86fb3f4f7d43", "id": "add8bbe5-14d0-42d4-a867-9c65ab8dd129",
"name": "latents", "name": "latents",
"type": "LatentsField", "type": "LatentsField",
"fieldKind": "input", "fieldKind": "input",
"label": "" "label": ""
}, },
"denoise_mask": { "denoise_mask": {
"id": "0d3dbdbf-b014-4e95-8b18-ff2ff9cb0bfa", "id": "f373a190-0fc8-45b7-ae62-c4aa8e9687e1",
"name": "denoise_mask", "name": "denoise_mask",
"type": "DenoiseMaskField", "type": "DenoiseMaskField",
"fieldKind": "input", "fieldKind": "input",
"label": "" "label": ""
},
"positive_conditioning": {
"id": "c7160303-8a23-4f15-9197-855d48802a7f",
"name": "positive_conditioning",
"type": "ConditioningField",
"fieldKind": "input",
"label": ""
},
"negative_conditioning": {
"id": "fd750efa-1dfc-4d0b-accb-828e905ba320",
"name": "negative_conditioning",
"type": "ConditioningField",
"fieldKind": "input",
"label": ""
},
"unet": {
"id": "af1f41ba-ce2a-4314-8d7f-494bb5800381",
"name": "unet",
"type": "UNetField",
"fieldKind": "input",
"label": ""
} }
}, },
"outputs": { "outputs": {
"latents": { "latents": {
"id": "70fa5bbc-0c38-41bb-861a-74d6d78d2f38", "id": "8508d04d-f999-4a44-94d0-388ab1401d27",
"name": "latents", "name": "latents",
"type": "LatentsField", "type": "LatentsField",
"fieldKind": "output" "fieldKind": "output"
}, },
"width": { "width": {
"id": "98ee0e6c-82aa-4e8f-8be5-dc5f00ee47f0", "id": "93dc8287-0a2a-4320-83a4-5e994b7ba23e",
"name": "width", "name": "width",
"type": "integer", "type": "integer",
"fieldKind": "output" "fieldKind": "output"
}, },
"height": { "height": {
"id": "e8cb184a-5e1a-47c8-9695-4b8979564f5d", "id": "d9862f5c-0ab5-46fa-8c29-5059bb581d96",
"name": "height", "name": "height",
"type": "integer", "type": "integer",
"fieldKind": "output" "fieldKind": "output"
@ -428,95 +486,13 @@
"isOpen": true, "isOpen": true,
"notes": "", "notes": "",
"embedWorkflow": false, "embedWorkflow": false,
"isIntermediate": true, "isIntermediate": true
"useCache": true,
"version": "1.4.0"
}, },
"width": 320, "width": 320,
"height": 646, "height": 558,
"position": { "position": {
"x": 1476.5794704734735, "x": 1400,
"y": 256.80174342731783 "y": 200
}
},
{
"id": "58c957f5-0d01-41fc-a803-b2bbf0413d4f",
"type": "invocation",
"data": {
"id": "58c957f5-0d01-41fc-a803-b2bbf0413d4f",
"type": "l2i",
"inputs": {
"metadata": {
"id": "ab375f12-0042-4410-9182-29e30db82c85",
"name": "metadata",
"type": "MetadataField",
"fieldKind": "input",
"label": ""
},
"latents": {
"id": "3a7e7efd-bff5-47d7-9d48-615127afee78",
"name": "latents",
"type": "LatentsField",
"fieldKind": "input",
"label": ""
},
"vae": {
"id": "a1f5f7a1-0795-4d58-b036-7820c0b0ef2b",
"name": "vae",
"type": "VaeField",
"fieldKind": "input",
"label": ""
},
"tiled": {
"id": "da52059a-0cee-4668-942f-519aa794d739",
"name": "tiled",
"type": "boolean",
"fieldKind": "input",
"label": "",
"value": false
},
"fp32": {
"id": "c4841df3-b24e-4140-be3b-ccd454c2522c",
"name": "fp32",
"type": "boolean",
"fieldKind": "input",
"label": "",
"value": false
}
},
"outputs": {
"image": {
"id": "72d667d0-cf85-459d-abf2-28bd8b823fe7",
"name": "image",
"type": "ImageField",
"fieldKind": "output"
},
"width": {
"id": "c8c907d8-1066-49d1-b9a6-83bdcd53addc",
"name": "width",
"type": "integer",
"fieldKind": "output"
},
"height": {
"id": "230f359c-b4ea-436c-b372-332d7dcdca85",
"name": "height",
"type": "integer",
"fieldKind": "output"
}
},
"label": "",
"isOpen": true,
"notes": "",
"embedWorkflow": false,
"isIntermediate": false,
"useCache": true,
"version": "1.0.0"
},
"width": 320,
"height": 267,
"position": {
"x": 2037.9648469717395,
"y": 426.10844427600136
} }
} }
], ],
@ -546,52 +522,52 @@
"type": "default" "type": "default"
}, },
{ {
"source": "55705012-79b9-4aac-9f26-c0b10309785b", "source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8",
"sourceHandle": "noise", "sourceHandle": "vae",
"target": "eea2702a-19fb-45b5-9d75-56b4211ec03c", "target": "dbcd2f98-d809-48c8-bf64-2635f88a2fe9",
"targetHandle": "noise", "targetHandle": "vae",
"id": "reactflow__edge-55705012-79b9-4aac-9f26-c0b10309785bnoise-eea2702a-19fb-45b5-9d75-56b4211ec03cnoise", "id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8vae-dbcd2f98-d809-48c8-bf64-2635f88a2fe9vae",
"type": "default"
},
{
"source": "75899702-fa44-46d2-b2d5-3e17f234c3e7",
"sourceHandle": "latents",
"target": "dbcd2f98-d809-48c8-bf64-2635f88a2fe9",
"targetHandle": "latents",
"id": "reactflow__edge-75899702-fa44-46d2-b2d5-3e17f234c3e7latents-dbcd2f98-d809-48c8-bf64-2635f88a2fe9latents",
"type": "default" "type": "default"
}, },
{ {
"source": "7d8bf987-284f-413a-b2fd-d825445a5d6c", "source": "7d8bf987-284f-413a-b2fd-d825445a5d6c",
"sourceHandle": "conditioning", "sourceHandle": "conditioning",
"target": "eea2702a-19fb-45b5-9d75-56b4211ec03c", "target": "75899702-fa44-46d2-b2d5-3e17f234c3e7",
"targetHandle": "positive_conditioning", "targetHandle": "positive_conditioning",
"id": "reactflow__edge-7d8bf987-284f-413a-b2fd-d825445a5d6cconditioning-eea2702a-19fb-45b5-9d75-56b4211ec03cpositive_conditioning", "id": "reactflow__edge-7d8bf987-284f-413a-b2fd-d825445a5d6cconditioning-75899702-fa44-46d2-b2d5-3e17f234c3e7positive_conditioning",
"type": "default" "type": "default"
}, },
{ {
"source": "93dc02a4-d05b-48ed-b99c-c9b616af3402", "source": "93dc02a4-d05b-48ed-b99c-c9b616af3402",
"sourceHandle": "conditioning", "sourceHandle": "conditioning",
"target": "eea2702a-19fb-45b5-9d75-56b4211ec03c", "target": "75899702-fa44-46d2-b2d5-3e17f234c3e7",
"targetHandle": "negative_conditioning", "targetHandle": "negative_conditioning",
"id": "reactflow__edge-93dc02a4-d05b-48ed-b99c-c9b616af3402conditioning-eea2702a-19fb-45b5-9d75-56b4211ec03cnegative_conditioning", "id": "reactflow__edge-93dc02a4-d05b-48ed-b99c-c9b616af3402conditioning-75899702-fa44-46d2-b2d5-3e17f234c3e7negative_conditioning",
"type": "default" "type": "default"
}, },
{ {
"source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8", "source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8",
"sourceHandle": "unet", "sourceHandle": "unet",
"target": "eea2702a-19fb-45b5-9d75-56b4211ec03c", "target": "75899702-fa44-46d2-b2d5-3e17f234c3e7",
"targetHandle": "unet", "targetHandle": "unet",
"id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8unet-eea2702a-19fb-45b5-9d75-56b4211ec03cunet", "id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8unet-75899702-fa44-46d2-b2d5-3e17f234c3e7unet",
"type": "default" "type": "default"
}, },
{ {
"source": "eea2702a-19fb-45b5-9d75-56b4211ec03c", "source": "55705012-79b9-4aac-9f26-c0b10309785b",
"sourceHandle": "latents", "sourceHandle": "noise",
"target": "58c957f5-0d01-41fc-a803-b2bbf0413d4f", "target": "75899702-fa44-46d2-b2d5-3e17f234c3e7",
"targetHandle": "latents", "targetHandle": "noise",
"id": "reactflow__edge-eea2702a-19fb-45b5-9d75-56b4211ec03clatents-58c957f5-0d01-41fc-a803-b2bbf0413d4flatents", "id": "reactflow__edge-55705012-79b9-4aac-9f26-c0b10309785bnoise-75899702-fa44-46d2-b2d5-3e17f234c3e7noise",
"type": "default"
},
{
"source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8",
"sourceHandle": "vae",
"target": "58c957f5-0d01-41fc-a803-b2bbf0413d4f",
"targetHandle": "vae",
"id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8vae-58c957f5-0d01-41fc-a803-b2bbf0413d4fvae",
"type": "default" "type": "default"
} }
] ]
} }

View File

@ -1,7 +1,7 @@
@echo off @echo off
setlocal EnableExtensions EnableDelayedExpansion setlocal EnableExtensions EnableDelayedExpansion
@rem This script requires the user to install Python 3.10 or higher. All other @rem This script requires the user to install Python 3.9 or higher. All other
@rem requirements are downloaded as needed. @rem requirements are downloaded as needed.
@rem change to the script's directory @rem change to the script's directory
@ -19,7 +19,7 @@ set INVOKEAI_VERSION=latest
set INSTRUCTIONS=https://invoke-ai.github.io/InvokeAI/installation/INSTALL_AUTOMATED/ set INSTRUCTIONS=https://invoke-ai.github.io/InvokeAI/installation/INSTALL_AUTOMATED/
set TROUBLESHOOTING=https://invoke-ai.github.io/InvokeAI/installation/INSTALL_AUTOMATED/#troubleshooting set TROUBLESHOOTING=https://invoke-ai.github.io/InvokeAI/installation/INSTALL_AUTOMATED/#troubleshooting
set PYTHON_URL=https://www.python.org/downloads/windows/ set PYTHON_URL=https://www.python.org/downloads/windows/
set MINIMUM_PYTHON_VERSION=3.10.0 set MINIMUM_PYTHON_VERSION=3.9.0
set PYTHON_URL=https://www.python.org/downloads/release/python-3109/ set PYTHON_URL=https://www.python.org/downloads/release/python-3109/
set err_msg=An error has occurred and the script could not continue. set err_msg=An error has occurred and the script could not continue.
@ -28,7 +28,8 @@ set err_msg=An error has occurred and the script could not continue.
echo This script will install InvokeAI and its dependencies. echo This script will install InvokeAI and its dependencies.
echo. echo.
echo BEFORE YOU START PLEASE MAKE SURE TO DO THE FOLLOWING echo BEFORE YOU START PLEASE MAKE SURE TO DO THE FOLLOWING
echo 1. Install python 3.10 or 3.11. Python version 3.9 is no longer supported. echo 1. Install python 3.9 or 3.10. Python version 3.11 and above are
echo not supported at the moment.
echo 2. Double-click on the file WinLongPathsEnabled.reg in order to echo 2. Double-click on the file WinLongPathsEnabled.reg in order to
echo enable long path support on your system. echo enable long path support on your system.
echo 3. Install the Visual C++ core libraries. echo 3. Install the Visual C++ core libraries.
@ -45,19 +46,19 @@ echo ***** Checking and Updating Python *****
call python --version >.tmp1 2>.tmp2 call python --version >.tmp1 2>.tmp2
if %errorlevel% == 1 ( if %errorlevel% == 1 (
set err_msg=Please install Python 3.10-11. See %INSTRUCTIONS% for details. set err_msg=Please install Python 3.10. See %INSTRUCTIONS% for details.
goto err_exit goto err_exit
) )
for /f "tokens=2" %%i in (.tmp1) do set python_version=%%i for /f "tokens=2" %%i in (.tmp1) do set python_version=%%i
if "%python_version%" == "" ( if "%python_version%" == "" (
set err_msg=No python was detected on your system. Please install Python version %MINIMUM_PYTHON_VERSION% or higher. We recommend Python 3.10.12 from %PYTHON_URL% set err_msg=No python was detected on your system. Please install Python version %MINIMUM_PYTHON_VERSION% or higher. We recommend Python 3.10.9 from %PYTHON_URL%
goto err_exit goto err_exit
) )
call :compareVersions %MINIMUM_PYTHON_VERSION% %python_version% call :compareVersions %MINIMUM_PYTHON_VERSION% %python_version%
if %errorlevel% == 1 ( if %errorlevel% == 1 (
set err_msg=Your version of Python is too low. You need at least %MINIMUM_PYTHON_VERSION% but you have %python_version%. We recommend Python 3.10.12 from %PYTHON_URL% set err_msg=Your version of Python is too low. You need at least %MINIMUM_PYTHON_VERSION% but you have %python_version%. We recommend Python 3.10.9 from %PYTHON_URL%
goto err_exit goto err_exit
) )

View File

@ -8,10 +8,10 @@ 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.10.0 MINIMUM_PYTHON_VERSION=3.9.0
MAXIMUM_PYTHON_VERSION=3.11.100 MAXIMUM_PYTHON_VERSION=3.11.100
PYTHON="" PYTHON=""
for candidate in python3.11 python3.10 python3 python ; do for candidate in python3.11 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 # 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 # we check that this found executable can actually run

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.10.0,<=3.11.100" SUPPORTED_PYTHON = ">=3.9.0,<=3.11.100"
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"
@ -67,6 +67,7 @@ class Installer:
# Cleaning up temporary directories on Windows results in a race condition # Cleaning up temporary directories on Windows results in a race condition
# and a stack trace. # and a stack trace.
# `ignore_cleanup_errors` was only added in Python 3.10 # `ignore_cleanup_errors` was only added in Python 3.10
# users of Python 3.9 will see a gnarly stack trace on installer exit
if OS == "Windows" and int(platform.python_version_tuple()[1]) >= 10: if OS == "Windows" and int(platform.python_version_tuple()[1]) >= 10:
venv_dir = TemporaryDirectory(prefix=BOOTSTRAP_VENV_PREFIX, ignore_cleanup_errors=True) venv_dir = TemporaryDirectory(prefix=BOOTSTRAP_VENV_PREFIX, ignore_cleanup_errors=True)
else: else:
@ -138,6 +139,13 @@ class Installer:
except shutil.SameFileError: except shutil.SameFileError:
venv.create(venv_dir, with_pip=True, symlinks=True) venv.create(venv_dir, with_pip=True, symlinks=True)
# upgrade pip in Python 3.9 environments
if int(platform.python_version_tuple()[1]) == 9:
from plumbum import FG, local
pip = local[get_pip_from_venv(venv_dir)]
pip["install", "--upgrade", "pip"] & FG
return venv_dir return venv_dir
def install( def install(
@ -244,7 +252,7 @@ class InvokeAiInstance:
"numpy~=1.24.0", # choose versions that won't be uninstalled during phase 2 "numpy~=1.24.0", # choose versions that won't be uninstalled during phase 2
"urllib3~=1.26.0", "urllib3~=1.26.0",
"requests~=2.28.0", "requests~=2.28.0",
"torch~=2.1.0", "torch~=2.0.0",
"torchmetrics==0.11.4", "torchmetrics==0.11.4",
"torchvision>=0.14.1", "torchvision>=0.14.1",
"--force-reinstall", "--force-reinstall",
@ -324,7 +332,6 @@ class InvokeAiInstance:
Configure the InvokeAI runtime directory Configure the InvokeAI runtime directory
""" """
auto_install = False
# 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)):
@ -333,17 +340,13 @@ class InvokeAiInstance:
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"]:
auto_install = True new_argv.append(el)
sys.argv = new_argv sys.argv = new_argv
import messages
import requests # to catch download exceptions import requests # to catch download exceptions
from messages import introduction
auto_install = auto_install or messages.user_wants_auto_configuration() introduction()
if auto_install:
sys.argv.append("--yes")
else:
messages.introduction()
from invokeai.frontend.install.invokeai_configure import invokeai_configure from invokeai.frontend.install.invokeai_configure import invokeai_configure
@ -460,10 +463,10 @@ def get_torch_source() -> (Union[str, None], str):
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/cu121" url = "https://download.pytorch.org/whl/cu118"
optional_modules = "[xformers,onnx-cuda]" optional_modules = "[xformers,onnx-cuda]"
if device == "cuda_and_dml": if device == "cuda_and_dml":
url = "https://download.pytorch.org/whl/cu121" url = "https://download.pytorch.org/whl/cu118"
optional_modules = "[xformers,onnx-directml]" 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

@ -7,7 +7,7 @@ import os
import platform import platform
from pathlib import Path from pathlib import Path
from prompt_toolkit import HTML, prompt from prompt_toolkit import prompt
from prompt_toolkit.completion import PathCompleter from prompt_toolkit.completion import PathCompleter
from prompt_toolkit.validation import Validator from prompt_toolkit.validation import Validator
from rich import box, print from rich import box, print
@ -65,50 +65,17 @@ 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:")
dest_confirmed = Confirm.ask( dest_confirmed = Confirm.ask(
":stop_sign: (re)install in this location?", ":stop_sign: Are you sure you want to (re)install in this location?",
default=False, default=False,
) )
else: else:
print(f"InvokeAI will be installed in {dest}") print(f"InvokeAI will be installed in {dest}")
dest_confirmed = Confirm.ask("Use this location?", default=True) dest_confirmed = not Confirm.ask("Would you like to pick a different location?", default=False)
console.line() console.line()
return dest_confirmed return dest_confirmed
def user_wants_auto_configuration() -> bool:
"""Prompt the user to choose between manual and auto configuration."""
console.rule("InvokeAI Configuration Section")
console.print(
Panel(
Group(
"\n".join(
[
"Libraries are installed and InvokeAI will now set up its root directory and configuration. Choose between:",
"",
" * AUTOMATIC configuration: install reasonable defaults and a minimal set of starter models.",
" * MANUAL configuration: manually inspect and adjust configuration options and pick from a larger set of starter models.",
"",
"Later you can fine tune your configuration by selecting option [6] 'Change InvokeAI startup options' from the invoke.bat/invoke.sh launcher script.",
]
),
),
box=box.MINIMAL,
padding=(1, 1),
)
)
choice = (
prompt(
HTML("Choose <b>&lt;a&gt;</b>utomatic or <b>&lt;m&gt;</b>anual configuration [a/m] (a): "),
validator=Validator.from_callable(
lambda n: n == "" or n.startswith(("a", "A", "m", "M")), error_message="Please select 'a' or 'm'"
),
)
or "a"
)
return choice.lower().startswith("a")
def dest_path(dest=None) -> Path: def dest_path(dest=None) -> Path:
""" """
Prompt the user for the destination path and create the path Prompt the user for the destination path and create the path
@ -137,7 +104,7 @@ def dest_path(dest=None) -> Path:
path_completer = PathCompleter( path_completer = PathCompleter(
only_directories=True, only_directories=True,
expanduser=True, expanduser=True,
get_paths=lambda: [browse_start], # noqa: B023 get_paths=lambda: [browse_start],
# get_paths=lambda: [".."].extend(list(browse_start.iterdir())) # get_paths=lambda: [".."].extend(list(browse_start.iterdir()))
) )
@ -149,7 +116,7 @@ def dest_path(dest=None) -> Path:
completer=path_completer, completer=path_completer,
default=str(browse_start) + os.sep, default=str(browse_start) + os.sep,
vi_mode=True, vi_mode=True,
complete_while_typing=True, complete_while_typing=True
# Test that this is not needed on Windows # Test that this is not needed on Windows
# complete_style=CompleteStyle.READLINE_LIKE, # complete_style=CompleteStyle.READLINE_LIKE,
) )

View File

@ -4,7 +4,7 @@ Project homepage: https://github.com/invoke-ai/InvokeAI
Preparations: Preparations:
You will need to install Python 3.10 or higher for this installer You will need to install Python 3.9 or higher for this installer
to work. Instructions are given here: to work. Instructions are given here:
https://invoke-ai.github.io/InvokeAI/installation/INSTALL_AUTOMATED/ https://invoke-ai.github.io/InvokeAI/installation/INSTALL_AUTOMATED/
@ -14,15 +14,15 @@ Preparations:
python --version python --version
If all is well, it will print "Python 3.X.X", where the version number If all is well, it will print "Python 3.X.X", where the version number
is at least 3.10.*, and not higher than 3.11.*. is at least 3.9.*, and not higher than 3.11.*.
If this works, check the version of the Python package manager, pip: If this works, check the version of the Python package manager, pip:
pip --version pip --version
You should get a message that indicates that the pip package You should get a message that indicates that the pip package
installer was derived from Python 3.10 or 3.11. For example: installer was derived from Python 3.9 or 3.10. For example:
"pip 22.0.1 from /usr/bin/pip (python 3.10)" "pip 22.3.1 from /usr/bin/pip (python 3.9)"
Long Paths on Windows: Long Paths on Windows:

View File

@ -9,14 +9,14 @@ set INVOKEAI_ROOT=.
:start :start
echo Desired action: echo Desired action:
echo 1. Generate images with the browser-based interface echo 1. Generate images with the browser-based interface
echo 2. Run textual inversion training echo 2. Explore InvokeAI nodes using a command-line interface
echo 3. Merge models (diffusers type only) echo 3. Run textual inversion training
echo 4. Download and install models echo 4. Merge models (diffusers type only)
echo 5. Change InvokeAI startup options echo 5. Download and install models
echo 6. Re-run the configure script to fix a broken install or to complete a major upgrade echo 6. Change InvokeAI startup options
echo 7. Open the developer console echo 7. Re-run the configure script to fix a broken install or to complete a major upgrade
echo 8. Update InvokeAI echo 8. Open the developer console
echo 9. Run the InvokeAI image database maintenance script 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: [1] "
@ -25,21 +25,24 @@ IF /I "%choice%" == "1" (
echo Starting the InvokeAI browser-based UI.. echo Starting the InvokeAI browser-based UI..
python .venv\Scripts\invokeai-web.exe %* python .venv\Scripts\invokeai-web.exe %*
) ELSE IF /I "%choice%" == "2" ( ) ELSE IF /I "%choice%" == "2" (
echo Starting the InvokeAI command-line..
python .venv\Scripts\invokeai.exe %*
) ELSE IF /I "%choice%" == "3" (
echo Starting textual inversion training.. echo Starting textual inversion training..
python .venv\Scripts\invokeai-ti.exe --gui python .venv\Scripts\invokeai-ti.exe --gui
) ELSE IF /I "%choice%" == "3" ( ) ELSE IF /I "%choice%" == "4" (
echo Starting model merging script.. echo Starting model merging script..
python .venv\Scripts\invokeai-merge.exe --gui python .venv\Scripts\invokeai-merge.exe --gui
) ELSE IF /I "%choice%" == "4" ( ) ELSE IF /I "%choice%" == "5" (
echo Running invokeai-model-install... echo Running invokeai-model-install...
python .venv\Scripts\invokeai-model-install.exe python .venv\Scripts\invokeai-model-install.exe
) ELSE IF /I "%choice%" == "5" (
echo Running invokeai-configure...
python .venv\Scripts\invokeai-configure.exe --skip-sd-weight --skip-support-models
) ELSE IF /I "%choice%" == "6" ( ) ELSE IF /I "%choice%" == "6" (
echo Running invokeai-configure... echo Running invokeai-configure...
python .venv\Scripts\invokeai-configure.exe --yes --skip-sd-weight 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...
python .venv\Scripts\invokeai-configure.exe --yes --skip-sd-weight
) ELSE IF /I "%choice%" == "8" (
echo Developer Console echo Developer Console
echo Python command is: echo Python command is:
where python where python
@ -51,15 +54,12 @@ IF /I "%choice%" == "1" (
echo ************************* echo *************************
echo *** Type `exit` to quit this shell and deactivate the Python virtual environment *** echo *** Type `exit` to quit this shell and deactivate the Python virtual environment ***
call cmd /k call cmd /k
) ELSE IF /I "%choice%" == "8" ( ) ELSE IF /I "%choice%" == "9" (
echo Running invokeai-update... echo Running invokeai-update...
python -m invokeai.frontend.install.invokeai_update python -m invokeai.frontend.install.invokeai_update
) ELSE IF /I "%choice%" == "9" (
echo Running the db maintenance script...
python .venv\Scripts\invokeai-db-maintenance.exe
) ELSE IF /I "%choice%" == "10" ( ) ELSE IF /I "%choice%" == "10" (
echo Displaying command line help... echo Displaying command line help...
python .venv\Scripts\invokeai-web.exe --help %* python .venv\Scripts\invokeai.exe --help %*
pause pause
exit /b exit /b
) ELSE IF /I "%choice%" == "q" ( ) ELSE IF /I "%choice%" == "q" (

View File

@ -46,9 +46,6 @@ if [ "$(uname -s)" == "Darwin" ]; then
export PYTORCH_ENABLE_MPS_FALLBACK=1 export PYTORCH_ENABLE_MPS_FALLBACK=1
fi fi
# Avoid glibc memory fragmentation. See invokeai/backend/model_management/README.md for details.
export MALLOC_MMAP_THRESHOLD_=1048576
# Primary function for the case statement to determine user input # Primary function for the case statement to determine user input
do_choice() { do_choice() {
case $1 in case $1 in
@ -58,50 +55,55 @@ do_choice() {
invokeai-web $PARAMS invokeai-web $PARAMS
;; ;;
2) 2)
clear
printf "Explore InvokeAI nodes using a command-line interface\n"
invokeai $PARAMS
;;
3)
clear clear
printf "Textual inversion training\n" printf "Textual inversion training\n"
invokeai-ti --gui $PARAMS invokeai-ti --gui $PARAMS
;; ;;
3) 4)
clear clear
printf "Merge models (diffusers type only)\n" printf "Merge models (diffusers type only)\n"
invokeai-merge --gui $PARAMS invokeai-merge --gui $PARAMS
;; ;;
4) 5)
clear clear
printf "Download and install models\n" printf "Download and install models\n"
invokeai-model-install --root ${INVOKEAI_ROOT} invokeai-model-install --root ${INVOKEAI_ROOT}
;; ;;
5) 6)
clear clear
printf "Change InvokeAI startup options\n" printf "Change InvokeAI startup options\n"
invokeai-configure --root ${INVOKEAI_ROOT} --skip-sd-weights --skip-support-models invokeai-configure --root ${INVOKEAI_ROOT} --skip-sd-weights --skip-support-models
;; ;;
6) 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 --skip-sd-weights
;; ;;
7) 8)
clear clear
printf "Open the developer console\n" printf "Open the developer console\n"
file_name=$(basename "${BASH_SOURCE[0]}") file_name=$(basename "${BASH_SOURCE[0]}")
bash --init-file "$file_name" bash --init-file "$file_name"
;; ;;
8) 9)
clear clear
printf "Update InvokeAI\n" printf "Update InvokeAI\n"
python -m invokeai.frontend.install.invokeai_update python -m invokeai.frontend.install.invokeai_update
;; ;;
9)
clear
printf "Running the db maintenance script\n"
invokeai-db-maintenance --root ${INVOKEAI_ROOT}
;;
10) 10)
clear clear
printf "Command-line help\n" printf "Command-line help\n"
invokeai-web --help invokeai --help
;;
"HELP 1")
clear
printf "Command-line help\n"
invokeai --help
;; ;;
*) *)
clear clear
@ -116,16 +118,14 @@ do_choice() {
do_dialog() { do_dialog() {
options=( options=(
1 "Generate images with a browser-based interface" 1 "Generate images with a browser-based interface"
2 "Textual inversion training" 2 "Explore InvokeAI nodes using a command-line interface"
3 "Merge models (diffusers type only)" 3 "Textual inversion training"
4 "Download and install models" 4 "Merge models (diffusers type only)"
5 "Change InvokeAI startup options" 5 "Download and install models"
6 "Re-run the configure script to fix a broken install or to complete a major upgrade" 6 "Change InvokeAI startup options"
7 "Open the developer console" 7 "Re-run the configure script to fix a broken install or to complete a major upgrade"
8 "Update InvokeAI" 8 "Open the developer console"
9 "Run the InvokeAI image database maintenance script" 9 "Update InvokeAI")
10 "Command-line help"
)
choice=$(dialog --clear \ choice=$(dialog --clear \
--backtitle "\Zb\Zu\Z3InvokeAI" \ --backtitle "\Zb\Zu\Z3InvokeAI" \
@ -149,14 +149,14 @@ do_line_input() {
printf " ** For a more attractive experience, please install the 'dialog' utility using your package manager. **\n\n" printf " ** For a more attractive experience, please install the 'dialog' utility using your package manager. **\n\n"
printf "What would you like to do?\n" printf "What would you like to do?\n"
printf "1: Generate images using the browser-based interface\n" printf "1: Generate images using the browser-based interface\n"
printf "2: Run textual inversion training\n" printf "2: Explore InvokeAI nodes using the command-line interface\n"
printf "3: Merge models (diffusers type only)\n" printf "3: Run textual inversion training\n"
printf "4: Download and install models\n" printf "4: Merge models (diffusers type only)\n"
printf "5: Change InvokeAI startup options\n" printf "5: Download and install models\n"
printf "6: Re-run the configure script to fix a broken install\n" printf "6: Change InvokeAI startup options\n"
printf "7: Open the developer console\n" printf "7: Re-run the configure script to fix a broken install\n"
printf "8: Update InvokeAI\n" printf "8: Open the developer console\n"
printf "9: Run the InvokeAI image database maintenance script\n" printf "9: Update InvokeAI\n"
printf "10: Command-line help\n" printf "10: Command-line help\n"
printf "Q: Quit\n\n" printf "Q: Quit\n\n"
read -p "Please enter 1-10, Q: [1] " yn read -p "Please enter 1-10, Q: [1] " yn

View File

@ -1,38 +1,35 @@
# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654)
import sqlite3
from logging import Logger from logging import Logger
from invokeai.app.services.workflow_image_records.workflow_image_records_sqlite import SqliteWorkflowImageRecordsStorage from invokeai.app.services.board_image_record_storage import SqliteBoardImageRecordStorage
from invokeai.app.services.board_images import BoardImagesService, BoardImagesServiceDependencies
from invokeai.app.services.board_record_storage import SqliteBoardRecordStorage
from invokeai.app.services.boards import BoardService, BoardServiceDependencies
from invokeai.app.services.config import InvokeAIAppConfig
from invokeai.app.services.image_record_storage import SqliteImageRecordStorage
from invokeai.app.services.images import ImageService, ImageServiceDependencies
from invokeai.app.services.invocation_cache.invocation_cache_memory import MemoryInvocationCache
from invokeai.app.services.resource_name import SimpleNameService
from invokeai.app.services.session_processor.session_processor_default import DefaultSessionProcessor
from invokeai.app.services.session_queue.session_queue_sqlite import SqliteSessionQueue
from invokeai.app.services.urls import LocalUrlService
from invokeai.backend.util.logging import InvokeAILogger from invokeai.backend.util.logging import InvokeAILogger
from invokeai.version.invokeai_version import __version__ from invokeai.version.invokeai_version import __version__
from ..services.board_image_records.board_image_records_sqlite import SqliteBoardImageRecordStorage from ..services.default_graphs import create_system_graphs
from ..services.board_images.board_images_default import BoardImagesService from ..services.graph import GraphExecutionState, LibraryGraph
from ..services.board_records.board_records_sqlite import SqliteBoardRecordStorage from ..services.image_file_storage import DiskImageFileStorage
from ..services.boards.boards_default import BoardService from ..services.invocation_queue import MemoryInvocationQueue
from ..services.config import InvokeAIAppConfig
from ..services.image_files.image_files_disk import DiskImageFileStorage
from ..services.image_records.image_records_sqlite import SqliteImageRecordStorage
from ..services.images.images_default import ImageService
from ..services.invocation_cache.invocation_cache_memory import MemoryInvocationCache
from ..services.invocation_processor.invocation_processor_default import DefaultInvocationProcessor
from ..services.invocation_queue.invocation_queue_memory import MemoryInvocationQueue
from ..services.invocation_services import InvocationServices from ..services.invocation_services import InvocationServices
from ..services.invocation_stats.invocation_stats_default import InvocationStatsService from ..services.invocation_stats import InvocationStatsService
from ..services.invoker import Invoker from ..services.invoker import Invoker
from ..services.item_storage.item_storage_sqlite import SqliteItemStorage from ..services.latent_storage import DiskLatentsStorage, ForwardCacheLatentsStorage
from ..services.latents_storage.latents_storage_disk import DiskLatentsStorage from ..services.model_manager_service import ModelManagerService
from ..services.latents_storage.latents_storage_forward_cache import ForwardCacheLatentsStorage from ..services.processor import DefaultInvocationProcessor
from ..services.model_manager.model_manager_default import ModelManagerService from ..services.sqlite import SqliteItemStorage
from ..services.model_records import ModelRecordServiceSQL from ..services.thread import lock
from ..services.names.names_default import SimpleNameService
from ..services.session_processor.session_processor_default import DefaultSessionProcessor
from ..services.session_queue.session_queue_sqlite import SqliteSessionQueue
from ..services.shared.default_graphs import create_system_graphs
from ..services.shared.graph import GraphExecutionState, LibraryGraph
from ..services.shared.sqlite import SqliteDatabase
from ..services.urls.urls_default import LocalUrlService
from ..services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage
from .events import FastAPIEventService from .events import FastAPIEventService
@ -52,7 +49,7 @@ def check_internet() -> bool:
return False return False
logger = InvokeAILogger.get_logger() logger = InvokeAILogger.getLogger()
class ApiDependencies: class ApiDependencies:
@ -66,70 +63,100 @@ class ApiDependencies:
logger.info(f"Root directory = {str(config.root_path)}") 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)
output_folder = config.output_path output_folder = config.output_path
db = SqliteDatabase(config, logger) # TODO: build a file/path manager?
if config.use_memory_db:
db_location = ":memory:"
else:
db_path = config.db_path
db_path.parent.mkdir(parents=True, exist_ok=True)
db_location = str(db_path)
configuration = config logger.info(f"Using database at {db_location}")
logger = logger db_conn = sqlite3.connect(db_location, check_same_thread=False) # TODO: figure out a better threading solution
if config.log_sql:
db_conn.set_trace_callback(print)
db_conn.execute("PRAGMA foreign_keys = ON;")
graph_execution_manager = SqliteItemStorage[GraphExecutionState](
conn=db_conn, table_name="graph_executions", lock=lock
)
board_image_records = SqliteBoardImageRecordStorage(db=db)
board_images = BoardImagesService()
board_records = SqliteBoardRecordStorage(db=db)
boards = BoardService()
events = FastAPIEventService(event_handler_id)
graph_execution_manager = SqliteItemStorage[GraphExecutionState](db=db, table_name="graph_executions")
graph_library = SqliteItemStorage[LibraryGraph](db=db, table_name="graphs")
image_files = DiskImageFileStorage(f"{output_folder}/images")
image_records = SqliteImageRecordStorage(db=db)
images = ImageService()
invocation_cache = MemoryInvocationCache(max_cache_size=config.node_cache_size)
latents = ForwardCacheLatentsStorage(DiskLatentsStorage(f"{output_folder}/latents"))
model_manager = ModelManagerService(config, logger)
model_record_service = ModelRecordServiceSQL(db=db)
names = SimpleNameService()
performance_statistics = InvocationStatsService()
processor = DefaultInvocationProcessor()
queue = MemoryInvocationQueue()
session_processor = DefaultSessionProcessor()
session_queue = SqliteSessionQueue(db=db)
urls = LocalUrlService() urls = LocalUrlService()
workflow_image_records = SqliteWorkflowImageRecordsStorage(db=db) image_record_storage = SqliteImageRecordStorage(conn=db_conn, lock=lock)
workflow_records = SqliteWorkflowRecordsStorage(db=db) image_file_storage = DiskImageFileStorage(f"{output_folder}/images")
names = SimpleNameService()
latents = ForwardCacheLatentsStorage(DiskLatentsStorage(f"{output_folder}/latents"))
board_record_storage = SqliteBoardRecordStorage(conn=db_conn, lock=lock)
board_image_record_storage = SqliteBoardImageRecordStorage(conn=db_conn, lock=lock)
boards = BoardService(
services=BoardServiceDependencies(
board_image_record_storage=board_image_record_storage,
board_record_storage=board_record_storage,
image_record_storage=image_record_storage,
url=urls,
logger=logger,
)
)
board_images = BoardImagesService(
services=BoardImagesServiceDependencies(
board_image_record_storage=board_image_record_storage,
board_record_storage=board_record_storage,
image_record_storage=image_record_storage,
url=urls,
logger=logger,
)
)
images = ImageService(
services=ImageServiceDependencies(
board_image_record_storage=board_image_record_storage,
image_record_storage=image_record_storage,
image_file_storage=image_file_storage,
url=urls,
logger=logger,
names=names,
graph_execution_manager=graph_execution_manager,
)
)
services = InvocationServices( services = InvocationServices(
board_image_records=board_image_records, model_manager=ModelManagerService(config, logger),
board_images=board_images,
board_records=board_records,
boards=boards,
configuration=configuration,
events=events, events=events,
graph_execution_manager=graph_execution_manager,
graph_library=graph_library,
image_files=image_files,
image_records=image_records,
images=images,
invocation_cache=invocation_cache,
latents=latents, latents=latents,
images=images,
boards=boards,
board_images=board_images,
queue=MemoryInvocationQueue(),
graph_library=SqliteItemStorage[LibraryGraph](conn=db_conn, lock=lock, table_name="graphs"),
graph_execution_manager=graph_execution_manager,
processor=DefaultInvocationProcessor(),
configuration=config,
performance_statistics=InvocationStatsService(graph_execution_manager),
logger=logger, logger=logger,
model_manager=model_manager, session_queue=SqliteSessionQueue(conn=db_conn, lock=lock),
model_records=model_record_service, session_processor=DefaultSessionProcessor(),
names=names, invocation_cache=MemoryInvocationCache(max_cache_size=config.node_cache_size),
performance_statistics=performance_statistics,
processor=processor,
queue=queue,
session_processor=session_processor,
session_queue=session_queue,
urls=urls,
workflow_image_records=workflow_image_records,
workflow_records=workflow_records,
) )
create_system_graphs(services.graph_library) create_system_graphs(services.graph_library)
ApiDependencies.invoker = Invoker(services) ApiDependencies.invoker = Invoker(services)
db.clean() try:
lock.acquire()
db_conn.execute("VACUUM;")
db_conn.commit()
logger.info("Cleaned database")
finally:
lock.release()
@staticmethod @staticmethod
def shutdown(): def shutdown():

View File

@ -7,7 +7,7 @@ from typing import Any
from fastapi_events.dispatcher import dispatch from fastapi_events.dispatcher import dispatch
from ..services.events.events_base import EventServiceBase from ..services.events import EventServiceBase
class FastAPIEventService(EventServiceBase): class FastAPIEventService(EventServiceBase):
@ -28,7 +28,7 @@ class FastAPIEventService(EventServiceBase):
self.__queue.put(None) self.__queue.put(None)
def dispatch(self, event_name: str, payload: Any) -> None: def dispatch(self, event_name: str, payload: Any) -> None:
self.__queue.put({"event_name": event_name, "payload": payload}) self.__queue.put(dict(event_name=event_name, payload=payload))
async def __dispatch_from_queue(self, stop_event: threading.Event): async def __dispatch_from_queue(self, stop_event: threading.Event):
"""Get events on from the queue and dispatch them, from the correct thread""" """Get events on from the queue and dispatch them, from the correct thread"""

View File

@ -7,7 +7,6 @@ from fastapi.routing import APIRouter
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from invokeai.app.invocations.upscale import ESRGAN_MODELS from invokeai.app.invocations.upscale import ESRGAN_MODELS
from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus
from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark
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.safety_checker import SafetyChecker
@ -104,43 +103,3 @@ async def set_log_level(
"""Sets the log verbosity level""" """Sets the log verbosity level"""
ApiDependencies.invoker.services.logger.setLevel(level) ApiDependencies.invoker.services.logger.setLevel(level)
return LogLevel(ApiDependencies.invoker.services.logger.level) return LogLevel(ApiDependencies.invoker.services.logger.level)
@app_router.delete(
"/invocation_cache",
operation_id="clear_invocation_cache",
responses={200: {"description": "The operation was successful"}},
)
async def clear_invocation_cache() -> None:
"""Clears the invocation cache"""
ApiDependencies.invoker.services.invocation_cache.clear()
@app_router.put(
"/invocation_cache/enable",
operation_id="enable_invocation_cache",
responses={200: {"description": "The operation was successful"}},
)
async def enable_invocation_cache() -> None:
"""Clears the invocation cache"""
ApiDependencies.invoker.services.invocation_cache.enable()
@app_router.put(
"/invocation_cache/disable",
operation_id="disable_invocation_cache",
responses={200: {"description": "The operation was successful"}},
)
async def disable_invocation_cache() -> None:
"""Clears the invocation cache"""
ApiDependencies.invoker.services.invocation_cache.disable()
@app_router.get(
"/invocation_cache/status",
operation_id="get_invocation_cache_status",
responses={200: {"model": InvocationCacheStatus}},
)
async def get_invocation_cache_status() -> InvocationCacheStatus:
"""Clears the invocation cache"""
return ApiDependencies.invoker.services.invocation_cache.get_status()

View File

@ -4,9 +4,9 @@ from fastapi import Body, HTTPException, Path, Query
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from invokeai.app.services.board_records.board_records_common import BoardChanges from invokeai.app.services.board_record_storage import BoardChanges
from invokeai.app.services.boards.boards_common import BoardDTO from invokeai.app.services.image_record_storage import OffsetPaginatedResults
from invokeai.app.services.shared.pagination import OffsetPaginatedResults from invokeai.app.services.models.board_record import BoardDTO
from ..dependencies import ApiDependencies from ..dependencies import ApiDependencies

View File

@ -1,17 +1,16 @@
import io import io
import traceback
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
from pydantic import BaseModel, Field, ValidationError from pydantic import BaseModel, Field
from invokeai.app.invocations.baseinvocation import MetadataField, MetadataFieldValidator, WorkflowFieldValidator from invokeai.app.invocations.metadata import ImageMetadata
from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin from invokeai.app.models.image import ImageCategory, ResourceOrigin
from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO from invokeai.app.services.image_record_storage import OffsetPaginatedResults
from invokeai.app.services.shared.pagination import OffsetPaginatedResults from invokeai.app.services.models.image_record import ImageDTO, ImageRecordChanges, ImageUrlsDTO
from ..dependencies import ApiDependencies from ..dependencies import ApiDependencies
@ -43,41 +42,20 @@ async def upload_image(
crop_visible: Optional[bool] = Query(default=False, description="Whether to crop the image"), 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 or not file.content_type.startswith("image"): if not file.content_type.startswith("image"):
raise HTTPException(status_code=415, detail="Not an image") raise HTTPException(status_code=415, detail="Not an image")
metadata = None
workflow = None
contents = await file.read() contents = await file.read()
try: try:
pil_image = Image.open(io.BytesIO(contents)) pil_image = Image.open(io.BytesIO(contents))
if crop_visible: if crop_visible:
bbox = pil_image.getbbox() bbox = pil_image.getbbox()
pil_image = pil_image.crop(bbox) pil_image = pil_image.crop(bbox)
except Exception: except Exception:
ApiDependencies.invoker.services.logger.error(traceback.format_exc()) # Error opening the image
raise HTTPException(status_code=415, detail="Failed to read image") raise HTTPException(status_code=415, detail="Failed to read image")
# TODO: retain non-invokeai metadata on upload?
# attempt to parse metadata from image
metadata_raw = pil_image.info.get("invokeai_metadata", None)
if metadata_raw:
try:
metadata = MetadataFieldValidator.validate_json(metadata_raw)
except ValidationError:
ApiDependencies.invoker.services.logger.warn("Failed to parse metadata for uploaded image")
pass
# attempt to parse workflow from image
workflow_raw = pil_image.info.get("invokeai_workflow", None)
if workflow_raw is not None:
try:
workflow = WorkflowFieldValidator.validate_json(workflow_raw)
except ValidationError:
ApiDependencies.invoker.services.logger.warn("Failed to parse metadata for uploaded image")
pass
try: try:
image_dto = ApiDependencies.invoker.services.images.create( image_dto = ApiDependencies.invoker.services.images.create(
image=pil_image, image=pil_image,
@ -85,8 +63,6 @@ async def upload_image(
image_category=image_category, image_category=image_category,
session_id=session_id, session_id=session_id,
board_id=board_id, board_id=board_id,
metadata=metadata,
workflow=workflow,
is_intermediate=is_intermediate, is_intermediate=is_intermediate,
) )
@ -95,7 +71,6 @@ async def upload_image(
return image_dto return image_dto
except Exception: except Exception:
ApiDependencies.invoker.services.logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail="Failed to create image") raise HTTPException(status_code=500, detail="Failed to create image")
@ -112,7 +87,7 @@ async def delete_image(
pass pass
@images_router.delete("/intermediates", operation_id="clear_intermediates") @images_router.post("/clear-intermediates", operation_id="clear_intermediates")
async def clear_intermediates() -> int: async def clear_intermediates() -> int:
"""Clears all intermediates""" """Clears all intermediates"""
@ -124,17 +99,6 @@ async def clear_intermediates() -> int:
pass pass
@images_router.get("/intermediates", operation_id="get_intermediates_count")
async def get_intermediates_count() -> int:
"""Gets the count of intermediate images"""
try:
return ApiDependencies.invoker.services.images.get_intermediates_count()
except Exception:
raise HTTPException(status_code=500, detail="Failed to get intermediates")
pass
@images_router.patch( @images_router.patch(
"/i/{image_name}", "/i/{image_name}",
operation_id="update_image", operation_id="update_image",
@ -171,11 +135,11 @@ async def get_image_dto(
@images_router.get( @images_router.get(
"/i/{image_name}/metadata", "/i/{image_name}/metadata",
operation_id="get_image_metadata", operation_id="get_image_metadata",
response_model=Optional[MetadataField], response_model=ImageMetadata,
) )
async def get_image_metadata( async def get_image_metadata(
image_name: str = Path(description="The name of image to get"), image_name: str = Path(description="The name of image to get"),
) -> Optional[MetadataField]: ) -> ImageMetadata:
"""Gets an image's metadata""" """Gets an image's metadata"""
try: try:
@ -358,20 +322,3 @@ async def unstar_images_in_list(
return ImagesUpdatedFromListResult(updated_image_names=updated_image_names) return ImagesUpdatedFromListResult(updated_image_names=updated_image_names)
except Exception: except Exception:
raise HTTPException(status_code=500, detail="Failed to unstar images") raise HTTPException(status_code=500, detail="Failed to unstar images")
class ImagesDownloaded(BaseModel):
response: Optional[str] = Field(
description="If defined, the message to display to the user when images begin downloading"
)
@images_router.post("/export", operation_id="download_images_from_list", response_model=ImagesDownloaded)
async def download_images_from_list(
image_names: list[str] = Body(description="The list of names of images to download", embed=True),
board_id: Optional[str] = Body(
default=None, description="The board from which image should be downloaded from", embed=True
),
) -> ImagesDownloaded:
# return ImagesDownloaded(response="Your images are downloading")
raise HTTPException(status_code=501, detail="Endpoint is not yet implemented")

View File

@ -1,164 +0,0 @@
# Copyright (c) 2023 Lincoln D. Stein
"""FastAPI route for model configuration records."""
from hashlib import sha1
from random import randbytes
from typing import List, Optional
from fastapi import Body, Path, Query, Response
from fastapi.routing import APIRouter
from pydantic import BaseModel, ConfigDict
from starlette.exceptions import HTTPException
from typing_extensions import Annotated
from invokeai.app.services.model_records import (
DuplicateModelException,
InvalidModelException,
UnknownModelException,
)
from invokeai.backend.model_manager.config import (
AnyModelConfig,
BaseModelType,
ModelType,
)
from ..dependencies import ApiDependencies
model_records_router = APIRouter(prefix="/v1/model/record", tags=["models"])
class ModelsList(BaseModel):
"""Return list of configs."""
models: list[AnyModelConfig]
model_config = ConfigDict(use_enum_values=True)
@model_records_router.get(
"/",
operation_id="list_model_records",
)
async def list_model_records(
base_models: Optional[List[BaseModelType]] = Query(default=None, description="Base models to include"),
model_type: Optional[ModelType] = Query(default=None, description="The type of model to get"),
) -> ModelsList:
"""Get a list of models."""
record_store = ApiDependencies.invoker.services.model_records
found_models: list[AnyModelConfig] = []
if base_models:
for base_model in base_models:
found_models.extend(record_store.search_by_attr(base_model=base_model, model_type=model_type))
else:
found_models.extend(record_store.search_by_attr(model_type=model_type))
return ModelsList(models=found_models)
@model_records_router.get(
"/i/{key}",
operation_id="get_model_record",
responses={
200: {"description": "Success"},
400: {"description": "Bad request"},
404: {"description": "The model could not be found"},
},
)
async def get_model_record(
key: str = Path(description="Key of the model record to fetch."),
) -> AnyModelConfig:
"""Get a model record"""
record_store = ApiDependencies.invoker.services.model_records
try:
return record_store.get_model(key)
except UnknownModelException as e:
raise HTTPException(status_code=404, detail=str(e))
@model_records_router.patch(
"/i/{key}",
operation_id="update_model_record",
responses={
200: {"description": "The model was updated successfully"},
400: {"description": "Bad request"},
404: {"description": "The model could not be found"},
409: {"description": "There is already a model corresponding to the new name"},
},
status_code=200,
response_model=AnyModelConfig,
)
async def update_model_record(
key: Annotated[str, Path(description="Unique key of model")],
info: Annotated[AnyModelConfig, Body(description="Model config", discriminator="type")],
) -> AnyModelConfig:
"""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
record_store = ApiDependencies.invoker.services.model_records
try:
model_response = record_store.update_model(key, config=info)
logger.info(f"Updated model: {key}")
except UnknownModelException as 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))
return model_response
@model_records_router.delete(
"/i/{key}",
operation_id="del_model_record",
responses={
204: {"description": "Model deleted successfully"},
404: {"description": "Model not found"},
},
status_code=204,
)
async def del_model_record(
key: str = Path(description="Unique key of model to remove from model registry."),
) -> Response:
"""Delete Model"""
logger = ApiDependencies.invoker.services.logger
try:
record_store = ApiDependencies.invoker.services.model_records
record_store.del_model(key)
logger.info(f"Deleted model: {key}")
return Response(status_code=204)
except UnknownModelException as e:
logger.error(str(e))
raise HTTPException(status_code=404, detail=str(e))
@model_records_router.post(
"/i/",
operation_id="add_model_record",
responses={
201: {"description": "The model added successfully"},
409: {"description": "There is already a model corresponding to this path or repo_id"},
415: {"description": "Unrecognized file/folder format"},
},
status_code=201,
)
async def add_model_record(
config: Annotated[AnyModelConfig, Body(description="Model config", discriminator="type")]
) -> AnyModelConfig:
"""
Add a model using the configuration information appropriate for its type.
"""
logger = ApiDependencies.invoker.services.logger
record_store = ApiDependencies.invoker.services.model_records
if config.key == "<NOKEY>":
config.key = sha1(randbytes(100)).hexdigest()
logger.info(f"Created model {config.key} for {config.name}")
try:
record_store.add_model(config.key, config)
except DuplicateModelException as e:
logger.error(str(e))
raise HTTPException(status_code=409, detail=str(e))
except InvalidModelException as e:
logger.error(str(e))
raise HTTPException(status_code=415)
# now fetch it out
return record_store.get_model(config.key)

View File

@ -1,11 +1,12 @@
# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654), 2023 Kent Keirsey (https://github.com/hipsterusername), 2023 Lincoln D. Stein # Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654), 2023 Kent Keirsey (https://github.com/hipsterusername), 2023 Lincoln D. Stein
import pathlib import pathlib
from typing import Annotated, List, Literal, Optional, Union from typing import List, Literal, Optional, Union
from fastapi import Body, Path, Query, Response from fastapi import Body, Path, Query, Response
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter from pydantic import BaseModel, parse_obj_as
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from invokeai.backend import BaseModelType, ModelType from invokeai.backend import BaseModelType, ModelType
@ -22,14 +23,8 @@ from ..dependencies import ApiDependencies
models_router = APIRouter(prefix="/v1/models", tags=["models"]) models_router = APIRouter(prefix="/v1/models", tags=["models"])
UpdateModelResponse = Union[tuple(OPENAPI_MODEL_CONFIGS)] UpdateModelResponse = Union[tuple(OPENAPI_MODEL_CONFIGS)]
UpdateModelResponseValidator = TypeAdapter(UpdateModelResponse)
ImportModelResponse = Union[tuple(OPENAPI_MODEL_CONFIGS)] ImportModelResponse = Union[tuple(OPENAPI_MODEL_CONFIGS)]
ImportModelResponseValidator = TypeAdapter(ImportModelResponse)
ConvertModelResponse = Union[tuple(OPENAPI_MODEL_CONFIGS)] ConvertModelResponse = Union[tuple(OPENAPI_MODEL_CONFIGS)]
ConvertModelResponseValidator = TypeAdapter(ConvertModelResponse)
MergeModelResponse = Union[tuple(OPENAPI_MODEL_CONFIGS)] MergeModelResponse = Union[tuple(OPENAPI_MODEL_CONFIGS)]
ImportModelAttributes = Union[tuple(OPENAPI_MODEL_CONFIGS)] ImportModelAttributes = Union[tuple(OPENAPI_MODEL_CONFIGS)]
@ -37,11 +32,6 @@ 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)]]
model_config = ConfigDict(use_enum_values=True)
ModelsListValidator = TypeAdapter(ModelsList)
@models_router.get( @models_router.get(
"/", "/",
@ -54,12 +44,12 @@ async def list_models(
) -> ModelsList: ) -> ModelsList:
"""Gets a list of models""" """Gets a list of models"""
if base_models and len(base_models) > 0: if base_models and len(base_models) > 0:
models_raw = [] models_raw = list()
for base_model in base_models: for base_model in base_models:
models_raw.extend(ApiDependencies.invoker.services.model_manager.list_models(base_model, model_type)) models_raw.extend(ApiDependencies.invoker.services.model_manager.list_models(base_model, model_type))
else: else:
models_raw = ApiDependencies.invoker.services.model_manager.list_models(None, model_type) models_raw = ApiDependencies.invoker.services.model_manager.list_models(None, model_type)
models = ModelsListValidator.validate_python({"models": models_raw}) models = parse_obj_as(ModelsList, {"models": models_raw})
return models return models
@ -115,14 +105,11 @@ async def update_model(
info.path = new_info.get("path") info.path = new_info.get("path")
# replace empty string values with None/null to avoid phenomenon of vae: '' # replace empty string values with None/null to avoid phenomenon of vae: ''
info_dict = info.model_dump() info_dict = info.dict()
info_dict = {x: info_dict[x] if info_dict[x] else None for x in info_dict.keys()} info_dict = {x: info_dict[x] if info_dict[x] else None for x in info_dict.keys()}
ApiDependencies.invoker.services.model_manager.update_model( ApiDependencies.invoker.services.model_manager.update_model(
model_name=model_name, model_name=model_name, base_model=base_model, model_type=model_type, model_attributes=info_dict
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(
@ -130,7 +117,7 @@ async def update_model(
base_model=base_model, base_model=base_model,
model_type=model_type, model_type=model_type,
) )
model_response = UpdateModelResponseValidator.validate_python(model_raw) model_response = parse_obj_as(UpdateModelResponse, model_raw)
except ModelNotFoundException as e: except ModelNotFoundException as e:
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except ValueError as e: except ValueError as e:
@ -159,21 +146,18 @@ async def update_model(
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"]] = Body(
description="Prediction type for SDv2 checkpoints and rare SDv1 checkpoints", description="Prediction type for SDv2 checkpoint files", default="v_prediction"
default=None,
), ),
) -> 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"""
location = location.strip("\"' ")
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, items_to_import=items_to_import, prediction_type_helper=lambda x: prediction_types.get(prediction_type)
prediction_type_helper=lambda x: prediction_types.get(prediction_type),
) )
info = installed_models.get(location) info = installed_models.get(location)
@ -185,7 +169,7 @@ async def import_model(
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 ImportModelResponseValidator.validate_python(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))
@ -219,18 +203,13 @@ async def add_model(
try: try:
ApiDependencies.invoker.services.model_manager.add_model( ApiDependencies.invoker.services.model_manager.add_model(
info.model_name, info.model_name, info.base_model, info.model_type, model_attributes=info.dict()
info.base_model,
info.model_type,
model_attributes=info.model_dump(),
) )
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, model_name=info.model_name, base_model=info.base_model, model_type=info.model_type
base_model=info.base_model,
model_type=info.model_type,
) )
return ImportModelResponseValidator.validate_python(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))
@ -242,10 +221,7 @@ 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={ responses={204: {"description": "Model deleted successfully"}, 404: {"description": "Model not found"}},
204: {"description": "Model deleted successfully"},
404: {"description": "Model not found"},
},
status_code=204, status_code=204,
response_model=None, response_model=None,
) )
@ -301,7 +277,7 @@ async def convert_model(
model_raw = ApiDependencies.invoker.services.model_manager.list_model( model_raw = ApiDependencies.invoker.services.model_manager.list_model(
model_name, base_model=base_model, model_type=model_type model_name, base_model=base_model, model_type=model_type
) )
response = ConvertModelResponseValidator.validate_python(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)}")
except ValueError as e: except ValueError as e:
@ -324,8 +300,7 @@ async def 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, status_code=404, detail=f"The search path '{search_path}' does not exist or is not directory"
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)
@ -360,26 +335,6 @@ async def sync_to_config() -> bool:
return True return True
# There's some weird pydantic-fastapi behaviour that requires this to be a separate class
# TODO: After a few updates, see if it works inside the route operation handler?
class MergeModelsBody(BaseModel):
model_names: List[str] = Field(description="model name", min_length=2, max_length=3)
merged_model_name: Optional[str] = Field(description="Name of destination model")
alpha: Optional[float] = Field(description="Alpha weighting strength to apply to 2d and 3d models", default=0.5)
interp: Optional[MergeInterpolationMethod] = Field(description="Interpolation method")
force: Optional[bool] = Field(
description="Force merging of models created with different versions of diffusers",
default=False,
)
merge_dest_directory: Optional[str] = Field(
description="Save the merged model to the designated directory (with 'merged_model_name' appended)",
default=None,
)
model_config = ConfigDict(protected_namespaces=())
@models_router.put( @models_router.put(
"/merge/{base_model}", "/merge/{base_model}",
operation_id="merge_models", operation_id="merge_models",
@ -392,23 +347,31 @@ class MergeModelsBody(BaseModel):
response_model=MergeModelResponse, response_model=MergeModelResponse,
) )
async def merge_models( async def merge_models(
body: Annotated[MergeModelsBody, Body(description="Model configuration", embed=True)],
base_model: BaseModelType = Path(description="Base model"), base_model: BaseModelType = Path(description="Base model"),
model_names: List[str] = Body(description="model name", min_items=2, max_items=3),
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),
interp: Optional[MergeInterpolationMethod] = Body(description="Interpolation method"),
force: Optional[bool] = Body(
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,
),
) -> 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( logger.info(f"Merging models: {model_names} into {merge_dest_directory or '<MODELS>'}/{merged_model_name}")
f"Merging models: {body.model_names} into {body.merge_dest_directory or '<MODELS>'}/{body.merged_model_name}" dest = pathlib.Path(merge_dest_directory) if merge_dest_directory else None
)
dest = pathlib.Path(body.merge_dest_directory) if body.merge_dest_directory else None
result = ApiDependencies.invoker.services.model_manager.merge_models( result = ApiDependencies.invoker.services.model_manager.merge_models(
model_names=body.model_names, model_names,
base_model=base_model, base_model,
merged_model_name=body.merged_model_name or "+".join(body.model_names), merged_model_name=merged_model_name or "+".join(model_names),
alpha=body.alpha, alpha=alpha,
interp=body.interp, interp=interp,
force=body.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(
@ -416,12 +379,9 @@ async def merge_models(
base_model=base_model, base_model=base_model,
model_type=ModelType.Main, model_type=ModelType.Main,
) )
response = ConvertModelResponseValidator.validate_python(model_raw) response = parse_obj_as(ConvertModelResponse, model_raw)
except ModelNotFoundException: except ModelNotFoundException:
raise HTTPException( raise HTTPException(status_code=404, detail=f"One or more of the models '{model_names}' not found")
status_code=404,
detail=f"One or more of the models '{body.model_names}' not found",
)
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

View File

@ -12,13 +12,15 @@ from invokeai.app.services.session_queue.session_queue_common import (
CancelByBatchIDsResult, CancelByBatchIDsResult,
ClearResult, ClearResult,
EnqueueBatchResult, EnqueueBatchResult,
EnqueueGraphResult,
PruneResult, PruneResult,
SessionQueueItem, SessionQueueItem,
SessionQueueItemDTO, SessionQueueItemDTO,
SessionQueueStatus, SessionQueueStatus,
) )
from invokeai.app.services.shared.pagination import CursorPaginatedResults from invokeai.app.services.shared.models import CursorPaginatedResults
from ...services.graph import Graph
from ..dependencies import ApiDependencies from ..dependencies import ApiDependencies
session_queue_router = APIRouter(prefix="/v1/queue", tags=["queue"]) session_queue_router = APIRouter(prefix="/v1/queue", tags=["queue"])
@ -31,6 +33,23 @@ class SessionQueueAndProcessorStatus(BaseModel):
processor: SessionProcessorStatus processor: SessionProcessorStatus
@session_queue_router.post(
"/{queue_id}/enqueue_graph",
operation_id="enqueue_graph",
responses={
201: {"model": EnqueueGraphResult},
},
)
async def enqueue_graph(
queue_id: str = Path(description="The queue id to perform this operation on"),
graph: Graph = Body(description="The graph to enqueue"),
prepend: bool = Body(default=False, description="Whether or not to prepend this batch in the queue"),
) -> EnqueueGraphResult:
"""Enqueues a graph for single execution."""
return ApiDependencies.invoker.services.session_queue.enqueue_graph(queue_id=queue_id, graph=graph, prepend=prepend)
@session_queue_router.post( @session_queue_router.post(
"/{queue_id}/enqueue_batch", "/{queue_id}/enqueue_batch",
operation_id="enqueue_batch", operation_id="enqueue_batch",
@ -65,7 +84,7 @@ async def list_queue_items(
"""Gets all queue items (without graphs)""" """Gets all queue items (without graphs)"""
return ApiDependencies.invoker.services.session_queue.list_queue_items( return ApiDependencies.invoker.services.session_queue.list_queue_items(
queue_id=queue_id, limit=limit, status=status, cursor=cursor, priority=priority queue_id=queue_id, limit=limit, status=status, order_id=cursor, priority=priority
) )
@ -206,7 +225,7 @@ async def get_batch_status(
) )
async def get_queue_item( async def get_queue_item(
queue_id: str = Path(description="The queue id to perform this operation on"), queue_id: str = Path(description="The queue id to perform this operation on"),
item_id: int = Path(description="The queue item to get"), item_id: str = Path(description="The queue item to get"),
) -> SessionQueueItem: ) -> SessionQueueItem:
"""Gets a queue item""" """Gets a queue item"""
return ApiDependencies.invoker.services.session_queue.get_queue_item(item_id) return ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
@ -221,7 +240,7 @@ async def get_queue_item(
) )
async def cancel_queue_item( async def cancel_queue_item(
queue_id: str = Path(description="The queue id to perform this operation on"), queue_id: str = Path(description="The queue id to perform this operation on"),
item_id: int = Path(description="The queue item to cancel"), item_id: str = Path(description="The queue item to cancel"),
) -> SessionQueueItem: ) -> SessionQueueItem:
"""Deletes a queue item""" """Deletes a queue item"""

View File

@ -1,50 +1,56 @@
# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654)
from typing import Annotated, Optional, Union
from fastapi import HTTPException, Path from fastapi import Body, HTTPException, Path, Query, Response
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from pydantic.fields import Field
from ...services.shared.graph import GraphExecutionState # Importing * is bad karma but needed here for node detection
from ...invocations import * # noqa: F401 F403
from ...invocations.baseinvocation import BaseInvocation
from ...services.graph import Edge, EdgeConnection, Graph, GraphExecutionState, NodeAlreadyExecutedError
from ...services.item_storage import PaginatedResults
from ..dependencies import ApiDependencies from ..dependencies import ApiDependencies
session_router = APIRouter(prefix="/v1/sessions", tags=["sessions"]) session_router = APIRouter(prefix="/v1/sessions", tags=["sessions"])
# @session_router.post( @session_router.post(
# "/", "/",
# operation_id="create_session", operation_id="create_session",
# responses={ responses={
# 200: {"model": GraphExecutionState}, 200: {"model": GraphExecutionState},
# 400: {"description": "Invalid json"}, 400: {"description": "Invalid json"},
# }, },
# deprecated=True, deprecated=True,
# ) )
# async def create_session( async def create_session(
# queue_id: str = Query(default="", description="The id of the queue to associate the session with"), queue_id: str = Query(default="", description="The id of the queue to associate the session with"),
# 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(queue_id=queue_id, graph=graph) session = ApiDependencies.invoker.create_execution_state(queue_id=queue_id, graph=graph)
# return session return session
# @session_router.get( @session_router.get(
# "/", "/",
# operation_id="list_sessions", operation_id="list_sessions",
# responses={200: {"model": PaginatedResults[GraphExecutionState]}}, responses={200: {"model": PaginatedResults[GraphExecutionState]}},
# deprecated=True, deprecated=True,
# ) )
# async def list_sessions( async def list_sessions(
# page: int = Query(default=0, description="The page of results to get"), page: int = Query(default=0, description="The page of results to get"),
# per_page: int = Query(default=10, description="The number of results per page"), per_page: int = Query(default=10, description="The number of results per page"),
# query: str = Query(default="", description="The query string to search for"), query: str = Query(default="", description="The query string to search for"),
# ) -> 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
@session_router.get( @session_router.get(
@ -54,6 +60,7 @@ session_router = APIRouter(prefix="/v1/sessions", tags=["sessions"])
200: {"model": GraphExecutionState}, 200: {"model": GraphExecutionState},
404: {"description": "Session not found"}, 404: {"description": "Session not found"},
}, },
deprecated=True,
) )
async def get_session( async def get_session(
session_id: str = Path(description="The id of the session to get"), session_id: str = Path(description="The id of the session to get"),
@ -66,211 +73,211 @@ async def get_session(
return session return session
# @session_router.post( @session_router.post(
# "/{session_id}/nodes", "/{session_id}/nodes",
# operation_id="add_node", operation_id="add_node",
# responses={ responses={
# 200: {"model": str}, 200: {"model": str},
# 400: {"description": "Invalid node or link"}, 400: {"description": "Invalid node or link"},
# 404: {"description": "Session not found"}, 404: {"description": "Session not found"},
# }, },
# deprecated=True, deprecated=True,
# ) )
# 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[Union[BaseInvocation.get_invocations()], Field(discriminator="type")] = Body( # type: ignore
# description="The node to add" 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)
# if session is None: if session is None:
# raise HTTPException(status_code=404) raise HTTPException(status_code=404)
# try: try:
# session.add_node(node) session.add_node(node)
# ApiDependencies.invoker.services.graph_execution_manager.set( ApiDependencies.invoker.services.graph_execution_manager.set(
# session session
# ) # TODO: can this be done automatically, or add node through an API? ) # TODO: can this be done automatically, or add node through an API?
# return session.id return session.id
# except NodeAlreadyExecutedError: except NodeAlreadyExecutedError:
# raise HTTPException(status_code=400) raise HTTPException(status_code=400)
# except IndexError: except IndexError:
# raise HTTPException(status_code=400) raise HTTPException(status_code=400)
# @session_router.put( @session_router.put(
# "/{session_id}/nodes/{node_path}", "/{session_id}/nodes/{node_path}",
# operation_id="update_node", operation_id="update_node",
# responses={ responses={
# 200: {"model": GraphExecutionState}, 200: {"model": GraphExecutionState},
# 400: {"description": "Invalid node or link"}, 400: {"description": "Invalid node or link"},
# 404: {"description": "Session not found"}, 404: {"description": "Session not found"},
# }, },
# deprecated=True, deprecated=True,
# ) )
# 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[Union[BaseInvocation.get_invocations()], Field(discriminator="type")] = Body( # type: ignore
# description="The new node" 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)
# if session is None: if session is None:
# raise HTTPException(status_code=404) raise HTTPException(status_code=404)
# try: try:
# session.update_node(node_path, node) session.update_node(node_path, node)
# ApiDependencies.invoker.services.graph_execution_manager.set( ApiDependencies.invoker.services.graph_execution_manager.set(
# session session
# ) # TODO: can this be done automatically, or add node through an API? ) # TODO: can this be done automatically, or add node through an API?
# return session return session
# except NodeAlreadyExecutedError: except NodeAlreadyExecutedError:
# raise HTTPException(status_code=400) raise HTTPException(status_code=400)
# except IndexError: except IndexError:
# raise HTTPException(status_code=400) raise HTTPException(status_code=400)
# @session_router.delete( @session_router.delete(
# "/{session_id}/nodes/{node_path}", "/{session_id}/nodes/{node_path}",
# operation_id="delete_node", operation_id="delete_node",
# responses={ responses={
# 200: {"model": GraphExecutionState}, 200: {"model": GraphExecutionState},
# 400: {"description": "Invalid node or link"}, 400: {"description": "Invalid node or link"},
# 404: {"description": "Session not found"}, 404: {"description": "Session not found"},
# }, },
# deprecated=True, deprecated=True,
# ) )
# async def delete_node( async def delete_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 to delete"), node_path: str = Path(description="The path to the node to delete"),
# ) -> GraphExecutionState: ) -> GraphExecutionState:
# """Deletes a node in the graph and removes all linked edges""" """Deletes 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)
# if session is None: if session is None:
# raise HTTPException(status_code=404) raise HTTPException(status_code=404)
# try: try:
# session.delete_node(node_path) session.delete_node(node_path)
# ApiDependencies.invoker.services.graph_execution_manager.set( ApiDependencies.invoker.services.graph_execution_manager.set(
# session session
# ) # TODO: can this be done automatically, or add node through an API? ) # TODO: can this be done automatically, or add node through an API?
# return session return session
# except NodeAlreadyExecutedError: except NodeAlreadyExecutedError:
# raise HTTPException(status_code=400) raise HTTPException(status_code=400)
# except IndexError: except IndexError:
# raise HTTPException(status_code=400) raise HTTPException(status_code=400)
# @session_router.post( @session_router.post(
# "/{session_id}/edges", "/{session_id}/edges",
# operation_id="add_edge", operation_id="add_edge",
# responses={ responses={
# 200: {"model": GraphExecutionState}, 200: {"model": GraphExecutionState},
# 400: {"description": "Invalid node or link"}, 400: {"description": "Invalid node or link"},
# 404: {"description": "Session not found"}, 404: {"description": "Session not found"},
# }, },
# deprecated=True, deprecated=True,
# ) )
# async def add_edge( async def add_edge(
# session_id: str = Path(description="The id of the session"), session_id: str = Path(description="The id of the session"),
# edge: Edge = Body(description="The edge to add"), edge: Edge = Body(description="The edge to add"),
# ) -> GraphExecutionState: ) -> GraphExecutionState:
# """Adds an edge to the graph""" """Adds an edge to the graph"""
# session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id) session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id)
# if session is None: if session is None:
# raise HTTPException(status_code=404) raise HTTPException(status_code=404)
# try: try:
# session.add_edge(edge) session.add_edge(edge)
# ApiDependencies.invoker.services.graph_execution_manager.set( ApiDependencies.invoker.services.graph_execution_manager.set(
# session session
# ) # TODO: can this be done automatically, or add node through an API? ) # TODO: can this be done automatically, or add node through an API?
# return session return session
# except NodeAlreadyExecutedError: except NodeAlreadyExecutedError:
# raise HTTPException(status_code=400) raise HTTPException(status_code=400)
# except IndexError: except IndexError:
# raise HTTPException(status_code=400) raise HTTPException(status_code=400)
# # TODO: the edge being in the path here is really ugly, find a better solution # TODO: the edge being in the path here is really ugly, find a better solution
# @session_router.delete( @session_router.delete(
# "/{session_id}/edges/{from_node_id}/{from_field}/{to_node_id}/{to_field}", "/{session_id}/edges/{from_node_id}/{from_field}/{to_node_id}/{to_field}",
# operation_id="delete_edge", operation_id="delete_edge",
# responses={ responses={
# 200: {"model": GraphExecutionState}, 200: {"model": GraphExecutionState},
# 400: {"description": "Invalid node or link"}, 400: {"description": "Invalid node or link"},
# 404: {"description": "Session not found"}, 404: {"description": "Session not found"},
# }, },
# deprecated=True, deprecated=True,
# ) )
# async def delete_edge( async def delete_edge(
# session_id: str = Path(description="The id of the session"), session_id: str = Path(description="The id of the session"),
# from_node_id: str = Path(description="The id of the node the edge is coming from"), from_node_id: str = Path(description="The id of the node the edge is coming from"),
# from_field: str = Path(description="The field of the node the edge is coming from"), from_field: str = Path(description="The field of the node the edge is coming from"),
# to_node_id: str = Path(description="The id of the node the edge is going to"), to_node_id: str = Path(description="The id of the node the edge is going to"),
# to_field: str = Path(description="The field of the node the edge is going to"), to_field: str = Path(description="The field of the node the edge is going to"),
# ) -> GraphExecutionState: ) -> GraphExecutionState:
# """Deletes an edge from the graph""" """Deletes an edge from the graph"""
# session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id) session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id)
# if session is None: if session is None:
# raise HTTPException(status_code=404) raise HTTPException(status_code=404)
# 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(
# session session
# ) # TODO: can this be done automatically, or add node through an API? ) # TODO: can this be done automatically, or add node through an API?
# return session return session
# except NodeAlreadyExecutedError: except NodeAlreadyExecutedError:
# raise HTTPException(status_code=400) raise HTTPException(status_code=400)
# except IndexError: except IndexError:
# raise HTTPException(status_code=400) raise HTTPException(status_code=400)
# @session_router.put( @session_router.put(
# "/{session_id}/invoke", "/{session_id}/invoke",
# operation_id="invoke_session", operation_id="invoke_session",
# responses={ responses={
# 200: {"model": None}, 200: {"model": None},
# 202: {"description": "The invocation is queued"}, 202: {"description": "The invocation is queued"},
# 400: {"description": "The session has no invocations ready to invoke"}, 400: {"description": "The session has no invocations ready to invoke"},
# 404: {"description": "Session not found"}, 404: {"description": "Session not found"},
# }, },
# deprecated=True, deprecated=True,
# ) )
# async def invoke_session( async def invoke_session(
# queue_id: str = Query(description="The id of the queue to associate the session with"), queue_id: str = Query(description="The id of the queue to associate the session with"),
# 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)
# if session is None: if session is None:
# raise HTTPException(status_code=404) raise HTTPException(status_code=404)
# if session.is_complete(): if session.is_complete():
# raise HTTPException(status_code=400) raise HTTPException(status_code=400)
# ApiDependencies.invoker.invoke(queue_id, session, invoke_all=all) ApiDependencies.invoker.invoke(queue_id, session, invoke_all=all)
# return Response(status_code=202) return Response(status_code=202)
# @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"}},
# deprecated=True, deprecated=True,
# ) )
# 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"),
# ) -> Response: ) -> Response:
# """Invokes a session""" """Invokes a session"""
# ApiDependencies.invoker.cancel(session_id) ApiDependencies.invoker.cancel(session_id)
# return Response(status_code=202) return Response(status_code=202)

View File

@ -1,4 +1,4 @@
from typing import Optional, Union from typing import Optional
from dynamicprompts.generators import CombinatorialPromptGenerator, RandomPromptGenerator from dynamicprompts.generators import CombinatorialPromptGenerator, RandomPromptGenerator
from fastapi import Body from fastapi import Body
@ -27,7 +27,6 @@ async def parse_dynamicprompts(
combinatorial: bool = Body(default=True, description="Whether to use the combinatorial generator"), combinatorial: bool = Body(default=True, description="Whether to use the combinatorial generator"),
) -> DynamicPromptsResponse: ) -> DynamicPromptsResponse:
"""Creates a batch process""" """Creates a batch process"""
generator: Union[RandomPromptGenerator, CombinatorialPromptGenerator]
try: try:
error: Optional[str] = None error: Optional[str] = None
if combinatorial: if combinatorial:

View File

@ -1,20 +0,0 @@
from fastapi import APIRouter, Path
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.invocations.baseinvocation import WorkflowField
workflows_router = APIRouter(prefix="/v1/workflows", tags=["workflows"])
@workflows_router.get(
"/i/{workflow_id}",
operation_id="get_workflow",
responses={
200: {"model": WorkflowField},
},
)
async def get_workflow(
workflow_id: str = Path(description="The workflow to get"),
) -> WorkflowField:
"""Gets a workflow"""
return ApiDependencies.invoker.services.workflow_records.get(workflow_id)

View File

@ -3,19 +3,16 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi_events.handlers.local import local_handler from fastapi_events.handlers.local import local_handler
from fastapi_events.typing import Event from fastapi_events.typing import Event
from socketio import ASGIApp, AsyncServer from fastapi_socketio import SocketManager
from ..services.events.events_base import EventServiceBase from ..services.events import EventServiceBase
class SocketIO: class SocketIO:
__sio: AsyncServer __sio: SocketManager
__app: ASGIApp
def __init__(self, app: FastAPI): def __init__(self, app: FastAPI):
self.__sio = AsyncServer(async_mode="asgi", cors_allowed_origins="*") self.__sio = SocketManager(app=app)
self.__app = ASGIApp(socketio_server=self.__sio, socketio_path="socket.io")
app.mount("/ws", self.__app)
self.__sio.on("subscribe_queue", handler=self._handle_sub_queue) self.__sio.on("subscribe_queue", handler=self._handle_sub_queue)
self.__sio.on("unsubscribe_queue", handler=self._handle_unsub_queue) self.__sio.on("unsubscribe_queue", handler=self._handle_unsub_queue)
@ -30,8 +27,8 @@ class SocketIO:
async def _handle_sub_queue(self, sid, data, *args, **kwargs): async def _handle_sub_queue(self, sid, data, *args, **kwargs):
if "queue_id" in data: if "queue_id" in data:
await self.__sio.enter_room(sid, data["queue_id"]) self.__sio.enter_room(sid, data["queue_id"])
async def _handle_unsub_queue(self, sid, data, *args, **kwargs): async def _handle_unsub_queue(self, sid, data, *args, **kwargs):
if "queue_id" in data: if "queue_id" in data:
await self.__sio.leave_room(sid, data["queue_id"]) self.__sio.enter_room(sid, data["queue_id"])

View File

@ -1,7 +1,3 @@
from typing import Any
from fastapi.responses import HTMLResponse
from .services.config import InvokeAIAppConfig from .services.config import InvokeAIAppConfig
# parse_args() must be called before any other imports. if it is not called first, consumers of the config # parse_args() must be called before any other imports. if it is not called first, consumers of the config
@ -12,25 +8,23 @@ app_config.parse_args()
if True: # hack to make flake8 happy with imports coming after setting up the config if True: # hack to make flake8 happy with imports coming after setting up the config
import asyncio import asyncio
import logging
import mimetypes import mimetypes
import socket import socket
from inspect import signature from inspect import signature
from pathlib import Path from pathlib import Path
import torch
import uvicorn import uvicorn
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
from fastapi.openapi.utils import get_openapi from fastapi.openapi.utils import get_openapi
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi_events.handlers.local import local_handler from fastapi_events.handlers.local import local_handler
from fastapi_events.middleware import EventHandlerASGIMiddleware from fastapi_events.middleware import EventHandlerASGIMiddleware
from pydantic.json_schema import models_json_schema from pydantic.schema import schema
from torch.backends.mps import is_available as is_mps_available
# for PyCharm:
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
import invokeai.backend.util.hotfixes # noqa: F401 (monkeypatching on import) import invokeai.backend.util.hotfixes # noqa: F401 (monkeypatching on import)
import invokeai.frontend.web as web_dir import invokeai.frontend.web as web_dir
@ -38,28 +32,17 @@ if True: # hack to make flake8 happy with imports coming after setting up the c
from ..backend.util.logging import InvokeAILogger from ..backend.util.logging import InvokeAILogger
from .api.dependencies import ApiDependencies from .api.dependencies import ApiDependencies
from .api.routers import ( from .api.routers import app_info, board_images, boards, images, models, session_queue, sessions, utilities
app_info,
board_images,
boards,
images,
model_records,
models,
session_queue,
sessions,
utilities,
workflows,
)
from .api.sockets import SocketIO from .api.sockets import SocketIO
from .invocations.baseinvocation import BaseInvocation, UIConfigBase, _InputField, _OutputField from .invocations.baseinvocation import BaseInvocation, UIConfigBase, _InputField, _OutputField
if is_mps_available(): if torch.backends.mps.is_available():
# noinspection PyUnresolvedReferences
import invokeai.backend.util.mps_fixes # noqa: F401 (monkeypatching on import) import invokeai.backend.util.mps_fixes # noqa: F401 (monkeypatching on import)
app_config = InvokeAIAppConfig.get_config() logger = InvokeAILogger.getLogger(config=app_config)
app_config.parse_args()
logger = InvokeAILogger.get_logger(config=app_config)
# 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")
@ -67,7 +50,7 @@ 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?
app = FastAPI(title="Invoke AI", docs_url=None, redoc_url=None, separate_input_output_schemas=False) app = FastAPI(title="Invoke AI", docs_url=None, redoc_url=None)
# Add event handler # Add event handler
event_handler_id: int = id(app) event_handler_id: int = id(app)
@ -79,46 +62,53 @@ app.add_middleware(
socket_io = SocketIO(app) socket_io = SocketIO(app)
app.add_middleware(
CORSMiddleware,
allow_origins=app_config.allow_origins,
allow_credentials=app_config.allow_credentials,
allow_methods=app_config.allow_methods,
allow_headers=app_config.allow_headers,
)
app.add_middleware(GZipMiddleware, minimum_size=1000)
# Add startup event to load dependencies # Add startup event to load dependencies
@app.on_event("startup") @app.on_event("startup")
async def startup_event() -> None: async def startup_event():
app.add_middleware(
CORSMiddleware,
allow_origins=app_config.allow_origins,
allow_credentials=app_config.allow_credentials,
allow_methods=app_config.allow_methods,
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
@app.on_event("shutdown") @app.on_event("shutdown")
async def shutdown_event() -> None: async def shutdown_event():
ApiDependencies.shutdown() ApiDependencies.shutdown()
# Include all routers # Include all routers
# TODO: REMOVE
# app.include_router(
# invocation.invocation_router,
# prefix = '/api')
app.include_router(sessions.session_router, prefix="/api") app.include_router(sessions.session_router, prefix="/api")
app.include_router(utilities.utilities_router, prefix="/api") app.include_router(utilities.utilities_router, prefix="/api")
app.include_router(models.models_router, prefix="/api") app.include_router(models.models_router, prefix="/api")
app.include_router(model_records.model_records_router, prefix="/api")
app.include_router(images.images_router, prefix="/api") app.include_router(images.images_router, prefix="/api")
app.include_router(boards.boards_router, prefix="/api") 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")
app.include_router(session_queue.session_queue_router, prefix="/api") app.include_router(session_queue.session_queue_router, prefix="/api")
app.include_router(workflows.workflows_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?
def custom_openapi() -> dict[str, Any]: def custom_openapi():
if app.openapi_schema: if app.openapi_schema:
return app.openapi_schema return app.openapi_schema
openapi_schema = get_openapi( openapi_schema = get_openapi(
@ -126,43 +116,39 @@ def custom_openapi() -> dict[str, Any]:
description="An API for invoking AI image operations", description="An API for invoking AI image operations",
version="1.0.0", version="1.0.0",
routes=app.routes, routes=app.routes,
separate_input_output_schemas=False, # https://fastapi.tiangolo.com/how-to/separate-openapi-schemas/
) )
# Add all outputs # Add all outputs
all_invocations = BaseInvocation.get_invocations() all_invocations = BaseInvocation.get_invocations()
output_types = set() output_types = set()
output_type_titles = {} output_type_titles = dict()
for invoker in all_invocations: for invoker in all_invocations:
output_type = signature(invoker.invoke).return_annotation output_type = signature(invoker.invoke).return_annotation
output_types.add(output_type) output_types.add(output_type)
output_schemas = models_json_schema( output_schemas = schema(output_types, ref_prefix="#/components/schemas/")
models=[(o, "serialization") for o in output_types], ref_template="#/components/schemas/{model}" for schema_key, output_schema in output_schemas["definitions"].items():
) output_schema["class"] = "output"
for schema_key, output_schema in output_schemas[1]["$defs"].items(): openapi_schema["components"]["schemas"][schema_key] = output_schema
# TODO: note that we assume the schema_key here is the TYPE.__name__ # TODO: note that we assume the schema_key here is the TYPE.__name__
# This could break in some cases, figure out a better way to do it # This could break in some cases, figure out a better way to do it
output_type_titles[schema_key] = output_schema["title"] output_type_titles[schema_key] = output_schema["title"]
# Add Node Editor UI helper schemas # Add Node Editor UI helper schemas
ui_config_schemas = models_json_schema( ui_config_schemas = schema([UIConfigBase, _InputField, _OutputField], ref_prefix="#/components/schemas/")
[(UIConfigBase, "serialization"), (_InputField, "serialization"), (_OutputField, "serialization")], for schema_key, ui_config_schema in ui_config_schemas["definitions"].items():
ref_template="#/components/schemas/{model}",
)
for schema_key, ui_config_schema in ui_config_schemas[1]["$defs"].items():
openapi_schema["components"]["schemas"][schema_key] = ui_config_schema openapi_schema["components"]["schemas"][schema_key] = ui_config_schema
# Add a reference to the output type to additionalProperties of the invoker schema # Add a reference to the output type to additionalProperties of the invoker schema
for invoker in all_invocations: for invoker in all_invocations:
invoker_name = invoker.__name__ invoker_name = invoker.__name__
output_type = signature(obj=invoker.invoke).return_annotation output_type = signature(invoker.invoke).return_annotation
output_type_title = output_type_titles[output_type.__name__] output_type_title = output_type_titles[output_type.__name__]
invoker_schema = openapi_schema["components"]["schemas"][f"{invoker_name}"] invoker_schema = openapi_schema["components"]["schemas"][invoker_name]
outputs_ref = {"$ref": f"#/components/schemas/{output_type_title}"} outputs_ref = {"$ref": f"#/components/schemas/{output_type_title}"}
invoker_schema["output"] = outputs_ref invoker_schema["output"] = outputs_ref
invoker_schema["class"] = "invocation" invoker_schema["class"] = "invocation"
openapi_schema["components"]["schemas"][f"{output_type_title}"]["class"] = "output"
from invokeai.backend.model_management.models import get_model_config_enums from invokeai.backend.model_management.models import get_model_config_enums
@ -173,56 +159,48 @@ def custom_openapi() -> dict[str, Any]:
# print(f"Config with name {name} already defined") # print(f"Config with name {name} already defined")
continue continue
openapi_schema["components"]["schemas"][name] = { # "BaseModelType":{"title":"BaseModelType","description":"An enumeration.","enum":["sd-1","sd-2"],"type":"string"}
"title": name, openapi_schema["components"]["schemas"][name] = dict(
"description": "An enumeration.", title=name,
"type": "string", description="An enumeration.",
"enum": [v.value for v in model_config_format_enum], type="string",
} enum=list(v.value for v in model_config_format_enum),
)
app.openapi_schema = openapi_schema app.openapi_schema = openapi_schema
return app.openapi_schema return app.openapi_schema
app.openapi = custom_openapi # type: ignore [method-assign] # this is a valid assignment app.openapi = custom_openapi
# Override API doc favicons
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() -> HTMLResponse: def overridden_swagger():
return get_swagger_ui_html( return get_swagger_ui_html(
openapi_url=app.openapi_url, # type: ignore [arg-type] # this is always a string openapi_url=app.openapi_url,
title=app.title, title=app.title,
swagger_favicon_url="/static/docs/favicon.ico", swagger_favicon_url="/static/favicon.ico",
) )
@app.get("/redoc", include_in_schema=False) @app.get("/redoc", include_in_schema=False)
def overridden_redoc() -> HTMLResponse: def overridden_redoc():
return get_redoc_html( return get_redoc_html(
openapi_url=app.openapi_url, # type: ignore [arg-type] # this is always a string openapi_url=app.openapi_url,
title=app.title, title=app.title,
redoc_favicon_url="/static/docs/favicon.ico", redoc_favicon_url="/static/favicon.ico",
) )
web_root_path = Path(list(web_dir.__path__)[0]) # Must mount *after* the other routes else it borks em
app.mount("/", StaticFiles(directory=Path(web_dir.__path__[0], "dist"), html=True), name="ui")
# Cannot add headers to StaticFiles, so we must serve index.html with a custom route def invoke_api():
# Add cache-control: no-store header to prevent caching of index.html, which leads to broken UIs at release def find_port(port: int):
@app.get("/", include_in_schema=False, name="ui_root")
def get_index() -> FileResponse:
return FileResponse(Path(web_root_path, "dist/index.html"), headers={"Cache-Control": "no-store"})
# # Must mount *after* the other routes else it borks em
app.mount("/static", StaticFiles(directory=Path(web_root_path, "static/")), name="static") # docs favicon is in here
app.mount("/assets", StaticFiles(directory=Path(web_root_path, "dist/assets/")), name="assets")
app.mount("/locales", StaticFiles(directory=Path(web_root_path, "dist/locales/")), name="locales")
def invoke_api() -> None:
def find_port(port: int) -> int:
"""Find a port not in use starting at given port""" """Find a port not in use starting at given port"""
# Taken from https://waylonwalker.com/python-find-available-port/, thanks Waylon! # Taken from https://waylonwalker.com/python-find-available-port/, thanks Waylon!
# https://github.com/WaylonWalker # https://github.com/WaylonWalker
@ -245,7 +223,7 @@ def invoke_api() -> None:
exc_info=e, exc_info=e,
) )
else: else:
jurigged.watch(logger=InvokeAILogger.get_logger(name="jurigged").info) jurigged.watch(logger=InvokeAILogger.getLogger(name="jurigged").info)
port = find_port(app_config.port) port = find_port(app_config.port)
if port != app_config.port: if port != app_config.port:
@ -257,14 +235,14 @@ def invoke_api() -> None:
app=app, app=app,
host=app_config.host, host=app_config.host,
port=port, port=port,
loop="asyncio", loop=loop,
log_level=app_config.log_level, log_level=app_config.log_level,
) )
server = uvicorn.Server(config) server = uvicorn.Server(config)
# replace uvicorn's loggers with InvokeAI's for consistent appearance # replace uvicorn's loggers with InvokeAI's for consistent appearance
for logname in ["uvicorn.access", "uvicorn"]: for logname in ["uvicorn.access", "uvicorn"]:
log = InvokeAILogger.get_logger(logname) log = logging.getLogger(logname)
log.handlers.clear() log.handlers.clear()
for ch in logger.handlers: for ch in logger.handlers:
log.addHandler(ch) log.addHandler(ch)

View File

@ -0,0 +1,313 @@
# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654)
import argparse
from abc import ABC, abstractmethod
from typing import Any, Callable, Iterable, Literal, Union, get_args, get_origin, get_type_hints
import matplotlib.pyplot as plt
import networkx as nx
from pydantic import BaseModel, Field
import invokeai.backend.util.logging as logger
from ..invocations.baseinvocation import BaseInvocation
from ..invocations.image import ImageField
from ..services.graph import Edge, GraphExecutionState, LibraryGraph
from ..services.invoker import Invoker
def add_field_argument(command_parser, name: str, field, default_override=None):
default = (
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:
allowed_values = get_args(field.type_)
allowed_types = set()
for val in allowed_values:
allowed_types.add(type(val))
allowed_types_list = list(allowed_types)
field_type = allowed_types_list[0] if len(allowed_types) == 1 else Union[allowed_types_list] # type: ignore
command_parser.add_argument(
f"--{name}",
dest=name,
type=field_type,
default=default,
choices=allowed_values,
help=field.field_info.description,
)
else:
command_parser.add_argument(
f"--{name}",
dest=name,
type=field.type_,
default=default,
help=field.field_info.description,
)
def add_parsers(
subparsers,
commands: list[type],
command_field: str = "type",
exclude_fields: list[str] = ["id", "type"],
add_arguments: Union[Callable[[argparse.ArgumentParser], None], None] = None,
):
"""Adds parsers for each command to the subparsers"""
# Create subparsers for each command
for command in commands:
hints = get_type_hints(command)
cmd_name = get_args(hints[command_field])[0]
command_parser = subparsers.add_parser(cmd_name, help=command.__doc__)
if add_arguments is not None:
add_arguments(command_parser)
# Convert all fields to arguments
fields = command.__fields__ # type: ignore
for name, field in fields.items():
if name in exclude_fields:
continue
add_field_argument(command_parser, name, field)
def add_graph_parsers(
subparsers, graphs: list[LibraryGraph], add_arguments: Union[Callable[[argparse.ArgumentParser], None], None] = None
):
for graph in graphs:
command_parser = subparsers.add_parser(graph.name, help=graph.description)
if add_arguments is not None:
add_arguments(command_parser)
# Add arguments for inputs
for exposed_input in graph.exposed_inputs:
node = graph.graph.get_node(exposed_input.node_path)
field = node.__fields__[exposed_input.field]
default_override = getattr(node, exposed_input.field)
add_field_argument(command_parser, exposed_input.alias, field, default_override)
class CliContext:
invoker: Invoker
session: GraphExecutionState
parser: argparse.ArgumentParser
defaults: dict[str, Any]
graph_nodes: dict[str, str]
nodes_added: list[str]
def __init__(self, invoker: Invoker, session: GraphExecutionState, parser: argparse.ArgumentParser):
self.invoker = invoker
self.session = session
self.parser = parser
self.defaults = dict()
self.graph_nodes = dict()
self.nodes_added = list()
def get_session(self):
self.session = self.invoker.services.graph_execution_manager.get(self.session.id)
return self.session
def reset(self):
self.session = self.invoker.create_execution_state()
self.graph_nodes = dict()
self.nodes_added = list()
# Leave defaults unchanged
def add_node(self, node: BaseInvocation):
self.get_session()
self.session.graph.add_node(node)
self.nodes_added.append(node.id)
self.invoker.services.graph_execution_manager.set(self.session)
def add_edge(self, edge: Edge):
self.get_session()
self.session.add_edge(edge)
self.invoker.services.graph_execution_manager.set(self.session)
class ExitCli(Exception):
"""Exception to exit the CLI"""
pass
class BaseCommand(ABC, BaseModel):
"""A CLI command"""
# All commands must include a type name like this:
# type: Literal['your_command_name'] = 'your_command_name'
@classmethod
def get_all_subclasses(cls):
subclasses = []
toprocess = [cls]
while len(toprocess) > 0:
next = toprocess.pop(0)
next_subclasses = next.__subclasses__()
subclasses.extend(next_subclasses)
toprocess.extend(next_subclasses)
return subclasses
@classmethod
def get_commands(cls):
return tuple(BaseCommand.get_all_subclasses())
@classmethod
def get_commands_map(cls):
# 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()))
@abstractmethod
def run(self, context: CliContext) -> None:
"""Run the command. Raise ExitCli to exit."""
pass
class ExitCommand(BaseCommand):
"""Exits the CLI"""
type: Literal["exit"] = "exit"
def run(self, context: CliContext) -> None:
raise ExitCli()
class HelpCommand(BaseCommand):
"""Shows help"""
type: Literal["help"] = "help"
def run(self, context: CliContext) -> None:
context.parser.print_help()
def get_graph_execution_history(
graph_execution_state: GraphExecutionState,
) -> Iterable[str]:
"""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)
def get_invocation_command(invocation) -> str:
fields = invocation.__fields__.items()
type_hints = get_type_hints(type(invocation))
command = [invocation.type]
for name, field in fields:
if name in ["id", "type"]:
continue
# TODO: add links
# Skip image fields when serializing command
type_hint = type_hints.get(name) or None
if type_hint is ImageField or ImageField in get_args(type_hint):
continue
field_value = getattr(invocation, name)
field_default = field.default
if field_value != field_default:
if type_hint is str or str in get_args(type_hint):
command.append(f'--{name} "{field_value}"')
else:
command.append(f"--{name} {field_value}")
return " ".join(command)
class HistoryCommand(BaseCommand):
"""Shows the invocation history"""
type: Literal["history"] = "history"
# Inputs
# fmt: off
count: int = Field(default=5, gt=0, description="The number of history entries to show")
# fmt: on
def run(self, context: CliContext) -> None:
history = list(get_graph_execution_history(context.get_session()))
for i in range(min(self.count, len(history))):
entry_id = history[-1 - i]
entry = context.get_session().graph.get_node(entry_id)
logger.info(f"{entry_id}: {get_invocation_command(entry)}")
class SetDefaultCommand(BaseCommand):
"""Sets a default value for a field"""
type: Literal["default"] = "default"
# Inputs
# fmt: off
field: str = Field(description="The field to set the default for")
value: str = Field(description="The value to set the default to, or None to clear the default")
# fmt: on
def run(self, context: CliContext) -> None:
if self.value is None:
if self.field in context.defaults:
del context.defaults[self.field]
else:
context.defaults[self.field] = self.value
class DrawGraphCommand(BaseCommand):
"""Debugs a graph"""
type: Literal["draw_graph"] = "draw_graph"
def run(self, context: CliContext) -> None:
session: GraphExecutionState = context.invoker.services.graph_execution_manager.get(context.session.id)
nxgraph = session.graph.nx_graph_flat()
# Draw the networkx graph
plt.figure(figsize=(20, 20))
pos = nx.spectral_layout(nxgraph)
nx.draw_networkx_nodes(nxgraph, pos, node_size=1000)
nx.draw_networkx_edges(nxgraph, pos, width=2)
nx.draw_networkx_labels(nxgraph, pos, font_size=20, font_family="sans-serif")
plt.axis("off")
plt.show()
class DrawExecutionGraphCommand(BaseCommand):
"""Debugs an execution graph"""
type: Literal["draw_xgraph"] = "draw_xgraph"
def run(self, context: CliContext) -> None:
session: GraphExecutionState = context.invoker.services.graph_execution_manager.get(context.session.id)
nxgraph = session.execution_graph.nx_graph_flat()
# Draw the networkx graph
plt.figure(figsize=(20, 20))
pos = nx.spectral_layout(nxgraph)
nx.draw_networkx_nodes(nxgraph, pos, node_size=1000)
nx.draw_networkx_edges(nxgraph, pos, width=2)
nx.draw_networkx_labels(nxgraph, pos, font_size=20, font_family="sans-serif")
plt.axis("off")
plt.show()
class SortedHelpFormatter(argparse.HelpFormatter):
def _iter_indented_subactions(self, action):
try:
get_subactions = action._get_subactions
except AttributeError:
pass
else:
self._indent()
if isinstance(action, argparse._SubParsersAction):
for subaction in sorted(get_subactions(), key=lambda x: x.dest):
yield subaction
else:
for subaction in get_subactions():
yield subaction
self._dedent()

View File

@ -0,0 +1,171 @@
"""
Readline helper functions for cli_app.py
You may import the global singleton `completer` to get access to the
completer object.
"""
import atexit
import readline
import shlex
from pathlib import Path
from typing import Dict, List, Literal, get_args, get_origin, get_type_hints
import invokeai.backend.util.logging as logger
from ...backend import ModelManager
from ..invocations.baseinvocation import BaseInvocation
from ..services.invocation_services import InvocationServices
from .commands import BaseCommand
# singleton object, class variable
completer = None
class Completer(object):
def __init__(self, model_manager: ModelManager):
self.commands = self.get_commands()
self.matches = None
self.linebuffer = None
self.manager = model_manager
return
def complete(self, text, state):
"""
Complete commands and switches fromm the node CLI command line.
Switches are determined in a context-specific manner.
"""
buffer = readline.get_line_buffer()
if state == 0:
options = None
try:
current_command, current_switch = self.get_current_command(buffer)
options = self.get_command_options(current_command, current_switch)
except IndexError:
pass
options = options or list(self.parse_commands().keys())
if not text: # first time
self.matches = options
else:
self.matches = [s for s in options if s and s.startswith(text)]
try:
match = self.matches[state]
except IndexError:
match = None
return match
@classmethod
def get_commands(self) -> List[object]:
"""
Return a list of all the client commands and invocations.
"""
return BaseCommand.get_commands() + BaseInvocation.get_invocations()
def get_current_command(self, buffer: str) -> tuple[str, str]:
"""
Parse the readline buffer to find the most recent command and its switch.
"""
if len(buffer) == 0:
return None, None
tokens = shlex.split(buffer)
command = None
switch = None
for t in tokens:
if t[0].isalpha():
if switch is None:
command = t
else:
switch = t
# don't try to autocomplete switches that are already complete
if switch and buffer.endswith(" "):
switch = None
return command or "", switch or ""
def parse_commands(self) -> Dict[str, List[str]]:
"""
Return a dict in which the keys are the command name
and the values are the parameters the command takes.
"""
result = dict()
for command in self.commands:
hints = get_type_hints(command)
name = get_args(hints["type"])[0]
result.update({name: hints})
return result
def get_command_options(self, command: str, switch: str) -> List[str]:
"""
Return all the parameters that can be passed to the command as
command-line switches. Returns None if the command is unrecognized.
"""
parsed_commands = self.parse_commands()
if command not in parsed_commands:
return None
# handle switches in the format "-foo=bar"
argument = None
if switch and "=" in switch:
switch, argument = switch.split("=")
parameter = switch.strip("-")
if parameter in parsed_commands[command]:
if argument is None:
return self.get_parameter_options(parameter, parsed_commands[command][parameter])
else:
return [
f"--{parameter}={x}"
for x in self.get_parameter_options(parameter, parsed_commands[command][parameter])
]
else:
return [f"--{x}" for x in parsed_commands[command].keys()]
def get_parameter_options(self, parameter: str, typehint) -> List[str]:
"""
Given a parameter type (such as Literal), offers autocompletions.
"""
if get_origin(typehint) == Literal:
return get_args(typehint)
if parameter == "model":
return self.manager.model_names()
def _pre_input_hook(self):
if self.linebuffer:
readline.insert_text(self.linebuffer)
readline.redisplay()
self.linebuffer = None
def set_autocompleter(services: InvocationServices) -> Completer:
global completer
if completer:
return completer
completer = Completer(services.model_manager)
readline.set_completer(completer.complete)
try:
readline.set_auto_history(True)
except AttributeError:
# pyreadline3 does not have a set_auto_history() method
pass
readline.set_pre_input_hook(completer._pre_input_hook)
readline.set_completer_delims(" ")
readline.parse_and_bind("tab: complete")
readline.parse_and_bind("set print-completions-horizontally off")
readline.parse_and_bind("set page-completions on")
readline.parse_and_bind("set skip-completed-text on")
readline.parse_and_bind("set show-all-if-ambiguous on")
histfile = Path(services.configuration.root_dir / ".invoke_history")
try:
readline.read_history_file(histfile)
readline.set_history_length(1000)
except FileNotFoundError:
pass
except OSError: # file likely corrupted
newname = f"{histfile}.old"
logger.error(f"Your history file {histfile} couldn't be loaded and may be corrupted. Renaming it to {newname}")
histfile.replace(Path(newname))
atexit.register(readline.write_history_file, histfile)

485
invokeai/app/cli_app.py Normal file
View File

@ -0,0 +1,485 @@
# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI Team
from invokeai.app.services.invocation_cache.invocation_cache_memory import MemoryInvocationCache
from .services.config import InvokeAIAppConfig
# parse_args() must be called before any other imports. if it is not called first, consumers of the config
# which are imported/used before parse_args() is called will get the default config values instead of the
# values from the command line or config file.
config = InvokeAIAppConfig.get_config()
config.parse_args()
if True: # hack to make flake8 happy with imports coming after setting up the config
import argparse
import re
import shlex
import sqlite3
import sys
import time
from typing import Optional, Union, get_type_hints
import torch
from pydantic import BaseModel, ValidationError
from pydantic.fields import Field
import invokeai.backend.util.hotfixes # noqa: F401 (monkeypatching on import)
from invokeai.app.services.board_image_record_storage import SqliteBoardImageRecordStorage
from invokeai.app.services.board_images import BoardImagesService, BoardImagesServiceDependencies
from invokeai.app.services.board_record_storage import SqliteBoardRecordStorage
from invokeai.app.services.boards import BoardService, BoardServiceDependencies
from invokeai.app.services.image_record_storage import SqliteImageRecordStorage
from invokeai.app.services.images import ImageService, ImageServiceDependencies
from invokeai.app.services.invocation_stats import InvocationStatsService
from invokeai.app.services.resource_name import SimpleNameService
from invokeai.app.services.urls import LocalUrlService
from invokeai.backend.util.logging import InvokeAILogger
from invokeai.version.invokeai_version import __version__
from .cli.commands import BaseCommand, CliContext, ExitCli, SortedHelpFormatter, add_graph_parsers, add_parsers
from .cli.completer import set_autocompleter
from .invocations.baseinvocation import BaseInvocation
from .services.default_graphs import create_system_graphs, default_text_to_image_graph_id
from .services.events import EventServiceBase
from .services.graph import (
Edge,
EdgeConnection,
GraphExecutionState,
GraphInvocation,
LibraryGraph,
are_connection_types_compatible,
)
from .services.image_file_storage import DiskImageFileStorage
from .services.invocation_queue import MemoryInvocationQueue
from .services.invocation_services import InvocationServices
from .services.invoker import Invoker
from .services.latent_storage import DiskLatentsStorage, ForwardCacheLatentsStorage
from .services.model_manager_service import ModelManagerService
from .services.processor import DefaultInvocationProcessor
from .services.sqlite import SqliteItemStorage
if torch.backends.mps.is_available():
import invokeai.backend.util.mps_fixes # noqa: F401 (monkeypatching on import)
logger = InvokeAILogger().getLogger(config=config)
class CliCommand(BaseModel):
command: Union[BaseCommand.get_commands() + BaseInvocation.get_invocations()] = Field(discriminator="type") # type: ignore
class InvalidArgs(Exception):
pass
def add_invocation_args(command_parser):
# Add linking capability
command_parser.add_argument(
"--link",
"-l",
action="append",
nargs=3,
help="A link in the format 'source_node source_field dest_field'. source_node can be relative to history (e.g. -1)",
)
command_parser.add_argument(
"--link_node",
"-ln",
action="append",
help="A link from all fields in the specified node. Node can be relative to history (e.g. -1)",
)
def get_command_parser(services: InvocationServices) -> argparse.ArgumentParser:
# Create invocation parser
parser = argparse.ArgumentParser(formatter_class=SortedHelpFormatter)
def exit(*args, **kwargs):
raise InvalidArgs
parser.exit = exit
subparsers = parser.add_subparsers(dest="type")
# Create subparsers for each invocation
invocations = BaseInvocation.get_all_subclasses()
add_parsers(subparsers, invocations, add_arguments=add_invocation_args)
# Create subparsers for each command
commands = BaseCommand.get_all_subclasses()
add_parsers(subparsers, commands, exclude_fields=["type"])
# Create subparsers for exposed CLI graphs
# TODO: add a way to identify these graphs
text_to_image = services.graph_library.get(default_text_to_image_graph_id)
add_graph_parsers(subparsers, [text_to_image], add_arguments=add_invocation_args)
return parser
class NodeField:
alias: str
node_path: str
field: str
field_type: type
def __init__(self, alias: str, node_path: str, field: str, field_type: type):
self.alias = alias
self.node_path = node_path
self.field = field
self.field_type = field_type
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()}
def get_node_input_field(graph: LibraryGraph, field_alias: str, node_id: str) -> NodeField:
"""Gets the node field for the specified 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))
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],
)
def get_node_output_field(graph: LibraryGraph, field_alias: str, node_id: str) -> NodeField:
"""Gets the node field for the specified 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_output_type = node_type.get_output_type()
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],
)
def get_node_inputs(invocation: BaseInvocation, context: CliContext) -> dict[str, NodeField]:
"""Gets the inputs for the specified invocation from the context"""
node_type = type(invocation)
if node_type is not GraphInvocation:
return fields_from_type_hints(get_type_hints(node_type), invocation.id)
else:
graph: LibraryGraph = context.invoker.services.graph_library.get(context.graph_nodes[invocation.id])
return {e.alias: get_node_input_field(graph, e.alias, invocation.id) for e in graph.exposed_inputs}
def get_node_outputs(invocation: BaseInvocation, context: CliContext) -> dict[str, NodeField]:
"""Gets the outputs for the specified invocation from the context"""
node_type = type(invocation)
if node_type is not GraphInvocation:
return fields_from_type_hints(get_type_hints(node_type.get_output_type()), invocation.id)
else:
graph: LibraryGraph = context.invoker.services.graph_library.get(context.graph_nodes[invocation.id])
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]:
"""Generates all possible edges between two invocations"""
afields = get_node_outputs(a, context)
bfields = get_node_inputs(b, context)
matching_fields = set(afields.keys()).intersection(bfields.keys())
# Remove invalid fields
invalid_fields = set(["type", "id"])
matching_fields = matching_fields.difference(invalid_fields)
# Validate types
matching_fields = [
f for f in matching_fields if are_connection_types_compatible(afields[f].field_type, bfields[f].field_type)
]
edges = [
Edge(
source=EdgeConnection(node_id=afields[alias].node_path, field=afields[alias].field),
destination=EdgeConnection(node_id=bfields[alias].node_path, field=bfields[alias].field),
)
for alias in matching_fields
]
return edges
class SessionError(Exception):
"""Raised when a session error has occurred"""
pass
def invoke_all(context: CliContext):
"""Runs all invocations in the specified session"""
context.invoker.invoke(context.session, invoke_all=True)
while not context.get_session().is_complete():
# Wait some time
time.sleep(0.1)
# Print any errors
if context.session.has_error():
for n in context.session.errors:
context.invoker.services.logger.error(
f"Error in node {n} (source node {context.session.prepared_source_mapping[n]}): {context.session.errors[n]}"
)
raise SessionError()
def invoke_cli():
logger.info(f"InvokeAI version {__version__}")
# get the optional list of invocations to execute on the command line
parser = config.get_parser()
parser.add_argument("commands", nargs="*")
invocation_commands = parser.parse_args().commands
# get the optional file to read commands from.
# Simplest is to use it for STDIN
if infile := config.from_file:
sys.stdin = open(infile, "r")
model_manager = ModelManagerService(config, logger)
events = EventServiceBase()
output_folder = config.output_path
# TODO: build a file/path manager?
if config.use_memory_db:
db_location = ":memory:"
else:
db_location = config.db_path
db_location.parent.mkdir(parents=True, exist_ok=True)
db_conn = sqlite3.connect(db_location, check_same_thread=False) # TODO: figure out a better threading solution
logger.info(f'InvokeAI database location is "{db_location}"')
graph_execution_manager = SqliteItemStorage[GraphExecutionState](conn=db_conn, table_name="graph_executions")
urls = LocalUrlService()
image_record_storage = SqliteImageRecordStorage(conn=db_conn)
image_file_storage = DiskImageFileStorage(f"{output_folder}/images")
names = SimpleNameService()
board_record_storage = SqliteBoardRecordStorage(conn=db_conn)
board_image_record_storage = SqliteBoardImageRecordStorage(conn=db_conn)
boards = BoardService(
services=BoardServiceDependencies(
board_image_record_storage=board_image_record_storage,
board_record_storage=board_record_storage,
image_record_storage=image_record_storage,
url=urls,
logger=logger,
)
)
board_images = BoardImagesService(
services=BoardImagesServiceDependencies(
board_image_record_storage=board_image_record_storage,
board_record_storage=board_record_storage,
image_record_storage=image_record_storage,
url=urls,
logger=logger,
)
)
images = ImageService(
services=ImageServiceDependencies(
board_image_record_storage=board_image_record_storage,
image_record_storage=image_record_storage,
image_file_storage=image_file_storage,
url=urls,
logger=logger,
names=names,
graph_execution_manager=graph_execution_manager,
)
)
services = InvocationServices(
model_manager=model_manager,
events=events,
latents=ForwardCacheLatentsStorage(DiskLatentsStorage(f"{output_folder}/latents")),
images=images,
boards=boards,
board_images=board_images,
queue=MemoryInvocationQueue(),
graph_library=SqliteItemStorage[LibraryGraph](conn=db_conn, table_name="graphs"),
graph_execution_manager=graph_execution_manager,
processor=DefaultInvocationProcessor(),
performance_statistics=InvocationStatsService(graph_execution_manager),
logger=logger,
configuration=config,
invocation_cache=MemoryInvocationCache(max_cache_size=config.node_cache_size),
)
system_graphs = create_system_graphs(services.graph_library)
system_graph_names = set([g.name for g in system_graphs])
set_autocompleter(services)
invoker = Invoker(services)
session: GraphExecutionState = invoker.create_execution_state()
parser = get_command_parser(services)
re_negid = re.compile("^-[0-9]+$")
# Uncomment to print out previous sessions at startup
# print(services.session_manager.list())
context = CliContext(invoker, session, parser)
set_autocompleter(services)
command_line_args_exist = len(invocation_commands) > 0
done = False
while not done:
try:
if command_line_args_exist:
cmd_input = invocation_commands.pop(0)
done = len(invocation_commands) == 0
else:
cmd_input = input("invoke> ")
except (KeyboardInterrupt, EOFError):
# Ctrl-c exits
break
try:
# Refresh the state of the session
# history = list(get_graph_execution_history(context.session))
history = list(reversed(context.nodes_added))
# Split the command for piping
cmds = cmd_input.split("|")
start_id = len(context.nodes_added)
current_id = start_id
new_invocations = list()
for cmd in cmds:
if cmd is None or cmd.strip() == "":
raise InvalidArgs("Empty command")
# Parse args to create invocation
args = vars(context.parser.parse_args(shlex.split(cmd.strip())))
# Override defaults
for field_name, field_default in context.defaults.items():
if field_name in args:
args[field_name] = field_default
# Parse invocation
command: CliCommand = None # type:ignore
system_graph: Optional[LibraryGraph] = None
if args["type"] in system_graph_names:
system_graph = next(filter(lambda g: g.name == args["type"], system_graphs))
invocation = GraphInvocation(graph=system_graph.graph, id=str(current_id))
for exposed_input in system_graph.exposed_inputs:
if exposed_input.alias in args:
node = invocation.graph.get_node(exposed_input.node_path)
field = exposed_input.field
setattr(node, field, args[exposed_input.alias])
command = CliCommand(command=invocation)
context.graph_nodes[invocation.id] = system_graph.id
else:
args["id"] = current_id
command = CliCommand(command=args)
if command is None:
continue
# Run any CLI commands immediately
if isinstance(command.command, BaseCommand):
# Invoke all current nodes to preserve operation order
invoke_all(context)
# Run the command
command.command.run(context)
continue
# TODO: handle linking with library graphs
# Pipe previous command output (if there was a previous command)
edges: list[Edge] = list()
if len(history) > 0 or current_id != start_id:
from_id = history[0] if current_id == start_id else str(current_id - 1)
from_node = (
next(filter(lambda n: n[0].id == from_id, new_invocations))[0]
if current_id != start_id
else context.session.graph.get_node(from_id)
)
matching_edges = generate_matching_edges(from_node, command.command, context)
edges.extend(matching_edges)
# Parse provided links
if "link_node" in args and args["link_node"]:
for link in args["link_node"]:
node_id = link
if re_negid.match(node_id):
node_id = str(current_id + int(node_id))
link_node = context.session.graph.get_node(node_id)
matching_edges = generate_matching_edges(link_node, command.command, context)
matching_destinations = [e.destination for e in matching_edges]
edges = [e for e in edges if e.destination not in matching_destinations]
edges.extend(matching_edges)
if "link" in args and args["link"]:
for link in args["link"]:
edges = [
e
for e in edges
if e.destination.node_id != command.command.id or e.destination.field != link[2]
]
node_id = link[0]
if re_negid.match(node_id):
node_id = str(current_id + int(node_id))
# TODO: handle missing input/output
node_output = get_node_outputs(context.session.graph.get_node(node_id), context)[link[1]]
node_input = get_node_inputs(command.command, context)[link[2]]
edges.append(
Edge(
source=EdgeConnection(node_id=node_output.node_path, field=node_output.field),
destination=EdgeConnection(node_id=node_input.node_path, field=node_input.field),
)
)
new_invocations.append((command.command, edges))
current_id = current_id + 1
# Add the node to the session
context.add_node(command.command)
for edge in edges:
print(edge)
context.add_edge(edge)
# Execute all remaining nodes
invoke_all(context)
except InvalidArgs:
invoker.services.logger.warning('Invalid command, use "help" to list commands')
continue
except ValidationError:
invoker.services.logger.warning('Invalid command arguments, run "<command> --help" for summary')
except SessionError:
# Start a new session
invoker.services.logger.warning("Session error: creating a new session")
context.reset()
except ExitCli:
break
except SystemExit:
continue
invoker.stop()
if __name__ == "__main__":
if config.version:
print(f"InvokeAI version {__version__}")
else:
invoke_cli()

View File

@ -1,28 +1,8 @@
import shutil import os
import sys
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
from invokeai.app.services.config.config_default import InvokeAIAppConfig __all__ = []
custom_nodes_path = Path(InvokeAIAppConfig.get_config().custom_nodes_path.absolute()) dirname = os.path.dirname(os.path.abspath(__file__))
custom_nodes_path.mkdir(parents=True, exist_ok=True) for f in os.listdir(dirname):
if f != "__init__.py" and os.path.isfile("%s/%s" % (dirname, f)) and f[-3:] == ".py":
custom_nodes_init_path = str(custom_nodes_path / "__init__.py") __all__.append(f[:-3])
custom_nodes_readme_path = str(custom_nodes_path / "README.md")
# copy our custom nodes __init__.py to the custom nodes directory
shutil.copy(Path(__file__).parent / "custom_nodes/init.py", custom_nodes_init_path)
shutil.copy(Path(__file__).parent / "custom_nodes/README.md", custom_nodes_readme_path)
# Import custom nodes, see https://docs.python.org/3/library/importlib.html#importing-programmatically
spec = spec_from_file_location("custom_nodes", custom_nodes_init_path)
if spec is None or spec.loader is None:
raise RuntimeError(f"Could not load custom nodes from {custom_nodes_init_path}")
module = module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
# add core nodes to __all__
python_files = filter(lambda f: not f.name.startswith("_"), Path(__file__).parent.glob("*.py"))
__all__ = [f.stem for f in python_files] # type: ignore

View File

@ -2,22 +2,33 @@
from __future__ import annotations from __future__ import annotations
import inspect import json
import re import re
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from enum import Enum from enum import Enum
from inspect import signature from inspect import signature
from types import UnionType from typing import (
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterable, Literal, Optional, Type, TypeVar, Union TYPE_CHECKING,
AbstractSet,
Any,
Callable,
ClassVar,
Literal,
Mapping,
Optional,
Type,
TypeVar,
Union,
get_args,
get_type_hints,
)
import semver import semver
from pydantic import BaseModel, ConfigDict, Field, RootModel, TypeAdapter, create_model from pydantic import BaseModel, Field, validator
from pydantic.fields import FieldInfo, _Unset from pydantic.fields import ModelField, Undefined
from pydantic_core import PydanticUndefined from pydantic.typing import NoArgAnyCallable
from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.app.services.config.invokeai_config import InvokeAIAppConfig
from invokeai.app.shared.fields import FieldDescriptions
from invokeai.app.util.misc import uuid_string
if TYPE_CHECKING: if TYPE_CHECKING:
from ..services.invocation_services import InvocationServices from ..services.invocation_services import InvocationServices
@ -27,8 +38,55 @@ class InvalidVersionError(ValueError):
pass pass
class InvalidFieldError(TypeError): class FieldDescriptions:
pass denoising_start = "When to start denoising, expressed a percentage of total steps"
denoising_end = "When to stop denoising, expressed a percentage of total steps"
cfg_scale = "Classifier-Free Guidance scale"
scheduler = "Scheduler to use during inference"
positive_cond = "Positive conditioning tensor"
negative_cond = "Negative conditioning tensor"
noise = "Noise tensor"
clip = "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count"
unet = "UNet (scheduler, LoRAs)"
vae = "VAE"
cond = "Conditioning tensor"
controlnet_model = "ControlNet model to load"
vae_model = "VAE model to load"
lora_model = "LoRA model to load"
main_model = "Main model (UNet, VAE, CLIP) to load"
sdxl_main_model = "SDXL Main model (UNet, VAE, CLIP1, CLIP2) to load"
sdxl_refiner_model = "SDXL Refiner Main Modde (UNet, VAE, CLIP2) to load"
onnx_main_model = "ONNX Main model (UNet, VAE, CLIP) to load"
lora_weight = "The weight at which the LoRA is applied to each model"
compel_prompt = "Prompt to be parsed by Compel to create a conditioning tensor"
raw_prompt = "Raw prompt text (no parsing)"
sdxl_aesthetic = "The aesthetic score to apply to the conditioning tensor"
skipped_layers = "Number of layers to skip in text encoder"
seed = "Seed for random number generation"
steps = "Number of steps to run"
width = "Width of output (px)"
height = "Height of output (px)"
control = "ControlNet(s) to apply"
denoised_latents = "Denoised latents tensor"
latents = "Latents tensor"
strength = "Strength of denoising (proportional to steps)"
core_metadata = "Optional core metadata to be written to image"
interp_mode = "Interpolation mode"
torch_antialias = "Whether or not to apply antialiasing (bilinear or bicubic only)"
fp32 = "Whether or not to use full float32 precision"
precision = "Precision to use"
tiled = "Processing using overlapping tiles (reduce memory consumption)"
detect_res = "Pixel resolution for detection"
image_res = "Pixel resolution for output image"
safe_mode = "Whether or not to use safe mode"
scribble_mode = "Whether or not to use scribble mode"
scale_factor = "The factor by which to scale"
blend_alpha = (
"Blending factor. 0.0 = use input A only, 1.0 = use input B only, 0.5 = 50% mix of input A and input B."
)
num_1 = "The first number"
num_2 = "The second number"
mask = "The mask to use for the operation"
class Input(str, Enum): class Input(str, Enum):
@ -97,7 +155,6 @@ class UIType(str, Enum):
VaeModel = "VaeModelField" VaeModel = "VaeModelField"
LoRAModel = "LoRAModelField" LoRAModel = "LoRAModelField"
ControlNetModel = "ControlNetModelField" ControlNetModel = "ControlNetModelField"
IPAdapterModel = "IPAdapterModelField"
UNet = "UNetField" UNet = "UNetField"
Vae = "VaeField" Vae = "VaeField"
CLIP = "ClipField" CLIP = "ClipField"
@ -113,12 +170,7 @@ class UIType(str, Enum):
Scheduler = "Scheduler" Scheduler = "Scheduler"
WorkflowField = "WorkflowField" WorkflowField = "WorkflowField"
IsIntermediate = "IsIntermediate" IsIntermediate = "IsIntermediate"
BoardField = "BoardField" MetadataField = "MetadataField"
Any = "Any"
MetadataItem = "MetadataItem"
MetadataItemCollection = "MetadataItemCollection"
MetadataItemPolymorphic = "MetadataItemPolymorphic"
MetadataDict = "MetadataDict"
# endregion # endregion
@ -149,11 +201,6 @@ class _InputField(BaseModel):
ui_choice_labels: Optional[dict[str, str]] ui_choice_labels: Optional[dict[str, str]]
item_default: Optional[Any] item_default: Optional[Any]
model_config = ConfigDict(
validate_assignment=True,
json_schema_serialization_defaults_required=True,
)
class _OutputField(BaseModel): class _OutputField(BaseModel):
""" """
@ -167,36 +214,34 @@ class _OutputField(BaseModel):
ui_type: Optional[UIType] ui_type: Optional[UIType]
ui_order: Optional[int] ui_order: Optional[int]
model_config = ConfigDict(
validate_assignment=True,
json_schema_serialization_defaults_required=True,
)
def get_type(klass: BaseModel) -> str:
"""Helper function to get an invocation or invocation output's type. This is the default value of the `type` field."""
return klass.model_fields["type"].default
def InputField( def InputField(
# copied from pydantic's Field *args: Any,
default: Any = _Unset, default: Any = Undefined,
default_factory: Callable[[], Any] | None = _Unset, default_factory: Optional[NoArgAnyCallable] = None,
title: str | None = _Unset, alias: Optional[str] = None,
description: str | None = _Unset, title: Optional[str] = None,
pattern: str | None = _Unset, description: Optional[str] = None,
strict: bool | None = _Unset, exclude: Optional[Union[AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any]] = None,
gt: float | None = _Unset, include: Optional[Union[AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any]] = None,
ge: float | None = _Unset, const: Optional[bool] = None,
lt: float | None = _Unset, gt: Optional[float] = None,
le: float | None = _Unset, ge: Optional[float] = None,
multiple_of: float | None = _Unset, lt: Optional[float] = None,
allow_inf_nan: bool | None = _Unset, le: Optional[float] = None,
max_digits: int | None = _Unset, multiple_of: Optional[float] = None,
decimal_places: int | None = _Unset, allow_inf_nan: Optional[bool] = None,
min_length: int | None = _Unset, max_digits: Optional[int] = None,
max_length: int | None = _Unset, decimal_places: Optional[int] = None,
# custom min_items: Optional[int] = None,
max_items: Optional[int] = None,
unique_items: Optional[bool] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
allow_mutation: bool = True,
regex: Optional[str] = None,
discriminator: Optional[str] = None,
repr: bool = True,
input: Input = Input.Any, input: Input = Input.Any,
ui_type: Optional[UIType] = None, ui_type: Optional[UIType] = None,
ui_component: Optional[UIComponent] = None, ui_component: Optional[UIComponent] = None,
@ -204,6 +249,7 @@ def InputField(
ui_order: Optional[int] = None, ui_order: Optional[int] = None,
ui_choice_labels: Optional[dict[str, str]] = None, ui_choice_labels: Optional[dict[str, str]] = None,
item_default: Optional[Any] = None, item_default: Optional[Any] = None,
**kwargs: Any,
) -> Any: ) -> Any:
""" """
Creates an input field for an invocation. Creates an input field for an invocation.
@ -233,121 +279,77 @@ def InputField(
: param int ui_order: [None] Specifies the order in which this field should be rendered in the UI. \ : param int ui_order: [None] Specifies the order in which this field should be rendered in the UI. \
: param bool item_default: [None] Specifies the default item value, if this is a collection input. \ : param bool item_default: [None] Specifies the default item value, if this is a collection input. \
Ignored for non-collection fields. Ignored for non-collection fields..
""" """
json_schema_extra_: dict[str, Any] = {
"input": input,
"ui_type": ui_type,
"ui_component": ui_component,
"ui_hidden": ui_hidden,
"ui_order": ui_order,
"item_default": item_default,
"ui_choice_labels": ui_choice_labels,
"_field_kind": "input",
}
field_args = {
"default": default,
"default_factory": default_factory,
"title": title,
"description": description,
"pattern": pattern,
"strict": strict,
"gt": gt,
"ge": ge,
"lt": lt,
"le": le,
"multiple_of": multiple_of,
"allow_inf_nan": allow_inf_nan,
"max_digits": max_digits,
"decimal_places": decimal_places,
"min_length": min_length,
"max_length": max_length,
}
"""
Invocation definitions have their fields typed correctly for their `invoke()` functions.
This typing is often more specific than the actual invocation definition requires, because
fields may have values provided only by connections.
For example, consider an ResizeImageInvocation with an `image: ImageField` field.
`image` is required during the call to `invoke()`, but when the python class is instantiated,
the field may not be present. This is fine, because that image field will be provided by a
an ancestor node that outputs the image.
So we'd like to type that `image` field as `Optional[ImageField]`. If we do that, however, then
we need to handle a lot of extra logic in the `invoke()` function to check if the field has a
value or not. This is very tedious.
Ideally, the invocation definition would be able to specify that the field is required during
invocation, but optional during instantiation. So the field would be typed as `image: ImageField`,
but when calling the `invoke()` function, we raise an error if the field is not present.
To do this, we need to do a bit of fanagling to make the pydantic field optional, and then do
extra validation when calling `invoke()`.
There is some additional logic here to cleaning create the pydantic field via the wrapper.
"""
# Filter out field args not provided
provided_args = {k: v for (k, v) in field_args.items() if v is not PydanticUndefined}
if (default is not PydanticUndefined) and (default_factory is not PydanticUndefined):
raise ValueError("Cannot specify both default and default_factory")
# because we are manually making fields optional, we need to store the original required bool for reference later
if default is PydanticUndefined and default_factory is PydanticUndefined:
json_schema_extra_.update({"orig_required": True})
else:
json_schema_extra_.update({"orig_required": False})
# make Input.Any and Input.Connection fields optional, providing None as a default if the field doesn't already have one
if (input is Input.Any or input is Input.Connection) and default_factory is PydanticUndefined:
default_ = None if default is PydanticUndefined else default
provided_args.update({"default": default_})
if default is not PydanticUndefined:
# before invoking, we'll grab the original default value and set it on the field if the field wasn't provided a value
json_schema_extra_.update({"default": default})
json_schema_extra_.update({"orig_default": default})
elif default is not PydanticUndefined and default_factory is PydanticUndefined:
default_ = default
provided_args.update({"default": default_})
json_schema_extra_.update({"orig_default": default_})
elif default_factory is not PydanticUndefined:
provided_args.update({"default_factory": default_factory})
# TODO: cannot serialize default_factory...
# json_schema_extra_.update(dict(orig_default_factory=default_factory))
return Field( return Field(
**provided_args, *args,
json_schema_extra=json_schema_extra_, default=default,
default_factory=default_factory,
alias=alias,
title=title,
description=description,
exclude=exclude,
include=include,
const=const,
gt=gt,
ge=ge,
lt=lt,
le=le,
multiple_of=multiple_of,
allow_inf_nan=allow_inf_nan,
max_digits=max_digits,
decimal_places=decimal_places,
min_items=min_items,
max_items=max_items,
unique_items=unique_items,
min_length=min_length,
max_length=max_length,
allow_mutation=allow_mutation,
regex=regex,
discriminator=discriminator,
repr=repr,
input=input,
ui_type=ui_type,
ui_component=ui_component,
ui_hidden=ui_hidden,
ui_order=ui_order,
item_default=item_default,
ui_choice_labels=ui_choice_labels,
**kwargs,
) )
def OutputField( def OutputField(
# copied from pydantic's Field *args: Any,
default: Any = _Unset, default: Any = Undefined,
default_factory: Callable[[], Any] | None = _Unset, default_factory: Optional[NoArgAnyCallable] = None,
title: str | None = _Unset, alias: Optional[str] = None,
description: str | None = _Unset, title: Optional[str] = None,
pattern: str | None = _Unset, description: Optional[str] = None,
strict: bool | None = _Unset, exclude: Optional[Union[AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any]] = None,
gt: float | None = _Unset, include: Optional[Union[AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any]] = None,
ge: float | None = _Unset, const: Optional[bool] = None,
lt: float | None = _Unset, gt: Optional[float] = None,
le: float | None = _Unset, ge: Optional[float] = None,
multiple_of: float | None = _Unset, lt: Optional[float] = None,
allow_inf_nan: bool | None = _Unset, le: Optional[float] = None,
max_digits: int | None = _Unset, multiple_of: Optional[float] = None,
decimal_places: int | None = _Unset, allow_inf_nan: Optional[bool] = None,
min_length: int | None = _Unset, max_digits: Optional[int] = None,
max_length: int | None = _Unset, decimal_places: Optional[int] = None,
# custom min_items: Optional[int] = None,
max_items: Optional[int] = None,
unique_items: Optional[bool] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
allow_mutation: bool = True,
regex: Optional[str] = None,
discriminator: Optional[str] = None,
repr: bool = True,
ui_type: Optional[UIType] = None, ui_type: Optional[UIType] = None,
ui_hidden: bool = False, ui_hidden: bool = False,
ui_order: Optional[int] = None, ui_order: Optional[int] = None,
**kwargs: Any,
) -> Any: ) -> Any:
""" """
Creates an output field for an invocation output. Creates an output field for an invocation output.
@ -367,12 +369,15 @@ def OutputField(
: param int ui_order: [None] Specifies the order in which this field should be rendered in the UI. \ : param int ui_order: [None] Specifies the order in which this field should be rendered in the UI. \
""" """
return Field( return Field(
*args,
default=default, default=default,
default_factory=default_factory, default_factory=default_factory,
alias=alias,
title=title, title=title,
description=description, description=description,
pattern=pattern, exclude=exclude,
strict=strict, include=include,
const=const,
gt=gt, gt=gt,
ge=ge, ge=ge,
lt=lt, lt=lt,
@ -381,14 +386,19 @@ def OutputField(
allow_inf_nan=allow_inf_nan, allow_inf_nan=allow_inf_nan,
max_digits=max_digits, max_digits=max_digits,
decimal_places=decimal_places, decimal_places=decimal_places,
min_items=min_items,
max_items=max_items,
unique_items=unique_items,
min_length=min_length, min_length=min_length,
max_length=max_length, max_length=max_length,
json_schema_extra={ allow_mutation=allow_mutation,
"ui_type": ui_type, regex=regex,
"ui_hidden": ui_hidden, discriminator=discriminator,
"ui_order": ui_order, repr=repr,
"_field_kind": "output", ui_type=ui_type,
}, ui_hidden=ui_hidden,
ui_order=ui_order,
**kwargs,
) )
@ -402,13 +412,7 @@ class UIConfigBase(BaseModel):
title: Optional[str] = Field(default=None, description="The node's display name") title: Optional[str] = Field(default=None, description="The node's display name")
category: Optional[str] = Field(default=None, description="The node's category") category: Optional[str] = Field(default=None, description="The node's category")
version: Optional[str] = Field( version: Optional[str] = Field(
default=None, default=None, description='The node\'s version. Should be a valid semver string e.g. "1.0.0" or "3.8.13".'
description='The node\'s version. Should be a valid semver string e.g. "1.0.0" or "3.8.13".',
)
model_config = ConfigDict(
validate_assignment=True,
json_schema_serialization_defaults_required=True,
) )
@ -418,22 +422,13 @@ class InvocationContext:
services: InvocationServices services: InvocationServices
graph_execution_state_id: str graph_execution_state_id: str
queue_id: str queue_id: str
queue_item_id: int queue_item_id: str
queue_batch_id: str
def __init__( def __init__(self, services: InvocationServices, queue_id: str, queue_item_id: str, graph_execution_state_id: str):
self,
services: InvocationServices,
queue_id: str,
queue_item_id: int,
queue_batch_id: str,
graph_execution_state_id: str,
):
self.services = services self.services = services
self.graph_execution_state_id = graph_execution_state_id self.graph_execution_state_id = graph_execution_state_id
self.queue_id = queue_id self.queue_id = queue_id
self.queue_item_id = queue_item_id self.queue_item_id = queue_item_id
self.queue_batch_id = queue_batch_id
class BaseInvocationOutput(BaseModel): class BaseInvocationOutput(BaseModel):
@ -443,39 +438,23 @@ class BaseInvocationOutput(BaseModel):
All invocation outputs must use the `@invocation_output` decorator to provide their unique type. All invocation outputs must use the `@invocation_output` decorator to provide their unique type.
""" """
_output_classes: ClassVar[set[BaseInvocationOutput]] = set()
@classmethod @classmethod
def register_output(cls, output: BaseInvocationOutput) -> None: def get_all_subclasses_tuple(cls):
cls._output_classes.add(output) subclasses = []
toprocess = [cls]
while len(toprocess) > 0:
next = toprocess.pop(0)
next_subclasses = next.__subclasses__()
subclasses.extend(next_subclasses)
toprocess.extend(next_subclasses)
return tuple(subclasses)
@classmethod class Config:
def get_outputs(cls) -> Iterable[BaseInvocationOutput]: @staticmethod
return cls._output_classes def schema_extra(schema: dict[str, Any], model_class: Type[BaseModel]) -> None:
if "required" not in schema or not isinstance(schema["required"], list):
@classmethod schema["required"] = list()
def get_outputs_union(cls) -> UnionType: schema["required"].extend(["type"])
outputs_union = Union[tuple(cls._output_classes)] # type: ignore [valid-type]
return outputs_union # type: ignore [return-value]
@classmethod
def get_output_types(cls) -> Iterable[str]:
return (get_type(i) for i in BaseInvocationOutput.get_outputs())
@staticmethod
def json_schema_extra(schema: dict[str, Any], model_class: Type[BaseModel]) -> None:
# Because we use a pydantic Literal field with default value for the invocation type,
# it will be typed as optional in the OpenAPI schema. Make it required manually.
if "required" not in schema or not isinstance(schema["required"], list):
schema["required"] = []
schema["required"].extend(["type"])
model_config = ConfigDict(
protected_namespaces=(),
validate_assignment=True,
json_schema_serialization_defaults_required=True,
json_schema_extra=json_schema_extra,
)
class RequiredConnectionException(Exception): class RequiredConnectionException(Exception):
@ -494,89 +473,110 @@ class MissingInputException(Exception):
class BaseInvocation(ABC, BaseModel): class BaseInvocation(ABC, BaseModel):
""" """
A node to process inputs and produce outputs.
May use dependency injection in __init__ to receive providers.
All invocations must use the `@invocation` decorator to provide their unique type. All invocations must use the `@invocation` decorator to provide their unique type.
""" """
_invocation_classes: ClassVar[set[BaseInvocation]] = set()
@classmethod @classmethod
def register_invocation(cls, invocation: BaseInvocation) -> None: def get_all_subclasses(cls):
cls._invocation_classes.add(invocation)
@classmethod
def get_invocations_union(cls) -> UnionType:
invocations_union = Union[tuple(cls._invocation_classes)] # type: ignore [valid-type]
return invocations_union # type: ignore [return-value]
@classmethod
def get_invocations(cls) -> Iterable[BaseInvocation]:
app_config = InvokeAIAppConfig.get_config() app_config = InvokeAIAppConfig.get_config()
allowed_invocations: set[BaseInvocation] = set() subclasses = []
for sc in cls._invocation_classes: toprocess = [cls]
invocation_type = get_type(sc) while len(toprocess) > 0:
next = toprocess.pop(0)
next_subclasses = next.__subclasses__()
subclasses.extend(next_subclasses)
toprocess.extend(next_subclasses)
allowed_invocations = []
for sc in subclasses:
is_in_allowlist = ( is_in_allowlist = (
invocation_type in app_config.allow_nodes if isinstance(app_config.allow_nodes, list) else True sc.__fields__.get("type").default in app_config.allow_nodes
if isinstance(app_config.allow_nodes, list)
else True
) )
is_in_denylist = ( is_in_denylist = (
invocation_type in app_config.deny_nodes if isinstance(app_config.deny_nodes, list) else False sc.__fields__.get("type").default in app_config.deny_nodes
if isinstance(app_config.deny_nodes, list)
else False
) )
if is_in_allowlist and not is_in_denylist: if is_in_allowlist and not is_in_denylist:
allowed_invocations.add(sc) allowed_invocations.append(sc)
return allowed_invocations return allowed_invocations
@classmethod @classmethod
def get_invocations_map(cls) -> dict[str, BaseInvocation]: def get_invocations(cls):
return tuple(BaseInvocation.get_all_subclasses())
@classmethod
def get_invocations_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 {get_type(i): i for i in BaseInvocation.get_invocations()} return dict(
map(
lambda t: (get_args(get_type_hints(t)["type"])[0], t),
BaseInvocation.get_all_subclasses(),
)
)
@classmethod @classmethod
def get_invocation_types(cls) -> Iterable[str]: def get_output_type(cls):
return (get_type(i) for i in BaseInvocation.get_invocations())
@classmethod
def get_output_type(cls) -> BaseInvocationOutput:
return signature(cls.invoke).return_annotation return signature(cls.invoke).return_annotation
@staticmethod class Config:
def json_schema_extra(schema: dict[str, Any], model_class: Type[BaseModel]) -> None: validate_assignment = True
# Add the various UI-facing attributes to the schema. These are used to build the invocation templates. validate_all = True
uiconfig = getattr(model_class, "UIConfig", None)
if uiconfig and hasattr(uiconfig, "title"): @staticmethod
schema["title"] = uiconfig.title def schema_extra(schema: dict[str, Any], model_class: Type[BaseModel]) -> None:
if uiconfig and hasattr(uiconfig, "tags"): uiconfig = getattr(model_class, "UIConfig", None)
schema["tags"] = uiconfig.tags if uiconfig and hasattr(uiconfig, "title"):
if uiconfig and hasattr(uiconfig, "category"): schema["title"] = uiconfig.title
schema["category"] = uiconfig.category if uiconfig and hasattr(uiconfig, "tags"):
if uiconfig and hasattr(uiconfig, "version"): schema["tags"] = uiconfig.tags
schema["version"] = uiconfig.version if uiconfig and hasattr(uiconfig, "category"):
if "required" not in schema or not isinstance(schema["required"], list): schema["category"] = uiconfig.category
schema["required"] = [] if uiconfig and hasattr(uiconfig, "version"):
schema["required"].extend(["type", "id"]) schema["version"] = uiconfig.version
if "required" not in schema or not isinstance(schema["required"], list):
schema["required"] = list()
schema["required"].extend(["type", "id"])
@abstractmethod @abstractmethod
def invoke(self, context: InvocationContext) -> BaseInvocationOutput: def invoke(self, context: InvocationContext) -> BaseInvocationOutput:
"""Invoke with provided context and return outputs.""" """Invoke with provided context and return outputs."""
pass pass
def __init__(self, **data):
# nodes may have required fields, that can accept input from connections
# on instantiation of the model, we need to exclude these from validation
restore = dict()
try:
field_names = list(self.__fields__.keys())
for field_name in field_names:
# if the field is required and may get its value from a connection, exclude it from validation
field = self.__fields__[field_name]
_input = field.field_info.extra.get("input", None)
if _input in [Input.Connection, Input.Any] and field.required:
if field_name not in data:
restore[field_name] = self.__fields__.pop(field_name)
# instantiate the node, which will validate the data
super().__init__(**data)
finally:
# restore the removed fields
for field_name, field in restore.items():
self.__fields__[field_name] = field
def invoke_internal(self, context: InvocationContext) -> BaseInvocationOutput: def invoke_internal(self, context: InvocationContext) -> BaseInvocationOutput:
for field_name, field in self.model_fields.items(): for field_name, field in self.__fields__.items():
if not field.json_schema_extra or callable(field.json_schema_extra): _input = field.field_info.extra.get("input", None)
# something has gone terribly awry, we should always have this and it should be a dict if field.required and not hasattr(self, field_name):
continue if _input == Input.Connection:
raise RequiredConnectionException(self.__fields__["type"].default, field_name)
# Here we handle the case where the field is optional in the pydantic class, but required elif _input == Input.Any:
# in the `invoke()` method. raise MissingInputException(self.__fields__["type"].default, field_name)
orig_default = field.json_schema_extra.get("orig_default", PydanticUndefined)
orig_required = field.json_schema_extra.get("orig_required", True)
input_ = field.json_schema_extra.get("input", None)
if orig_default is not PydanticUndefined and not hasattr(self, field_name):
setattr(self, field_name, orig_default)
if orig_required and orig_default is PydanticUndefined and getattr(self, field_name) is None:
if input_ == Input.Connection:
raise RequiredConnectionException(self.model_fields["type"].default, field_name)
elif input_ == Input.Any:
raise MissingInputException(self.model_fields["type"].default, field_name)
# skip node cache codepath if it's disabled # skip node cache codepath if it's disabled
if context.services.configuration.node_cache_size == 0: if context.services.configuration.node_cache_size == 0:
@ -589,7 +589,7 @@ class BaseInvocation(ABC, BaseModel):
if cached_value is None: if cached_value is None:
context.services.logger.debug(f'Invocation cache miss for type "{self.get_type()}": {self.id}') context.services.logger.debug(f'Invocation cache miss for type "{self.get_type()}": {self.id}')
output = self.invoke(context) output = self.invoke(context)
context.services.invocation_cache.save(key, output) context.services.invocation_cache.save(output)
return output return output
else: else:
context.services.logger.debug(f'Invocation cache hit for type "{self.get_type()}": {self.id}') context.services.logger.debug(f'Invocation cache hit for type "{self.get_type()}": {self.id}')
@ -599,94 +599,35 @@ class BaseInvocation(ABC, BaseModel):
return self.invoke(context) return self.invoke(context)
def get_type(self) -> str: def get_type(self) -> str:
return self.model_fields["type"].default return self.__fields__["type"].default
id: str = Field( id: str = Field(
default_factory=uuid_string, description="The id of this instance of an invocation. Must be unique among all instances of invocations."
description="The id of this instance of an invocation. Must be unique among all instances of invocations.",
json_schema_extra={"_field_kind": "internal"},
) )
is_intermediate: bool = Field( is_intermediate: bool = InputField(
default=False, default=False, description="Whether or not this is an intermediate invocation.", ui_type=UIType.IsIntermediate
description="Whether or not this is an intermediate invocation.",
json_schema_extra={"ui_type": UIType.IsIntermediate, "_field_kind": "internal"},
) )
use_cache: bool = Field( workflow: Optional[str] = InputField(
default=True, description="Whether or not to use the cache", json_schema_extra={"_field_kind": "internal"} default=None,
description="The workflow to save with the image",
ui_type=UIType.WorkflowField,
) )
use_cache: bool = InputField(default=True, description="Whether or not to use the cache")
@validator("workflow", pre=True)
def validate_workflow_is_json(cls, v):
if v is None:
return None
try:
json.loads(v)
except json.decoder.JSONDecodeError:
raise ValueError("Workflow must be valid JSON")
return v
UIConfig: ClassVar[Type[UIConfigBase]] UIConfig: ClassVar[Type[UIConfigBase]]
model_config = ConfigDict(
protected_namespaces=(),
validate_assignment=True,
json_schema_extra=json_schema_extra,
json_schema_serialization_defaults_required=True,
coerce_numbers_to_str=True,
)
GenericBaseInvocation = TypeVar("GenericBaseInvocation", bound=BaseInvocation)
TBaseInvocation = TypeVar("TBaseInvocation", bound=BaseInvocation)
RESERVED_INPUT_FIELD_NAMES = {
"id",
"is_intermediate",
"use_cache",
"type",
"workflow",
"metadata",
}
RESERVED_OUTPUT_FIELD_NAMES = {"type"}
class _Model(BaseModel):
pass
# Get all pydantic model attrs, methods, etc
RESERVED_PYDANTIC_FIELD_NAMES = {m[0] for m in inspect.getmembers(_Model())}
def validate_fields(model_fields: dict[str, FieldInfo], model_type: str) -> None:
"""
Validates the fields of an invocation or invocation output:
- must not override any pydantic reserved fields
- must be created via `InputField`, `OutputField`, or be an internal field defined in this file
"""
for name, field in model_fields.items():
if name in RESERVED_PYDANTIC_FIELD_NAMES:
raise InvalidFieldError(f'Invalid field name "{name}" on "{model_type}" (reserved by pydantic)')
field_kind = (
# _field_kind is defined via InputField(), OutputField() or by one of the internal fields defined in this file
field.json_schema_extra.get("_field_kind", None) if field.json_schema_extra else None
)
# must have a field_kind
if field_kind is None or field_kind not in {"input", "output", "internal"}:
raise InvalidFieldError(
f'Invalid field definition for "{name}" on "{model_type}" (maybe it\'s not an InputField or OutputField?)'
)
if field_kind == "input" and name in RESERVED_INPUT_FIELD_NAMES:
raise InvalidFieldError(f'Invalid field name "{name}" on "{model_type}" (reserved input field name)')
if field_kind == "output" and name in RESERVED_OUTPUT_FIELD_NAMES:
raise InvalidFieldError(f'Invalid field name "{name}" on "{model_type}" (reserved output field name)')
# internal fields *must* be in the reserved list
if (
field_kind == "internal"
and name not in RESERVED_INPUT_FIELD_NAMES
and name not in RESERVED_OUTPUT_FIELD_NAMES
):
raise InvalidFieldError(
f'Invalid field name "{name}" on "{model_type}" (internal field without reserved name)'
)
return None
def invocation( def invocation(
@ -696,33 +637,26 @@ def invocation(
category: Optional[str] = None, category: Optional[str] = None,
version: Optional[str] = None, version: Optional[str] = None,
use_cache: Optional[bool] = True, use_cache: Optional[bool] = True,
) -> Callable[[Type[TBaseInvocation]], Type[TBaseInvocation]]: ) -> Callable[[Type[GenericBaseInvocation]], Type[GenericBaseInvocation]]:
""" """
Registers an invocation. Adds metadata to an invocation.
:param str invocation_type: The type of the invocation. Must be unique among all invocations. :param str invocation_type: The type of the invocation. Must be unique among all invocations.
:param Optional[str] title: Adds a title to the invocation. Use if the auto-generated title isn't quite right. Defaults to None. :param Optional[str] title: Adds a title to the invocation. Use if the auto-generated title isn't quite right. Defaults to None.
:param Optional[list[str]] tags: Adds tags to the invocation. Invocations may be searched for by their tags. Defaults to None. :param Optional[list[str]] tags: Adds tags to the invocation. Invocations may be searched for by their tags. Defaults to None.
:param Optional[str] category: Adds a category to the invocation. Used to group the invocations in the UI. Defaults to None. :param Optional[str] category: Adds a category to the invocation. Used to group the invocations in the UI. Defaults to None.
:param Optional[str] version: Adds a version to the invocation. Must be a valid semver string. Defaults to None.
:param Optional[bool] use_cache: Whether or not to use the invocation cache. Defaults to True. The user may override this in the workflow editor.
""" """
def wrapper(cls: Type[TBaseInvocation]) -> Type[TBaseInvocation]: def wrapper(cls: Type[GenericBaseInvocation]) -> Type[GenericBaseInvocation]:
# Validate invocation types on creation of invocation classes # Validate invocation types on creation of invocation classes
# TODO: ensure unique? # TODO: ensure unique?
if re.compile(r"^\S+$").match(invocation_type) is None: if re.compile(r"^\S+$").match(invocation_type) is None:
raise ValueError(f'"invocation_type" must consist of non-whitespace characters, got "{invocation_type}"') raise ValueError(f'"invocation_type" must consist of non-whitespace characters, got "{invocation_type}"')
if invocation_type in BaseInvocation.get_invocation_types():
raise ValueError(f'Invocation type "{invocation_type}" already exists')
validate_fields(cls.model_fields, invocation_type)
# Add OpenAPI schema extras # Add OpenAPI schema extras
uiconf_name = cls.__qualname__ + ".UIConfig" uiconf_name = cls.__qualname__ + ".UIConfig"
if not hasattr(cls, "UIConfig") or cls.UIConfig.__qualname__ != uiconf_name: if not hasattr(cls, "UIConfig") or cls.UIConfig.__qualname__ != uiconf_name:
cls.UIConfig = type(uiconf_name, (UIConfigBase,), {}) cls.UIConfig = type(uiconf_name, (UIConfigBase,), dict())
if title is not None: if title is not None:
cls.UIConfig.title = title cls.UIConfig.title = title
if tags is not None: if tags is not None:
@ -736,114 +670,59 @@ def invocation(
raise InvalidVersionError(f'Invalid version string for node "{invocation_type}": "{version}"') from e raise InvalidVersionError(f'Invalid version string for node "{invocation_type}": "{version}"') from e
cls.UIConfig.version = version cls.UIConfig.version = version
if use_cache is not None: if use_cache is not None:
cls.model_fields["use_cache"].default = use_cache cls.__fields__["use_cache"].default = use_cache
# Add the invocation type to the model.
# You'd be tempted to just add the type field and rebuild the model, like this:
# cls.model_fields.update(type=FieldInfo.from_annotated_attribute(Literal[invocation_type], invocation_type))
# cls.model_rebuild() or cls.model_rebuild(force=True)
# Unfortunately, because the `GraphInvocation` uses a forward ref in its `graph` field's annotation, this does
# not work. Instead, we have to create a new class with the type field and patch the original class with it.
# Add the invocation type to the pydantic model of the invocation
invocation_type_annotation = Literal[invocation_type] # type: ignore invocation_type_annotation = Literal[invocation_type] # type: ignore
invocation_type_field = Field( invocation_type_field = ModelField.infer(
title="type", default=invocation_type, json_schema_extra={"_field_kind": "internal"} name="type",
value=invocation_type,
annotation=invocation_type_annotation,
class_validators=None,
config=cls.__config__,
) )
cls.__fields__.update({"type": invocation_type_field})
docstring = cls.__doc__ # to support 3.9, 3.10 and 3.11, as described in https://docs.python.org/3/howto/annotations.html
cls = create_model( if annotations := cls.__dict__.get("__annotations__", None):
cls.__qualname__, annotations.update({"type": invocation_type_annotation})
__base__=cls,
__module__=cls.__module__,
type=(invocation_type_annotation, invocation_type_field),
)
cls.__doc__ = docstring
# TODO: how to type this correctly? it's typed as ModelMetaclass, a private class in pydantic
BaseInvocation.register_invocation(cls) # type: ignore
return cls return cls
return wrapper return wrapper
TBaseInvocationOutput = TypeVar("TBaseInvocationOutput", bound=BaseInvocationOutput) GenericBaseInvocationOutput = TypeVar("GenericBaseInvocationOutput", bound=BaseInvocationOutput)
def invocation_output( def invocation_output(
output_type: str, output_type: str,
) -> Callable[[Type[TBaseInvocationOutput]], Type[TBaseInvocationOutput]]: ) -> Callable[[Type[GenericBaseInvocationOutput]], Type[GenericBaseInvocationOutput]]:
""" """
Adds metadata to an invocation output. Adds metadata to an invocation output.
:param str output_type: The type of the invocation output. Must be unique among all invocation outputs. :param str output_type: The type of the invocation output. Must be unique among all invocation outputs.
""" """
def wrapper(cls: Type[TBaseInvocationOutput]) -> Type[TBaseInvocationOutput]: def wrapper(cls: Type[GenericBaseInvocationOutput]) -> Type[GenericBaseInvocationOutput]:
# Validate output types on creation of invocation output classes # Validate output types on creation of invocation output classes
# TODO: ensure unique? # TODO: ensure unique?
if re.compile(r"^\S+$").match(output_type) is None: if re.compile(r"^\S+$").match(output_type) is None:
raise ValueError(f'"output_type" must consist of non-whitespace characters, got "{output_type}"') raise ValueError(f'"output_type" must consist of non-whitespace characters, got "{output_type}"')
if output_type in BaseInvocationOutput.get_output_types(): # Add the output type to the pydantic model of the invocation output
raise ValueError(f'Invocation type "{output_type}" already exists')
validate_fields(cls.model_fields, output_type)
# Add the output type to the model.
output_type_annotation = Literal[output_type] # type: ignore output_type_annotation = Literal[output_type] # type: ignore
output_type_field = Field(title="type", default=output_type, json_schema_extra={"_field_kind": "internal"}) output_type_field = ModelField.infer(
name="type",
docstring = cls.__doc__ value=output_type,
cls = create_model( annotation=output_type_annotation,
cls.__qualname__, class_validators=None,
__base__=cls, config=cls.__config__,
__module__=cls.__module__,
type=(output_type_annotation, output_type_field),
) )
cls.__doc__ = docstring cls.__fields__.update({"type": output_type_field})
BaseInvocationOutput.register_output(cls) # type: ignore # TODO: how to type this correctly? # to support 3.9, 3.10 and 3.11, as described in https://docs.python.org/3/howto/annotations.html
if annotations := cls.__dict__.get("__annotations__", None):
annotations.update({"type": output_type_annotation})
return cls return cls
return wrapper return wrapper
class WorkflowField(RootModel):
"""
Pydantic model for workflows with custom root of type dict[str, Any].
Workflows are stored without a strict schema.
"""
root: dict[str, Any] = Field(description="The workflow")
WorkflowFieldValidator = TypeAdapter(WorkflowField)
class WithWorkflow(BaseModel):
workflow: Optional[WorkflowField] = Field(
default=None, description=FieldDescriptions.workflow, json_schema_extra={"_field_kind": "internal"}
)
class MetadataField(RootModel):
"""
Pydantic model for metadata with custom root of type dict[str, Any].
Metadata is stored without a strict schema.
"""
root: dict[str, Any] = Field(description="The metadata")
MetadataFieldValidator = TypeAdapter(MetadataField)
class WithMetadata(BaseModel):
metadata: Optional[MetadataField] = Field(
default=None, description=FieldDescriptions.metadata, json_schema_extra={"_field_kind": "internal"}
)

View File

@ -2,7 +2,7 @@
import numpy as np import numpy as np
from pydantic import ValidationInfo, field_validator from pydantic import validator
from invokeai.app.invocations.primitives import IntegerCollectionOutput from invokeai.app.invocations.primitives import IntegerCollectionOutput
from invokeai.app.util.misc import SEED_MAX, get_random_seed from invokeai.app.util.misc import SEED_MAX, get_random_seed
@ -20,9 +20,9 @@ class RangeInvocation(BaseInvocation):
stop: int = InputField(default=10, description="The stop of the range") stop: int = InputField(default=10, description="The stop of the range")
step: int = InputField(default=1, description="The step of the range") step: int = InputField(default=1, description="The step of the range")
@field_validator("stop") @validator("stop")
def stop_gt_start(cls, v: int, info: ValidationInfo): def stop_gt_start(cls, v, values):
if "start" in info.data and v <= info.data["start"]: if "start" in values and v <= values["start"]:
raise ValueError("stop must be greater than start") raise ValueError("stop must be greater than start")
return v return v

View File

@ -1,25 +1,25 @@
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from typing import List, Optional, Union from typing import List, Union
import torch import torch
from compel import Compel, ReturnedEmbeddingsType from compel import Compel, ReturnedEmbeddingsType
from compel.prompt_parser import Blend, Conjunction, CrossAttentionControlSubstitute, FlattenedPrompt, Fragment from compel.prompt_parser import Blend, Conjunction, CrossAttentionControlSubstitute, FlattenedPrompt, Fragment
from invokeai.app.invocations.primitives import ConditioningField, ConditioningOutput from invokeai.app.invocations.primitives import ConditioningField, ConditioningOutput
from invokeai.app.shared.fields import FieldDescriptions from invokeai.backend.stable_diffusion.diffusion.shared_invokeai_diffusion import (
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
BasicConditioningInfo, BasicConditioningInfo,
ExtraConditioningInfo,
SDXLConditioningInfo, SDXLConditioningInfo,
) )
from ...backend.model_management.lora import ModelPatcher from ...backend.model_management.lora import ModelPatcher
from ...backend.model_management.models import ModelNotFoundException, ModelType from ...backend.model_management.models import ModelNotFoundException, ModelType
from ...backend.stable_diffusion.diffusion import InvokeAIDiffuserComponent
from ...backend.util.devices import torch_dtype from ...backend.util.devices import torch_dtype
from .baseinvocation import ( from .baseinvocation import (
BaseInvocation, BaseInvocation,
BaseInvocationOutput, BaseInvocationOutput,
FieldDescriptions,
Input, Input,
InputField, InputField,
InvocationContext, InvocationContext,
@ -43,13 +43,7 @@ class ConditioningFieldData:
# PerpNeg = "perp_neg" # PerpNeg = "perp_neg"
@invocation( @invocation("compel", title="Prompt", tags=["prompt", "compel"], category="conditioning", version="1.0.0")
"compel",
title="Prompt",
tags=["prompt", "compel"],
category="conditioning",
version="1.0.0",
)
class CompelInvocation(BaseInvocation): class CompelInvocation(BaseInvocation):
"""Parse prompt using compel package to conditioning.""" """Parse prompt using compel package to conditioning."""
@ -67,19 +61,17 @@ class CompelInvocation(BaseInvocation):
@torch.no_grad() @torch.no_grad()
def invoke(self, context: InvocationContext) -> ConditioningOutput: def invoke(self, context: InvocationContext) -> ConditioningOutput:
tokenizer_info = context.services.model_manager.get_model( tokenizer_info = context.services.model_manager.get_model(
**self.clip.tokenizer.model_dump(), **self.clip.tokenizer.dict(),
context=context, context=context,
) )
text_encoder_info = context.services.model_manager.get_model( text_encoder_info = context.services.model_manager.get_model(
**self.clip.text_encoder.model_dump(), **self.clip.text_encoder.dict(),
context=context, 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_info = context.services.model_manager.get_model(**lora.dict(exclude={"weight"}), context=context)
**lora.model_dump(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
@ -107,17 +99,14 @@ class CompelInvocation(BaseInvocation):
# print(traceback.format_exc()) # print(traceback.format_exc())
print(f'Warn: trigger: "{trigger}" not found') print(f'Warn: trigger: "{trigger}" not found')
with ( with ModelPatcher.apply_lora_text_encoder(
ModelPatcher.apply_ti(tokenizer_info.context.model, text_encoder_info.context.model, ti_list) as ( text_encoder_info.context.model, _lora_loader()
tokenizer, ), ModelPatcher.apply_ti(tokenizer_info.context.model, text_encoder_info.context.model, ti_list) as (
ti_manager, tokenizer,
), ti_manager,
text_encoder_info as text_encoder, ), ModelPatcher.apply_clip_skip(
# Apply the LoRA after text_encoder has been moved to its target device for faster patching. text_encoder_info.context.model, self.clip.skipped_layers
ModelPatcher.apply_lora_text_encoder(text_encoder, _lora_loader()), ), text_encoder_info as text_encoder:
# Apply CLIP Skip after LoRA to prevent LoRA application from failing on skipped layers.
ModelPatcher.apply_clip_skip(text_encoder_info.context.model, self.clip.skipped_layers),
):
compel = Compel( compel = Compel(
tokenizer=tokenizer, tokenizer=tokenizer,
text_encoder=text_encoder, text_encoder=text_encoder,
@ -133,7 +122,7 @@ class CompelInvocation(BaseInvocation):
c, options = compel.build_conditioning_tensor_for_conjunction(conjunction) c, options = compel.build_conditioning_tensor_for_conjunction(conjunction)
ec = ExtraConditioningInfo( ec = InvokeAIDiffuserComponent.ExtraConditioningInfo(
tokens_count_including_eos_bos=get_max_token_count(tokenizer, conjunction), tokens_count_including_eos_bos=get_max_token_count(tokenizer, conjunction),
cross_attention_control_args=options.get("cross_attention_control", None), cross_attention_control_args=options.get("cross_attention_control", None),
) )
@ -170,11 +159,11 @@ class SDXLPromptInvocationBase:
zero_on_empty: bool, zero_on_empty: bool,
): ):
tokenizer_info = context.services.model_manager.get_model( tokenizer_info = context.services.model_manager.get_model(
**clip_field.tokenizer.model_dump(), **clip_field.tokenizer.dict(),
context=context, context=context,
) )
text_encoder_info = context.services.model_manager.get_model( text_encoder_info = context.services.model_manager.get_model(
**clip_field.text_encoder.model_dump(), **clip_field.text_encoder.dict(),
context=context, context=context,
) )
@ -182,11 +171,7 @@ class SDXLPromptInvocationBase:
if prompt == "" and zero_on_empty: if prompt == "" and zero_on_empty:
cpu_text_encoder = text_encoder_info.context.model cpu_text_encoder = text_encoder_info.context.model
c = torch.zeros( c = torch.zeros(
( (1, cpu_text_encoder.config.max_position_embeddings, cpu_text_encoder.config.hidden_size),
1,
cpu_text_encoder.config.max_position_embeddings,
cpu_text_encoder.config.hidden_size,
),
dtype=text_encoder_info.context.cache.precision, dtype=text_encoder_info.context.cache.precision,
) )
if get_pooled: if get_pooled:
@ -200,9 +185,7 @@ class SDXLPromptInvocationBase:
def _lora_loader(): def _lora_loader():
for lora in clip_field.loras: for lora in clip_field.loras:
lora_info = context.services.model_manager.get_model( lora_info = context.services.model_manager.get_model(**lora.dict(exclude={"weight"}), context=context)
**lora.model_dump(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
@ -230,17 +213,14 @@ class SDXLPromptInvocationBase:
# print(traceback.format_exc()) # print(traceback.format_exc())
print(f'Warn: trigger: "{trigger}" not found') print(f'Warn: trigger: "{trigger}" not found')
with ( with ModelPatcher.apply_lora(
ModelPatcher.apply_ti(tokenizer_info.context.model, text_encoder_info.context.model, ti_list) as ( text_encoder_info.context.model, _lora_loader(), lora_prefix
tokenizer, ), ModelPatcher.apply_ti(tokenizer_info.context.model, text_encoder_info.context.model, ti_list) as (
ti_manager, tokenizer,
), ti_manager,
text_encoder_info as text_encoder, ), ModelPatcher.apply_clip_skip(
# Apply the LoRA after text_encoder has been moved to its target device for faster patching. text_encoder_info.context.model, clip_field.skipped_layers
ModelPatcher.apply_lora(text_encoder, _lora_loader(), lora_prefix), ), text_encoder_info as text_encoder:
# Apply CLIP Skip after LoRA to prevent LoRA application from failing on skipped layers.
ModelPatcher.apply_clip_skip(text_encoder_info.context.model, clip_field.skipped_layers),
):
compel = Compel( compel = Compel(
tokenizer=tokenizer, tokenizer=tokenizer,
text_encoder=text_encoder, text_encoder=text_encoder,
@ -264,7 +244,7 @@ class SDXLPromptInvocationBase:
else: else:
c_pooled = None c_pooled = None
ec = ExtraConditioningInfo( ec = InvokeAIDiffuserComponent.ExtraConditioningInfo(
tokens_count_including_eos_bos=get_max_token_count(tokenizer, conjunction), tokens_count_including_eos_bos=get_max_token_count(tokenizer, conjunction),
cross_attention_control_args=options.get("cross_attention_control", None), cross_attention_control_args=options.get("cross_attention_control", None),
) )
@ -291,16 +271,8 @@ class SDXLPromptInvocationBase:
class SDXLCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase): class SDXLCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase):
"""Parse prompt using compel package to conditioning.""" """Parse prompt using compel package to conditioning."""
prompt: str = InputField( prompt: str = InputField(default="", description=FieldDescriptions.compel_prompt, ui_component=UIComponent.Textarea)
default="", style: str = InputField(default="", description=FieldDescriptions.compel_prompt, ui_component=UIComponent.Textarea)
description=FieldDescriptions.compel_prompt,
ui_component=UIComponent.Textarea,
)
style: str = InputField(
default="",
description=FieldDescriptions.compel_prompt,
ui_component=UIComponent.Textarea,
)
original_width: int = InputField(default=1024, description="") original_width: int = InputField(default=1024, description="")
original_height: int = InputField(default=1024, description="") original_height: int = InputField(default=1024, description="")
crop_top: int = InputField(default=0, description="") crop_top: int = InputField(default=0, description="")
@ -336,9 +308,7 @@ class SDXLCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase):
[ [
c1, c1,
torch.zeros( torch.zeros(
(c1.shape[0], c2.shape[1] - c1.shape[1], c1.shape[2]), (c1.shape[0], c2.shape[1] - c1.shape[1], c1.shape[2]), device=c1.device, dtype=c1.dtype
device=c1.device,
dtype=c1.dtype,
), ),
], ],
dim=1, dim=1,
@ -349,9 +319,7 @@ class SDXLCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase):
[ [
c2, c2,
torch.zeros( torch.zeros(
(c2.shape[0], c1.shape[1] - c2.shape[1], c2.shape[2]), (c2.shape[0], c1.shape[1] - c2.shape[1], c2.shape[2]), device=c2.device, dtype=c2.dtype
device=c2.device,
dtype=c2.dtype,
), ),
], ],
dim=1, dim=1,
@ -389,9 +357,7 @@ class SDXLRefinerCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase
"""Parse prompt using compel package to conditioning.""" """Parse prompt using compel package to conditioning."""
style: str = InputField( style: str = InputField(
default="", default="", description=FieldDescriptions.compel_prompt, ui_component=UIComponent.Textarea
description=FieldDescriptions.compel_prompt,
ui_component=UIComponent.Textarea,
) # TODO: ? ) # TODO: ?
original_width: int = InputField(default=1024, description="") original_width: int = InputField(default=1024, description="")
original_height: int = InputField(default=1024, description="") original_height: int = InputField(default=1024, description="")
@ -435,16 +401,10 @@ class SDXLRefinerCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase
class ClipSkipInvocationOutput(BaseInvocationOutput): class ClipSkipInvocationOutput(BaseInvocationOutput):
"""Clip skip node output""" """Clip skip node output"""
clip: Optional[ClipField] = OutputField(default=None, description=FieldDescriptions.clip, title="CLIP") clip: ClipField = OutputField(default=None, description=FieldDescriptions.clip, title="CLIP")
@invocation( @invocation("clip_skip", title="CLIP Skip", tags=["clipskip", "clip", "skip"], category="conditioning", version="1.0.0")
"clip_skip",
title="CLIP Skip",
tags=["clipskip", "clip", "skip"],
category="conditioning",
version="1.0.0",
)
class ClipSkipInvocation(BaseInvocation): class ClipSkipInvocation(BaseInvocation):
"""Skip layers in clip text_encoder model.""" """Skip layers in clip text_encoder model."""
@ -459,9 +419,7 @@ class ClipSkipInvocation(BaseInvocation):
def get_max_token_count( def get_max_token_count(
tokenizer, tokenizer, prompt: Union[FlattenedPrompt, Blend, Conjunction], truncate_if_too_long=False
prompt: Union[FlattenedPrompt, Blend, Conjunction],
truncate_if_too_long=False,
) -> int: ) -> int:
if type(prompt) is Blend: if type(prompt) is Blend:
blend: Blend = prompt blend: Blend = prompt
@ -478,11 +436,9 @@ def get_tokens_for_prompt_object(tokenizer, parsed_prompt: FlattenedPrompt, trun
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)

View File

@ -2,7 +2,7 @@
# 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 bool, float
from typing import Dict, List, Literal, Union from typing import Dict, List, Literal, Optional, Union
import cv2 import cv2
import numpy as np import numpy as np
@ -24,22 +24,21 @@ from controlnet_aux import (
) )
from controlnet_aux.util import HWC3, ade_palette from controlnet_aux.util import HWC3, ade_palette
from PIL import Image from PIL import Image
from pydantic import BaseModel, ConfigDict, Field, field_validator from pydantic import BaseModel, Field, validator
from invokeai.app.invocations.primitives import ImageField, ImageOutput from invokeai.app.invocations.primitives import ImageField, ImageOutput
from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
from invokeai.app.shared.fields import FieldDescriptions
from ...backend.model_management import BaseModelType from ...backend.model_management import BaseModelType
from ..models.image import ImageCategory, ResourceOrigin
from .baseinvocation import ( from .baseinvocation import (
BaseInvocation, BaseInvocation,
BaseInvocationOutput, BaseInvocationOutput,
FieldDescriptions,
Input, Input,
InputField, InputField,
InvocationContext, InvocationContext,
OutputField, OutputField,
WithMetadata, UIType,
WithWorkflow,
invocation, invocation,
invocation_output, invocation_output,
) )
@ -59,8 +58,6 @@ 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")
model_config = ConfigDict(protected_namespaces=())
class ControlField(BaseModel): class ControlField(BaseModel):
image: ImageField = Field(description="The control image") image: ImageField = Field(description="The control image")
@ -75,7 +72,7 @@ class ControlField(BaseModel):
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")
@field_validator("control_weight") @validator("control_weight")
def validate_control_weight(cls, v): def validate_control_weight(cls, v):
"""Validate that all control weights in the valid range""" """Validate that all control weights in the valid range"""
if isinstance(v, list): if isinstance(v, list):
@ -103,7 +100,7 @@ class ControlNetInvocation(BaseInvocation):
image: ImageField = InputField(description="The control image") image: ImageField = InputField(description="The control image")
control_model: ControlNetModelField = InputField(description=FieldDescriptions.controlnet_model, input=Input.Direct) control_model: ControlNetModelField = InputField(description=FieldDescriptions.controlnet_model, input=Input.Direct)
control_weight: Union[float, List[float]] = InputField( control_weight: Union[float, List[float]] = InputField(
default=1.0, description="The weight given to the ControlNet" default=1.0, description="The weight given to the ControlNet", ui_type=UIType.Float
) )
begin_step_percent: float = InputField( begin_step_percent: float = InputField(
default=0, ge=-1, le=2, description="When the ControlNet is first applied (% of total steps)" default=0, ge=-1, le=2, description="When the ControlNet is first applied (% of total steps)"
@ -128,13 +125,15 @@ class ControlNetInvocation(BaseInvocation):
) )
# This invocation exists for other invocations to subclass it - do not register with @invocation! @invocation(
class ImageProcessorInvocation(BaseInvocation, WithMetadata, WithWorkflow): "image_processor", title="Base Image Processor", tags=["controlnet"], category="controlnet", version="1.0.0"
)
class ImageProcessorInvocation(BaseInvocation):
"""Base class for invocations that preprocess images for ControlNet""" """Base class for invocations that preprocess images for ControlNet"""
image: ImageField = InputField(description="The image to process") image: ImageField = InputField(description="The image to process")
def run_processor(self, image: Image.Image) -> Image.Image: def run_processor(self, image):
# superclass just passes through image without processing # superclass just passes through image without processing
return image return image
@ -152,7 +151,6 @@ class ImageProcessorInvocation(BaseInvocation, WithMetadata, WithWorkflow):
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,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -396,9 +394,9 @@ class ContentShuffleImageProcessorInvocation(ImageProcessorInvocation):
detect_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.detect_res) detect_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.detect_res)
image_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.image_res) image_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.image_res)
h: int = InputField(default=512, ge=0, description="Content shuffle `h` parameter") h: Optional[int] = InputField(default=512, ge=0, description="Content shuffle `h` parameter")
w: int = InputField(default=512, ge=0, description="Content shuffle `w` parameter") w: Optional[int] = InputField(default=512, ge=0, description="Content shuffle `w` parameter")
f: int = InputField(default=256, ge=0, description="Content shuffle `f` parameter") f: Optional[int] = InputField(default=256, ge=0, description="Content shuffle `f` parameter")
def run_processor(self, image): def run_processor(self, image):
content_shuffle_processor = ContentShuffleDetector() content_shuffle_processor = ContentShuffleDetector()
@ -562,33 +560,3 @@ class SamDetectorReproducibleColors(SamDetector):
img[:, :] = ann_color img[:, :] = ann_color
final_img.paste(Image.fromarray(img, mode="RGB"), (0, 0), Image.fromarray(np.uint8(m * 255))) final_img.paste(Image.fromarray(img, mode="RGB"), (0, 0), Image.fromarray(np.uint8(m * 255)))
return np.array(final_img, dtype=np.uint8) return np.array(final_img, dtype=np.uint8)
@invocation(
"color_map_image_processor",
title="Color Map Processor",
tags=["controlnet"],
category="controlnet",
version="1.0.0",
)
class ColorMapImageProcessorInvocation(ImageProcessorInvocation):
"""Generates a color map from the provided image"""
color_map_tile_size: int = InputField(default=64, ge=0, description=FieldDescriptions.tile_size)
def run_processor(self, image: Image.Image):
image = image.convert("RGB")
np_image = np.array(image, dtype=np.uint8)
height, width = np_image.shape[:2]
width_tile_size = min(self.color_map_tile_size, width)
height_tile_size = min(self.color_map_tile_size, height)
color_map = cv2.resize(
np_image,
(width // width_tile_size, height // height_tile_size),
interpolation=cv2.INTER_CUBIC,
)
color_map = cv2.resize(color_map, (width, height), interpolation=cv2.INTER_NEAREST)
color_map = Image.fromarray(color_map)
return color_map

View File

@ -1,51 +0,0 @@
# Custom Nodes / Node Packs
Copy your node packs to this directory.
When nodes are added or changed, you must restart the app to see the changes.
## Directory Structure
For a node pack to be loaded, it must be placed in a directory alongside this
file. Here's an example structure:
```py
.
├── __init__.py # Invoke-managed custom node loader
├── cool_node
├── __init__.py # see example below
└── cool_node.py
└── my_node_pack
├── __init__.py # see example below
├── tasty_node.py
├── bodacious_node.py
├── utils.py
└── extra_nodes
└── fancy_node.py
```
## Node Pack `__init__.py`
Each node pack must have an `__init__.py` file that imports its nodes.
The structure of each node or node pack is otherwise not important.
Here are examples, based on the example directory structure.
### `cool_node/__init__.py`
```py
from .cool_node import CoolInvocation
```
### `my_node_pack/__init__.py`
```py
from .tasty_node import TastyInvocation
from .bodacious_node import BodaciousInvocation
from .extra_nodes.fancy_node import FancyInvocation
```
Only nodes imported in the `__init__.py` file are loaded.

View File

@ -1,51 +0,0 @@
"""
Invoke-managed custom node loader. See README.md for more information.
"""
import sys
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
from invokeai.backend.util.logging import InvokeAILogger
logger = InvokeAILogger.get_logger()
loaded_count = 0
for d in Path(__file__).parent.iterdir():
# skip files
if not d.is_dir():
continue
# skip hidden directories
if d.name.startswith("_") or d.name.startswith("."):
continue
# skip directories without an `__init__.py`
init = d / "__init__.py"
if not init.exists():
continue
module_name = init.parent.stem
# skip if already imported
if module_name in globals():
continue
# we have a legit module to import
spec = spec_from_file_location(module_name, init.absolute())
if spec is None or spec.loader is None:
logger.warn(f"Could not load {init}")
continue
module = module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
loaded_count += 1
del init, module_name
logger.info(f"Loaded {loaded_count} modules from {Path(__file__).parent}")

View File

@ -6,13 +6,13 @@ import numpy
from PIL import Image, ImageOps from PIL import Image, ImageOps
from invokeai.app.invocations.primitives import ImageField, ImageOutput from invokeai.app.invocations.primitives import ImageField, ImageOutput
from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin from invokeai.app.models.image import ImageCategory, ResourceOrigin
from .baseinvocation import BaseInvocation, InputField, InvocationContext, WithMetadata, WithWorkflow, invocation from .baseinvocation import BaseInvocation, InputField, InvocationContext, invocation
@invocation("cv_inpaint", title="OpenCV Inpaint", tags=["opencv", "inpaint"], category="inpaint", version="1.0.0") @invocation("cv_inpaint", title="OpenCV Inpaint", tags=["opencv", "inpaint"], category="inpaint", version="1.0.0")
class CvInpaintInvocation(BaseInvocation, WithMetadata, WithWorkflow): class CvInpaintInvocation(BaseInvocation):
"""Simple inpaint using opencv.""" """Simple inpaint using opencv."""
image: ImageField = InputField(description="The image to inpaint") image: ImageField = InputField(description="The image to inpaint")

View File

@ -1,726 +0,0 @@
import math
import re
from pathlib import Path
from typing import Optional, TypedDict
import cv2
import numpy as np
from mediapipe.python.solutions.face_mesh import FaceMesh # type: ignore[import]
from PIL import Image, ImageDraw, ImageFilter, ImageFont, ImageOps
from PIL.Image import Image as ImageType
from pydantic import field_validator
import invokeai.assets.fonts as font_assets
from invokeai.app.invocations.baseinvocation import (
BaseInvocation,
InputField,
InvocationContext,
OutputField,
WithMetadata,
WithWorkflow,
invocation,
invocation_output,
)
from invokeai.app.invocations.primitives import ImageField, ImageOutput
from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
@invocation_output("face_mask_output")
class FaceMaskOutput(ImageOutput):
"""Base class for FaceMask output"""
mask: ImageField = OutputField(description="The output mask")
@invocation_output("face_off_output")
class FaceOffOutput(ImageOutput):
"""Base class for FaceOff Output"""
mask: ImageField = OutputField(description="The output mask")
x: int = OutputField(description="The x coordinate of the bounding box's left side")
y: int = OutputField(description="The y coordinate of the bounding box's top side")
class FaceResultData(TypedDict):
image: ImageType
mask: ImageType
x_center: float
y_center: float
mesh_width: int
mesh_height: int
chunk_x_offset: int
chunk_y_offset: int
class FaceResultDataWithId(FaceResultData):
face_id: int
class ExtractFaceData(TypedDict):
bounded_image: ImageType
bounded_mask: ImageType
x_min: int
y_min: int
x_max: int
y_max: int
class FaceMaskResult(TypedDict):
image: ImageType
mask: ImageType
def create_white_image(w: int, h: int) -> ImageType:
return Image.new("L", (w, h), color=255)
def create_black_image(w: int, h: int) -> ImageType:
return Image.new("L", (w, h), color=0)
FONT_SIZE = 32
FONT_STROKE_WIDTH = 4
def coalesce_faces(face1: FaceResultData, face2: FaceResultData) -> FaceResultData:
face1_x_offset = face1["chunk_x_offset"] - min(face1["chunk_x_offset"], face2["chunk_x_offset"])
face2_x_offset = face2["chunk_x_offset"] - min(face1["chunk_x_offset"], face2["chunk_x_offset"])
face1_y_offset = face1["chunk_y_offset"] - min(face1["chunk_y_offset"], face2["chunk_y_offset"])
face2_y_offset = face2["chunk_y_offset"] - min(face1["chunk_y_offset"], face2["chunk_y_offset"])
new_im_width = (
max(face1["image"].width, face2["image"].width)
+ max(face1["chunk_x_offset"], face2["chunk_x_offset"])
- min(face1["chunk_x_offset"], face2["chunk_x_offset"])
)
new_im_height = (
max(face1["image"].height, face2["image"].height)
+ max(face1["chunk_y_offset"], face2["chunk_y_offset"])
- min(face1["chunk_y_offset"], face2["chunk_y_offset"])
)
pil_image = Image.new(mode=face1["image"].mode, size=(new_im_width, new_im_height))
pil_image.paste(face1["image"], (face1_x_offset, face1_y_offset))
pil_image.paste(face2["image"], (face2_x_offset, face2_y_offset))
# Mask images are always from the origin
new_mask_im_width = max(face1["mask"].width, face2["mask"].width)
new_mask_im_height = max(face1["mask"].height, face2["mask"].height)
mask_pil = create_white_image(new_mask_im_width, new_mask_im_height)
black_image = create_black_image(face1["mask"].width, face1["mask"].height)
mask_pil.paste(black_image, (0, 0), ImageOps.invert(face1["mask"]))
black_image = create_black_image(face2["mask"].width, face2["mask"].height)
mask_pil.paste(black_image, (0, 0), ImageOps.invert(face2["mask"]))
new_face = FaceResultData(
image=pil_image,
mask=mask_pil,
x_center=max(face1["x_center"], face2["x_center"]),
y_center=max(face1["y_center"], face2["y_center"]),
mesh_width=max(face1["mesh_width"], face2["mesh_width"]),
mesh_height=max(face1["mesh_height"], face2["mesh_height"]),
chunk_x_offset=max(face1["chunk_x_offset"], face2["chunk_x_offset"]),
chunk_y_offset=max(face2["chunk_y_offset"], face2["chunk_y_offset"]),
)
return new_face
def prepare_faces_list(
face_result_list: list[FaceResultData],
) -> list[FaceResultDataWithId]:
"""Deduplicates a list of faces, adding IDs to them."""
deduped_faces: list[FaceResultData] = []
if len(face_result_list) == 0:
return []
for candidate in face_result_list:
should_add = True
candidate_x_center = candidate["x_center"]
candidate_y_center = candidate["y_center"]
for idx, face in enumerate(deduped_faces):
face_center_x = face["x_center"]
face_center_y = face["y_center"]
face_radius_w = face["mesh_width"] / 2
face_radius_h = face["mesh_height"] / 2
# Determine if the center of the candidate_face is inside the ellipse of the added face
# p < 1 -> Inside
# p = 1 -> Exactly on the ellipse
# p > 1 -> Outside
p = (math.pow((candidate_x_center - face_center_x), 2) / math.pow(face_radius_w, 2)) + (
math.pow((candidate_y_center - face_center_y), 2) / math.pow(face_radius_h, 2)
)
if p < 1: # Inside of the already-added face's radius
deduped_faces[idx] = coalesce_faces(face, candidate)
should_add = False
break
if should_add is True:
deduped_faces.append(candidate)
sorted_faces = sorted(deduped_faces, key=lambda x: x["y_center"])
sorted_faces = sorted(sorted_faces, key=lambda x: x["x_center"])
# add face_id for reference
sorted_faces_with_ids: list[FaceResultDataWithId] = []
face_id_counter = 0
for face in sorted_faces:
sorted_faces_with_ids.append(
FaceResultDataWithId(
**face,
face_id=face_id_counter,
)
)
face_id_counter += 1
return sorted_faces_with_ids
def generate_face_box_mask(
context: InvocationContext,
minimum_confidence: float,
x_offset: float,
y_offset: float,
pil_image: ImageType,
chunk_x_offset: int = 0,
chunk_y_offset: int = 0,
draw_mesh: bool = True,
) -> list[FaceResultData]:
result = []
mask_pil = None
# Convert the PIL image to a NumPy array.
np_image = np.array(pil_image, dtype=np.uint8)
# Check if the input image has four channels (RGBA).
if np_image.shape[2] == 4:
# Convert RGBA to RGB by removing the alpha channel.
np_image = np_image[:, :, :3]
# Create a FaceMesh object for face landmark detection and mesh generation.
face_mesh = FaceMesh(
max_num_faces=999,
min_detection_confidence=minimum_confidence,
min_tracking_confidence=minimum_confidence,
)
# Detect the face landmarks and mesh in the input image.
results = face_mesh.process(np_image)
# Check if any face is detected.
if results.multi_face_landmarks: # type: ignore # this are via protobuf and not typed
# Search for the face_id in the detected faces.
for _face_id, face_landmarks in enumerate(results.multi_face_landmarks): # type: ignore #this are via protobuf and not typed
# Get the bounding box of the face mesh.
x_coordinates = [landmark.x for landmark in face_landmarks.landmark]
y_coordinates = [landmark.y for landmark in face_landmarks.landmark]
x_min, x_max = min(x_coordinates), max(x_coordinates)
y_min, y_max = min(y_coordinates), max(y_coordinates)
# Calculate the width and height of the face mesh.
mesh_width = int((x_max - x_min) * np_image.shape[1])
mesh_height = int((y_max - y_min) * np_image.shape[0])
# Get the center of the face.
x_center = np.mean([landmark.x * np_image.shape[1] for landmark in face_landmarks.landmark])
y_center = np.mean([landmark.y * np_image.shape[0] for landmark in face_landmarks.landmark])
face_landmark_points = np.array(
[
[landmark.x * np_image.shape[1], landmark.y * np_image.shape[0]]
for landmark in face_landmarks.landmark
]
)
# Apply the scaling offsets to the face landmark points with a multiplier.
scale_multiplier = 0.2
x_center = np.mean(face_landmark_points[:, 0])
y_center = np.mean(face_landmark_points[:, 1])
if draw_mesh:
x_scaled = face_landmark_points[:, 0] + scale_multiplier * x_offset * (
face_landmark_points[:, 0] - x_center
)
y_scaled = face_landmark_points[:, 1] + scale_multiplier * y_offset * (
face_landmark_points[:, 1] - y_center
)
convex_hull = cv2.convexHull(np.column_stack((x_scaled, y_scaled)).astype(np.int32))
# Generate a binary face mask using the face mesh.
mask_image = np.ones(np_image.shape[:2], dtype=np.uint8) * 255
cv2.fillConvexPoly(mask_image, convex_hull, 0)
# Convert the binary mask image to a PIL Image.
init_mask_pil = Image.fromarray(mask_image, mode="L")
w, h = init_mask_pil.size
mask_pil = create_white_image(w + chunk_x_offset, h + chunk_y_offset)
mask_pil.paste(init_mask_pil, (chunk_x_offset, chunk_y_offset))
x_center = float(x_center)
y_center = float(y_center)
face = FaceResultData(
image=pil_image,
mask=mask_pil or create_white_image(*pil_image.size),
x_center=x_center + chunk_x_offset,
y_center=y_center + chunk_y_offset,
mesh_width=mesh_width,
mesh_height=mesh_height,
chunk_x_offset=chunk_x_offset,
chunk_y_offset=chunk_y_offset,
)
result.append(face)
return result
def extract_face(
context: InvocationContext,
image: ImageType,
face: FaceResultData,
padding: int,
) -> ExtractFaceData:
mask = face["mask"]
center_x = face["x_center"]
center_y = face["y_center"]
mesh_width = face["mesh_width"]
mesh_height = face["mesh_height"]
# Determine the minimum size of the square crop
min_size = min(mask.width, mask.height)
# Calculate the crop boundaries for the output image and mask.
mesh_width += 128 + padding # add pixels to account for mask variance
mesh_height += 128 + padding # add pixels to account for mask variance
crop_size = min(
max(mesh_width, mesh_height, 128), min_size
) # Choose the smaller of the two (given value or face mask size)
if crop_size > 128:
crop_size = (crop_size + 7) // 8 * 8 # Ensure crop side is multiple of 8
# Calculate the actual crop boundaries within the bounds of the original image.
x_min = int(center_x - crop_size / 2)
y_min = int(center_y - crop_size / 2)
x_max = int(center_x + crop_size / 2)
y_max = int(center_y + crop_size / 2)
# Adjust the crop boundaries to stay within the original image's dimensions
if x_min < 0:
context.services.logger.warning("FaceTools --> -X-axis padding reached image edge.")
x_max -= x_min
x_min = 0
elif x_max > mask.width:
context.services.logger.warning("FaceTools --> +X-axis padding reached image edge.")
x_min -= x_max - mask.width
x_max = mask.width
if y_min < 0:
context.services.logger.warning("FaceTools --> +Y-axis padding reached image edge.")
y_max -= y_min
y_min = 0
elif y_max > mask.height:
context.services.logger.warning("FaceTools --> -Y-axis padding reached image edge.")
y_min -= y_max - mask.height
y_max = mask.height
# Ensure the crop is square and adjust the boundaries if needed
if x_max - x_min != crop_size:
context.services.logger.warning("FaceTools --> Limiting x-axis padding to constrain bounding box to a square.")
diff = crop_size - (x_max - x_min)
x_min -= diff // 2
x_max += diff - diff // 2
if y_max - y_min != crop_size:
context.services.logger.warning("FaceTools --> Limiting y-axis padding to constrain bounding box to a square.")
diff = crop_size - (y_max - y_min)
y_min -= diff // 2
y_max += diff - diff // 2
context.services.logger.info(f"FaceTools --> Calculated bounding box (8 multiple): {crop_size}")
# Crop the output image to the specified size with the center of the face mesh as the center.
mask = mask.crop((x_min, y_min, x_max, y_max))
bounded_image = image.crop((x_min, y_min, x_max, y_max))
# blur mask edge by small radius
mask = mask.filter(ImageFilter.GaussianBlur(radius=2))
return ExtractFaceData(
bounded_image=bounded_image,
bounded_mask=mask,
x_min=x_min,
y_min=y_min,
x_max=x_max,
y_max=y_max,
)
def get_faces_list(
context: InvocationContext,
image: ImageType,
should_chunk: bool,
minimum_confidence: float,
x_offset: float,
y_offset: float,
draw_mesh: bool = True,
) -> list[FaceResultDataWithId]:
result = []
# Generate the face box mask and get the center of the face.
if not should_chunk:
context.services.logger.info("FaceTools --> Attempting full image face detection.")
result = generate_face_box_mask(
context=context,
minimum_confidence=minimum_confidence,
x_offset=x_offset,
y_offset=y_offset,
pil_image=image,
chunk_x_offset=0,
chunk_y_offset=0,
draw_mesh=draw_mesh,
)
if should_chunk or len(result) == 0:
context.services.logger.info("FaceTools --> Chunking image (chunk toggled on, or no face found in full image).")
width, height = image.size
image_chunks = []
x_offsets = []
y_offsets = []
result = []
# If width == height, there's nothing more we can do... otherwise...
if width > height:
# Landscape - slice the image horizontally
fx = 0.0
steps = int(width * 2 / height) + 1
increment = (width - height) / (steps - 1)
while fx <= (width - height):
x = int(fx)
image_chunks.append(image.crop((x, 0, x + height, height)))
x_offsets.append(x)
y_offsets.append(0)
fx += increment
context.services.logger.info(f"FaceTools --> Chunk starting at x = {x}")
elif height > width:
# Portrait - slice the image vertically
fy = 0.0
steps = int(height * 2 / width) + 1
increment = (height - width) / (steps - 1)
while fy <= (height - width):
y = int(fy)
image_chunks.append(image.crop((0, y, width, y + width)))
x_offsets.append(0)
y_offsets.append(y)
fy += increment
context.services.logger.info(f"FaceTools --> Chunk starting at y = {y}")
for idx in range(len(image_chunks)):
context.services.logger.info(f"FaceTools --> Evaluating faces in chunk {idx}")
result = result + generate_face_box_mask(
context=context,
minimum_confidence=minimum_confidence,
x_offset=x_offset,
y_offset=y_offset,
pil_image=image_chunks[idx],
chunk_x_offset=x_offsets[idx],
chunk_y_offset=y_offsets[idx],
draw_mesh=draw_mesh,
)
if len(result) == 0:
# Give up
context.services.logger.warning(
"FaceTools --> No face detected in chunked input image. Passing through original image."
)
all_faces = prepare_faces_list(result)
return all_faces
@invocation("face_off", title="FaceOff", tags=["image", "faceoff", "face", "mask"], category="image", version="1.0.2")
class FaceOffInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Bound, extract, and mask a face from an image using MediaPipe detection"""
image: ImageField = InputField(description="Image for face detection")
face_id: int = InputField(
default=0,
ge=0,
description="The face ID to process, numbered from 0. Multiple faces not supported. Find a face's ID with FaceIdentifier node.",
)
minimum_confidence: float = InputField(
default=0.5, description="Minimum confidence for face detection (lower if detection is failing)"
)
x_offset: float = InputField(default=0.0, description="X-axis offset of the mask")
y_offset: float = InputField(default=0.0, description="Y-axis offset of the mask")
padding: int = InputField(default=0, description="All-axis padding around the mask in pixels")
chunk: bool = InputField(
default=False,
description="Whether to bypass full image face detection and default to image chunking. Chunking will occur if no faces are found in the full image.",
)
def faceoff(self, context: InvocationContext, image: ImageType) -> Optional[ExtractFaceData]:
all_faces = get_faces_list(
context=context,
image=image,
should_chunk=self.chunk,
minimum_confidence=self.minimum_confidence,
x_offset=self.x_offset,
y_offset=self.y_offset,
draw_mesh=True,
)
if len(all_faces) == 0:
context.services.logger.warning("FaceOff --> No faces detected. Passing through original image.")
return None
if self.face_id > len(all_faces) - 1:
context.services.logger.warning(
f"FaceOff --> Face ID {self.face_id} is outside of the number of faces detected ({len(all_faces)}). Passing through original image."
)
return None
face_data = extract_face(context=context, image=image, face=all_faces[self.face_id], padding=self.padding)
# Convert the input image to RGBA mode to ensure it has an alpha channel.
face_data["bounded_image"] = face_data["bounded_image"].convert("RGBA")
return face_data
def invoke(self, context: InvocationContext) -> FaceOffOutput:
image = context.services.images.get_pil_image(self.image.image_name)
result = self.faceoff(context=context, image=image)
if result is None:
result_image = image
result_mask = create_white_image(*image.size)
x = 0
y = 0
else:
result_image = result["bounded_image"]
result_mask = result["bounded_mask"]
x = result["x_min"]
y = result["y_min"]
image_dto = context.services.images.create(
image=result_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,
workflow=self.workflow,
)
mask_dto = context.services.images.create(
image=result_mask,
image_origin=ResourceOrigin.INTERNAL,
image_category=ImageCategory.MASK,
node_id=self.id,
session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate,
)
output = FaceOffOutput(
image=ImageField(image_name=image_dto.image_name),
width=image_dto.width,
height=image_dto.height,
mask=ImageField(image_name=mask_dto.image_name),
x=x,
y=y,
)
return output
@invocation("face_mask_detection", title="FaceMask", tags=["image", "face", "mask"], category="image", version="1.0.2")
class FaceMaskInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Face mask creation using mediapipe face detection"""
image: ImageField = InputField(description="Image to face detect")
face_ids: str = InputField(
default="",
description="Comma-separated list of face ids to mask eg '0,2,7'. Numbered from 0. Leave empty to mask all. Find face IDs with FaceIdentifier node.",
)
minimum_confidence: float = InputField(
default=0.5, description="Minimum confidence for face detection (lower if detection is failing)"
)
x_offset: float = InputField(default=0.0, description="Offset for the X-axis of the face mask")
y_offset: float = InputField(default=0.0, description="Offset for the Y-axis of the face mask")
chunk: bool = InputField(
default=False,
description="Whether to bypass full image face detection and default to image chunking. Chunking will occur if no faces are found in the full image.",
)
invert_mask: bool = InputField(default=False, description="Toggle to invert the mask")
@field_validator("face_ids")
def validate_comma_separated_ints(cls, v) -> str:
comma_separated_ints_regex = re.compile(r"^\d*(,\d+)*$")
if comma_separated_ints_regex.match(v) is None:
raise ValueError('Face IDs must be a comma-separated list of integers (e.g. "1,2,3")')
return v
def facemask(self, context: InvocationContext, image: ImageType) -> FaceMaskResult:
all_faces = get_faces_list(
context=context,
image=image,
should_chunk=self.chunk,
minimum_confidence=self.minimum_confidence,
x_offset=self.x_offset,
y_offset=self.y_offset,
draw_mesh=True,
)
mask_pil = create_white_image(*image.size)
id_range = list(range(0, len(all_faces)))
ids_to_extract = id_range
if self.face_ids != "":
parsed_face_ids = [int(id) for id in self.face_ids.split(",")]
# get requested face_ids that are in range
intersected_face_ids = set(parsed_face_ids) & set(id_range)
if len(intersected_face_ids) == 0:
id_range_str = ",".join([str(id) for id in id_range])
context.services.logger.warning(
f"Face IDs must be in range of detected faces - requested {self.face_ids}, detected {id_range_str}. Passing through original image."
)
return FaceMaskResult(
image=image, # original image
mask=mask_pil, # white mask
)
ids_to_extract = list(intersected_face_ids)
for face_id in ids_to_extract:
face_data = extract_face(context=context, image=image, face=all_faces[face_id], padding=0)
face_mask_pil = face_data["bounded_mask"]
x_min = face_data["x_min"]
y_min = face_data["y_min"]
x_max = face_data["x_max"]
y_max = face_data["y_max"]
mask_pil.paste(
create_black_image(x_max - x_min, y_max - y_min),
box=(x_min, y_min),
mask=ImageOps.invert(face_mask_pil),
)
if self.invert_mask:
mask_pil = ImageOps.invert(mask_pil)
# Create an RGBA image with transparency
image = image.convert("RGBA")
return FaceMaskResult(
image=image,
mask=mask_pil,
)
def invoke(self, context: InvocationContext) -> FaceMaskOutput:
image = context.services.images.get_pil_image(self.image.image_name)
result = self.facemask(context=context, image=image)
image_dto = context.services.images.create(
image=result["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,
workflow=self.workflow,
)
mask_dto = context.services.images.create(
image=result["mask"],
image_origin=ResourceOrigin.INTERNAL,
image_category=ImageCategory.MASK,
node_id=self.id,
session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate,
)
output = FaceMaskOutput(
image=ImageField(image_name=image_dto.image_name),
width=image_dto.width,
height=image_dto.height,
mask=ImageField(image_name=mask_dto.image_name),
)
return output
@invocation(
"face_identifier", title="FaceIdentifier", tags=["image", "face", "identifier"], category="image", version="1.0.2"
)
class FaceIdentifierInvocation(BaseInvocation, WithWorkflow, WithMetadata):
"""Outputs an image with detected face IDs printed on each face. For use with other FaceTools."""
image: ImageField = InputField(description="Image to face detect")
minimum_confidence: float = InputField(
default=0.5, description="Minimum confidence for face detection (lower if detection is failing)"
)
chunk: bool = InputField(
default=False,
description="Whether to bypass full image face detection and default to image chunking. Chunking will occur if no faces are found in the full image.",
)
def faceidentifier(self, context: InvocationContext, image: ImageType) -> ImageType:
image = image.copy()
all_faces = get_faces_list(
context=context,
image=image,
should_chunk=self.chunk,
minimum_confidence=self.minimum_confidence,
x_offset=0,
y_offset=0,
draw_mesh=False,
)
# Note - font may be found either in the repo if running an editable install, or in the venv if running a package install
font_path = [x for x in [Path(y, "inter/Inter-Regular.ttf") for y in font_assets.__path__] if x.exists()]
font = ImageFont.truetype(font_path[0].as_posix(), FONT_SIZE)
# Paste face IDs on the output image
draw = ImageDraw.Draw(image)
for face in all_faces:
x_coord = face["x_center"]
y_coord = face["y_center"]
text = str(face["face_id"])
# get bbox of the text so we can center the id on the face
_, _, bbox_w, bbox_h = draw.textbbox(xy=(0, 0), text=text, font=font, stroke_width=FONT_STROKE_WIDTH)
x = x_coord - bbox_w / 2
y = y_coord - bbox_h / 2
draw.text(
xy=(x, y),
text=str(text),
fill=(255, 255, 255, 255),
font=font,
stroke_width=FONT_STROKE_WIDTH,
stroke_fill=(0, 0, 0, 255),
)
# Create an RGBA image with transparency
image = image.convert("RGBA")
return image
def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.services.images.get_pil_image(self.image.image_name)
result_image = self.faceidentifier(context=context, image=image)
image_dto = context.services.images.create(
image=result_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,
workflow=self.workflow,
)
return ImageOutput(
image=ImageField(image_name=image_dto.image_name),
width=image_dto.width,
height=image_dto.height,
)

View File

@ -7,13 +7,13 @@ import cv2
import numpy import numpy
from PIL import Image, ImageChops, ImageFilter, ImageOps from PIL import Image, ImageChops, ImageFilter, ImageOps
from invokeai.app.invocations.primitives import BoardField, ColorField, ImageField, ImageOutput from invokeai.app.invocations.metadata import CoreMetadata
from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin from invokeai.app.invocations.primitives import ColorField, ImageField, ImageOutput
from invokeai.app.shared.fields import FieldDescriptions
from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark
from invokeai.backend.image_util.safety_checker import SafetyChecker from invokeai.backend.image_util.safety_checker import SafetyChecker
from .baseinvocation import BaseInvocation, Input, InputField, InvocationContext, WithMetadata, WithWorkflow, invocation from ..models.image import ImageCategory, ResourceOrigin
from .baseinvocation import BaseInvocation, FieldDescriptions, InputField, InvocationContext, invocation
@invocation("show_image", title="Show Image", tags=["image"], category="image", version="1.0.0") @invocation("show_image", title="Show Image", tags=["image"], category="image", version="1.0.0")
@ -37,7 +37,7 @@ class ShowImageInvocation(BaseInvocation):
@invocation("blank_image", title="Blank Image", tags=["image"], category="image", version="1.0.0") @invocation("blank_image", title="Blank Image", tags=["image"], category="image", version="1.0.0")
class BlankImageInvocation(BaseInvocation, WithMetadata, WithWorkflow): class BlankImageInvocation(BaseInvocation):
"""Creates a blank image and forwards it to the pipeline""" """Creates a blank image and forwards it to the pipeline"""
width: int = InputField(default=512, description="The width of the image") width: int = InputField(default=512, description="The width of the image")
@ -55,7 +55,6 @@ class BlankImageInvocation(BaseInvocation, WithMetadata, WithWorkflow):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -67,7 +66,7 @@ class BlankImageInvocation(BaseInvocation, WithMetadata, WithWorkflow):
@invocation("img_crop", title="Crop Image", tags=["image", "crop"], category="image", version="1.0.0") @invocation("img_crop", title="Crop Image", tags=["image", "crop"], category="image", version="1.0.0")
class ImageCropInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageCropInvocation(BaseInvocation):
"""Crops an image to a specified box. The box can be outside of the image.""" """Crops an image to a specified box. The box can be outside of the image."""
image: ImageField = InputField(description="The image to crop") image: ImageField = InputField(description="The image to crop")
@ -89,7 +88,6 @@ class ImageCropInvocation(BaseInvocation, WithWorkflow, WithMetadata):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -101,7 +99,7 @@ class ImageCropInvocation(BaseInvocation, WithWorkflow, WithMetadata):
@invocation("img_paste", title="Paste Image", tags=["image", "paste"], category="image", version="1.0.1") @invocation("img_paste", title="Paste Image", tags=["image", "paste"], category="image", version="1.0.1")
class ImagePasteInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImagePasteInvocation(BaseInvocation):
"""Pastes an image into another image.""" """Pastes an image into another image."""
base_image: ImageField = InputField(description="The base image") base_image: ImageField = InputField(description="The base image")
@ -143,7 +141,6 @@ class ImagePasteInvocation(BaseInvocation, WithWorkflow, WithMetadata):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -155,7 +152,7 @@ class ImagePasteInvocation(BaseInvocation, WithWorkflow, WithMetadata):
@invocation("tomask", title="Mask from Alpha", tags=["image", "mask"], category="image", version="1.0.0") @invocation("tomask", title="Mask from Alpha", tags=["image", "mask"], category="image", version="1.0.0")
class MaskFromAlphaInvocation(BaseInvocation, WithWorkflow, WithMetadata): class MaskFromAlphaInvocation(BaseInvocation):
"""Extracts the alpha channel of an image as a mask.""" """Extracts the alpha channel of an image as a mask."""
image: ImageField = InputField(description="The image to create the mask from") image: ImageField = InputField(description="The image to create the mask from")
@ -175,7 +172,6 @@ class MaskFromAlphaInvocation(BaseInvocation, WithWorkflow, WithMetadata):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -187,7 +183,7 @@ class MaskFromAlphaInvocation(BaseInvocation, WithWorkflow, WithMetadata):
@invocation("img_mul", title="Multiply Images", tags=["image", "multiply"], category="image", version="1.0.0") @invocation("img_mul", title="Multiply Images", tags=["image", "multiply"], category="image", version="1.0.0")
class ImageMultiplyInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageMultiplyInvocation(BaseInvocation):
"""Multiplies two images together using `PIL.ImageChops.multiply()`.""" """Multiplies two images together using `PIL.ImageChops.multiply()`."""
image1: ImageField = InputField(description="The first image to multiply") image1: ImageField = InputField(description="The first image to multiply")
@ -206,7 +202,6 @@ class ImageMultiplyInvocation(BaseInvocation, WithWorkflow, WithMetadata):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -221,7 +216,7 @@ IMAGE_CHANNELS = Literal["A", "R", "G", "B"]
@invocation("img_chan", title="Extract Image Channel", tags=["image", "channel"], category="image", version="1.0.0") @invocation("img_chan", title="Extract Image Channel", tags=["image", "channel"], category="image", version="1.0.0")
class ImageChannelInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageChannelInvocation(BaseInvocation):
"""Gets a channel from an image.""" """Gets a channel from an image."""
image: ImageField = InputField(description="The image to get the channel from") image: ImageField = InputField(description="The image to get the channel from")
@ -239,7 +234,6 @@ class ImageChannelInvocation(BaseInvocation, WithWorkflow, WithMetadata):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -254,7 +248,7 @@ IMAGE_MODES = Literal["L", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F
@invocation("img_conv", title="Convert Image Mode", tags=["image", "convert"], category="image", version="1.0.0") @invocation("img_conv", title="Convert Image Mode", tags=["image", "convert"], category="image", version="1.0.0")
class ImageConvertInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageConvertInvocation(BaseInvocation):
"""Converts an image to a different mode.""" """Converts an image to a different mode."""
image: ImageField = InputField(description="The image to convert") image: ImageField = InputField(description="The image to convert")
@ -272,7 +266,6 @@ class ImageConvertInvocation(BaseInvocation, WithWorkflow, WithMetadata):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -284,7 +277,7 @@ class ImageConvertInvocation(BaseInvocation, WithWorkflow, WithMetadata):
@invocation("img_blur", title="Blur Image", tags=["image", "blur"], category="image", version="1.0.0") @invocation("img_blur", title="Blur Image", tags=["image", "blur"], category="image", version="1.0.0")
class ImageBlurInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageBlurInvocation(BaseInvocation):
"""Blurs an image""" """Blurs an image"""
image: ImageField = InputField(description="The image to blur") image: ImageField = InputField(description="The image to blur")
@ -307,7 +300,6 @@ class ImageBlurInvocation(BaseInvocation, WithWorkflow, WithMetadata):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -339,13 +331,16 @@ PIL_RESAMPLING_MAP = {
@invocation("img_resize", title="Resize Image", tags=["image", "resize"], category="image", version="1.0.0") @invocation("img_resize", title="Resize Image", tags=["image", "resize"], category="image", version="1.0.0")
class ImageResizeInvocation(BaseInvocation, WithMetadata, WithWorkflow): class ImageResizeInvocation(BaseInvocation):
"""Resizes an image to specific dimensions""" """Resizes an image to specific dimensions"""
image: ImageField = InputField(description="The image to resize") image: ImageField = InputField(description="The image to resize")
width: int = InputField(default=512, gt=0, description="The width to resize to (px)") width: int = InputField(default=512, gt=0, description="The width to resize to (px)")
height: int = InputField(default=512, gt=0, description="The height to resize to (px)") height: int = InputField(default=512, gt=0, description="The height to resize to (px)")
resample_mode: PIL_RESAMPLING_MODES = InputField(default="bicubic", description="The resampling mode") resample_mode: PIL_RESAMPLING_MODES = InputField(default="bicubic", description="The resampling mode")
metadata: Optional[CoreMetadata] = InputField(
default=None, description=FieldDescriptions.core_metadata, ui_hidden=True
)
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)
@ -364,7 +359,7 @@ class ImageResizeInvocation(BaseInvocation, WithMetadata, WithWorkflow):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata, metadata=self.metadata.dict() if self.metadata else None,
workflow=self.workflow, workflow=self.workflow,
) )
@ -376,7 +371,7 @@ class ImageResizeInvocation(BaseInvocation, WithMetadata, WithWorkflow):
@invocation("img_scale", title="Scale Image", tags=["image", "scale"], category="image", version="1.0.0") @invocation("img_scale", title="Scale Image", tags=["image", "scale"], category="image", version="1.0.0")
class ImageScaleInvocation(BaseInvocation, WithMetadata, WithWorkflow): class ImageScaleInvocation(BaseInvocation):
"""Scales an image by a factor""" """Scales an image by a factor"""
image: ImageField = InputField(description="The image to scale") image: ImageField = InputField(description="The image to scale")
@ -406,7 +401,6 @@ class ImageScaleInvocation(BaseInvocation, WithMetadata, WithWorkflow):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -418,7 +412,7 @@ class ImageScaleInvocation(BaseInvocation, WithMetadata, WithWorkflow):
@invocation("img_lerp", title="Lerp Image", tags=["image", "lerp"], category="image", version="1.0.0") @invocation("img_lerp", title="Lerp Image", tags=["image", "lerp"], category="image", version="1.0.0")
class ImageLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageLerpInvocation(BaseInvocation):
"""Linear interpolation of all pixels of an image""" """Linear interpolation of all pixels of an image"""
image: ImageField = InputField(description="The image to lerp") image: ImageField = InputField(description="The image to lerp")
@ -440,7 +434,6 @@ class ImageLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -452,7 +445,7 @@ class ImageLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata):
@invocation("img_ilerp", title="Inverse Lerp Image", tags=["image", "ilerp"], category="image", version="1.0.0") @invocation("img_ilerp", title="Inverse Lerp Image", tags=["image", "ilerp"], category="image", version="1.0.0")
class ImageInverseLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageInverseLerpInvocation(BaseInvocation):
"""Inverse linear interpolation of all pixels of an image""" """Inverse linear interpolation of all pixels of an image"""
image: ImageField = InputField(description="The image to lerp") image: ImageField = InputField(description="The image to lerp")
@ -463,7 +456,7 @@ class ImageInverseLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata):
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 # type: ignore [assignment] 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))
@ -474,7 +467,6 @@ class ImageInverseLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -486,10 +478,13 @@ class ImageInverseLerpInvocation(BaseInvocation, WithWorkflow, WithMetadata):
@invocation("img_nsfw", title="Blur NSFW Image", tags=["image", "nsfw"], category="image", version="1.0.0") @invocation("img_nsfw", title="Blur NSFW Image", tags=["image", "nsfw"], category="image", version="1.0.0")
class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithWorkflow): class ImageNSFWBlurInvocation(BaseInvocation):
"""Add blur to NSFW-flagged images""" """Add blur to NSFW-flagged images"""
image: ImageField = InputField(description="The image to check") image: ImageField = InputField(description="The image to check")
metadata: Optional[CoreMetadata] = InputField(
default=None, description=FieldDescriptions.core_metadata, ui_hidden=True
)
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)
@ -510,7 +505,7 @@ class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithWorkflow):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata, metadata=self.metadata.dict() if self.metadata else None,
workflow=self.workflow, workflow=self.workflow,
) )
@ -520,7 +515,7 @@ class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithWorkflow):
height=image_dto.height, height=image_dto.height,
) )
def _get_caution_img(self) -> Image.Image: def _get_caution_img(self) -> Image:
import invokeai.app.assets.images as image_assets import invokeai.app.assets.images as image_assets
caution = Image.open(Path(image_assets.__path__[0]) / "caution.png") caution = Image.open(Path(image_assets.__path__[0]) / "caution.png")
@ -528,17 +523,16 @@ class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithWorkflow):
@invocation( @invocation(
"img_watermark", "img_watermark", title="Add Invisible Watermark", tags=["image", "watermark"], category="image", version="1.0.0"
title="Add Invisible Watermark",
tags=["image", "watermark"],
category="image",
version="1.0.0",
) )
class ImageWatermarkInvocation(BaseInvocation, WithMetadata, WithWorkflow): class ImageWatermarkInvocation(BaseInvocation):
"""Add an invisible watermark to an image""" """Add an invisible watermark to an image"""
image: ImageField = InputField(description="The image to check") image: ImageField = InputField(description="The image to check")
text: str = InputField(default="InvokeAI", description="Watermark text") text: str = InputField(default="InvokeAI", description="Watermark text")
metadata: Optional[CoreMetadata] = InputField(
default=None, description=FieldDescriptions.core_metadata, ui_hidden=True
)
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)
@ -550,7 +544,7 @@ class ImageWatermarkInvocation(BaseInvocation, WithMetadata, WithWorkflow):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata, metadata=self.metadata.dict() if self.metadata else None,
workflow=self.workflow, workflow=self.workflow,
) )
@ -562,7 +556,7 @@ class ImageWatermarkInvocation(BaseInvocation, WithMetadata, WithWorkflow):
@invocation("mask_edge", title="Mask Edge", tags=["image", "mask", "inpaint"], category="image", version="1.0.0") @invocation("mask_edge", title="Mask Edge", tags=["image", "mask", "inpaint"], category="image", version="1.0.0")
class MaskEdgeInvocation(BaseInvocation, WithWorkflow, WithMetadata): class MaskEdgeInvocation(BaseInvocation):
"""Applies an edge mask to an image""" """Applies an edge mask to an image"""
image: ImageField = InputField(description="The image to apply the mask to") image: ImageField = InputField(description="The image to apply the mask to")
@ -596,7 +590,6 @@ class MaskEdgeInvocation(BaseInvocation, WithWorkflow, WithMetadata):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -608,13 +601,9 @@ class MaskEdgeInvocation(BaseInvocation, WithWorkflow, WithMetadata):
@invocation( @invocation(
"mask_combine", "mask_combine", title="Combine Masks", tags=["image", "mask", "multiply"], category="image", version="1.0.0"
title="Combine Masks",
tags=["image", "mask", "multiply"],
category="image",
version="1.0.0",
) )
class MaskCombineInvocation(BaseInvocation, WithWorkflow, WithMetadata): class MaskCombineInvocation(BaseInvocation):
"""Combine two masks together by multiplying them using `PIL.ImageChops.multiply()`.""" """Combine two masks together by multiplying them using `PIL.ImageChops.multiply()`."""
mask1: ImageField = InputField(description="The first mask to combine") mask1: ImageField = InputField(description="The first mask to combine")
@ -633,7 +622,6 @@ class MaskCombineInvocation(BaseInvocation, WithWorkflow, WithMetadata):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -645,7 +633,7 @@ class MaskCombineInvocation(BaseInvocation, WithWorkflow, WithMetadata):
@invocation("color_correct", title="Color Correct", tags=["image", "color"], category="image", version="1.0.0") @invocation("color_correct", title="Color Correct", tags=["image", "color"], category="image", version="1.0.0")
class ColorCorrectInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ColorCorrectInvocation(BaseInvocation):
""" """
Shifts the colors of a target image to match the reference image, optionally Shifts the colors of a target image to match the reference image, optionally
using a mask to only color-correct certain regions of the target image. using a mask to only color-correct certain regions of the target image.
@ -744,7 +732,6 @@ class ColorCorrectInvocation(BaseInvocation, WithWorkflow, WithMetadata):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -756,7 +743,7 @@ class ColorCorrectInvocation(BaseInvocation, WithWorkflow, WithMetadata):
@invocation("img_hue_adjust", title="Adjust Image Hue", tags=["image", "hue"], category="image", version="1.0.0") @invocation("img_hue_adjust", title="Adjust Image Hue", tags=["image", "hue"], category="image", version="1.0.0")
class ImageHueAdjustmentInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageHueAdjustmentInvocation(BaseInvocation):
"""Adjusts the Hue of an image.""" """Adjusts the Hue of an image."""
image: ImageField = InputField(description="The image to adjust") image: ImageField = InputField(description="The image to adjust")
@ -784,7 +771,6 @@ class ImageHueAdjustmentInvocation(BaseInvocation, WithWorkflow, WithMetadata):
node_id=self.id, node_id=self.id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -860,7 +846,7 @@ CHANNEL_FORMATS = {
category="image", category="image",
version="1.0.0", version="1.0.0",
) )
class ImageChannelOffsetInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageChannelOffsetInvocation(BaseInvocation):
"""Add or subtract a value from a specific color channel of an image.""" """Add or subtract a value from a specific color channel of an image."""
image: ImageField = InputField(description="The image to adjust") image: ImageField = InputField(description="The image to adjust")
@ -894,7 +880,6 @@ class ImageChannelOffsetInvocation(BaseInvocation, WithWorkflow, WithMetadata):
node_id=self.id, node_id=self.id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -931,7 +916,7 @@ class ImageChannelOffsetInvocation(BaseInvocation, WithWorkflow, WithMetadata):
category="image", category="image",
version="1.0.0", version="1.0.0",
) )
class ImageChannelMultiplyInvocation(BaseInvocation, WithWorkflow, WithMetadata): class ImageChannelMultiplyInvocation(BaseInvocation):
"""Scale a specific color channel of an image.""" """Scale a specific color channel of an image."""
image: ImageField = InputField(description="The image to adjust") image: ImageField = InputField(description="The image to adjust")
@ -971,7 +956,6 @@ class ImageChannelMultiplyInvocation(BaseInvocation, WithWorkflow, WithMetadata)
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
workflow=self.workflow, workflow=self.workflow,
metadata=self.metadata,
) )
return ImageOutput( return ImageOutput(
@ -988,14 +972,18 @@ class ImageChannelMultiplyInvocation(BaseInvocation, WithWorkflow, WithMetadata)
title="Save Image", title="Save Image",
tags=["primitives", "image"], tags=["primitives", "image"],
category="primitives", category="primitives",
version="1.0.1", version="1.0.0",
use_cache=False, use_cache=False,
) )
class SaveImageInvocation(BaseInvocation, WithWorkflow, WithMetadata): class SaveImageInvocation(BaseInvocation):
"""Saves an image. Unlike an image primitive, this invocation stores a copy of the image.""" """Saves an image. Unlike an image primitive, this invocation stores a copy of the image."""
image: ImageField = InputField(description=FieldDescriptions.image) image: ImageField = InputField(description="The image to load")
board: BoardField = InputField(default=None, description=FieldDescriptions.board, input=Input.Direct) metadata: CoreMetadata = InputField(
default=None,
description=FieldDescriptions.core_metadata,
ui_hidden=True,
)
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)
@ -1004,11 +992,10 @@ class SaveImageInvocation(BaseInvocation, WithWorkflow, WithMetadata):
image=image, image=image,
image_origin=ResourceOrigin.INTERNAL, image_origin=ResourceOrigin.INTERNAL,
image_category=ImageCategory.GENERAL, image_category=ImageCategory.GENERAL,
board_id=self.board.board_id if self.board else None,
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata, metadata=self.metadata.dict() if self.metadata else None,
workflow=self.workflow, workflow=self.workflow,
) )

View File

@ -7,13 +7,13 @@ import numpy as np
from PIL import Image, ImageOps from PIL import Image, ImageOps
from invokeai.app.invocations.primitives import ColorField, ImageField, ImageOutput from invokeai.app.invocations.primitives import ColorField, ImageField, ImageOutput
from invokeai.app.services.image_records.image_records_common import ImageCategory, 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.image_util.cv2_inpaint import cv2_inpaint from invokeai.backend.image_util.cv2_inpaint import cv2_inpaint
from invokeai.backend.image_util.lama import LaMA from invokeai.backend.image_util.lama import LaMA
from invokeai.backend.image_util.patchmatch import PatchMatch from invokeai.backend.image_util.patchmatch import PatchMatch
from .baseinvocation import BaseInvocation, InputField, InvocationContext, WithMetadata, WithWorkflow, invocation from ..models.image import ImageCategory, ResourceOrigin
from .baseinvocation import BaseInvocation, InputField, InvocationContext, invocation
from .image import PIL_RESAMPLING_MAP, PIL_RESAMPLING_MODES from .image import PIL_RESAMPLING_MAP, PIL_RESAMPLING_MODES
@ -119,7 +119,7 @@ def tile_fill_missing(im: Image.Image, tile_size: int = 16, seed: Optional[int]
@invocation("infill_rgba", title="Solid Color Infill", tags=["image", "inpaint"], category="inpaint", version="1.0.0") @invocation("infill_rgba", title="Solid Color Infill", tags=["image", "inpaint"], category="inpaint", version="1.0.0")
class InfillColorInvocation(BaseInvocation, WithWorkflow, WithMetadata): class InfillColorInvocation(BaseInvocation):
"""Infills transparent areas of an image with a solid color""" """Infills transparent areas of an image with a solid color"""
image: ImageField = InputField(description="The image to infill") image: ImageField = InputField(description="The image to infill")
@ -143,7 +143,6 @@ class InfillColorInvocation(BaseInvocation, WithWorkflow, WithMetadata):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -155,7 +154,7 @@ class InfillColorInvocation(BaseInvocation, WithWorkflow, WithMetadata):
@invocation("infill_tile", title="Tile Infill", tags=["image", "inpaint"], category="inpaint", version="1.0.0") @invocation("infill_tile", title="Tile Infill", tags=["image", "inpaint"], category="inpaint", version="1.0.0")
class InfillTileInvocation(BaseInvocation, WithWorkflow, WithMetadata): class InfillTileInvocation(BaseInvocation):
"""Infills transparent areas of an image with tiles of the image""" """Infills transparent areas of an image with tiles of the image"""
image: ImageField = InputField(description="The image to infill") image: ImageField = InputField(description="The image to infill")
@ -180,7 +179,6 @@ class InfillTileInvocation(BaseInvocation, WithWorkflow, WithMetadata):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -194,7 +192,7 @@ class InfillTileInvocation(BaseInvocation, WithWorkflow, WithMetadata):
@invocation( @invocation(
"infill_patchmatch", title="PatchMatch Infill", tags=["image", "inpaint"], category="inpaint", version="1.0.0" "infill_patchmatch", title="PatchMatch Infill", tags=["image", "inpaint"], category="inpaint", version="1.0.0"
) )
class InfillPatchMatchInvocation(BaseInvocation, WithWorkflow, WithMetadata): class InfillPatchMatchInvocation(BaseInvocation):
"""Infills transparent areas of an image using the PatchMatch algorithm""" """Infills transparent areas of an image using the PatchMatch algorithm"""
image: ImageField = InputField(description="The image to infill") image: ImageField = InputField(description="The image to infill")
@ -234,7 +232,6 @@ class InfillPatchMatchInvocation(BaseInvocation, WithWorkflow, WithMetadata):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata,
workflow=self.workflow, workflow=self.workflow,
) )
@ -246,7 +243,7 @@ class InfillPatchMatchInvocation(BaseInvocation, WithWorkflow, WithMetadata):
@invocation("infill_lama", title="LaMa Infill", tags=["image", "inpaint"], category="inpaint", version="1.0.0") @invocation("infill_lama", title="LaMa Infill", tags=["image", "inpaint"], category="inpaint", version="1.0.0")
class LaMaInfillInvocation(BaseInvocation, WithWorkflow, WithMetadata): class LaMaInfillInvocation(BaseInvocation):
"""Infills transparent areas of an image using the LaMa model""" """Infills transparent areas of an image using the LaMa model"""
image: ImageField = InputField(description="The image to infill") image: ImageField = InputField(description="The image to infill")
@ -263,8 +260,6 @@ class LaMaInfillInvocation(BaseInvocation, WithWorkflow, WithMetadata):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata,
workflow=self.workflow,
) )
return ImageOutput( return ImageOutput(
@ -275,7 +270,7 @@ class LaMaInfillInvocation(BaseInvocation, WithWorkflow, WithMetadata):
@invocation("infill_cv2", title="CV2 Infill", tags=["image", "inpaint"], category="inpaint") @invocation("infill_cv2", title="CV2 Infill", tags=["image", "inpaint"], category="inpaint")
class CV2InfillInvocation(BaseInvocation, WithWorkflow, WithMetadata): class CV2InfillInvocation(BaseInvocation):
"""Infills transparent areas of an image using OpenCV Inpainting""" """Infills transparent areas of an image using OpenCV Inpainting"""
image: ImageField = InputField(description="The image to infill") image: ImageField = InputField(description="The image to infill")
@ -292,8 +287,6 @@ class CV2InfillInvocation(BaseInvocation, WithWorkflow, WithMetadata):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata,
workflow=self.workflow,
) )
return ImageOutput( return ImageOutput(

View File

@ -1,107 +0,0 @@
import os
from builtins import float
from typing import List, Union
from pydantic import BaseModel, ConfigDict, Field
from invokeai.app.invocations.baseinvocation import (
BaseInvocation,
BaseInvocationOutput,
Input,
InputField,
InvocationContext,
OutputField,
UIType,
invocation,
invocation_output,
)
from invokeai.app.invocations.primitives import ImageField
from invokeai.app.shared.fields import FieldDescriptions
from invokeai.backend.model_management.models.base import BaseModelType, ModelType
from invokeai.backend.model_management.models.ip_adapter import get_ip_adapter_image_encoder_model_id
class IPAdapterModelField(BaseModel):
model_name: str = Field(description="Name of the IP-Adapter model")
base_model: BaseModelType = Field(description="Base model")
model_config = ConfigDict(protected_namespaces=())
class CLIPVisionModelField(BaseModel):
model_name: str = Field(description="Name of the CLIP Vision image encoder model")
base_model: BaseModelType = Field(description="Base model (usually 'Any')")
model_config = ConfigDict(protected_namespaces=())
class IPAdapterField(BaseModel):
image: Union[ImageField, List[ImageField]] = Field(description="The IP-Adapter image prompt(s).")
ip_adapter_model: IPAdapterModelField = Field(description="The IP-Adapter model to use.")
image_encoder_model: CLIPVisionModelField = Field(description="The name of the CLIP image encoder model.")
weight: Union[float, List[float]] = Field(default=1, description="The weight given to the ControlNet")
# weight: float = Field(default=1.0, ge=0, description="The weight of the IP-Adapter.")
begin_step_percent: float = Field(
default=0, ge=0, le=1, description="When the IP-Adapter is first applied (% of total steps)"
)
end_step_percent: float = Field(
default=1, ge=0, le=1, description="When the IP-Adapter is last applied (% of total steps)"
)
@invocation_output("ip_adapter_output")
class IPAdapterOutput(BaseInvocationOutput):
# Outputs
ip_adapter: IPAdapterField = OutputField(description=FieldDescriptions.ip_adapter, title="IP-Adapter")
@invocation("ip_adapter", title="IP-Adapter", tags=["ip_adapter", "control"], category="ip_adapter", version="1.1.0")
class IPAdapterInvocation(BaseInvocation):
"""Collects IP-Adapter info to pass to other nodes."""
# Inputs
image: Union[ImageField, List[ImageField]] = InputField(description="The IP-Adapter image prompt(s).")
ip_adapter_model: IPAdapterModelField = InputField(
description="The IP-Adapter model.", title="IP-Adapter Model", input=Input.Direct, ui_order=-1
)
# weight: float = InputField(default=1.0, description="The weight of the IP-Adapter.", ui_type=UIType.Float)
weight: Union[float, List[float]] = InputField(
default=1, ge=-1, description="The weight given to the IP-Adapter", ui_type=UIType.Float, title="Weight"
)
begin_step_percent: float = InputField(
default=0, ge=-1, le=2, description="When the IP-Adapter is first applied (% of total steps)"
)
end_step_percent: float = InputField(
default=1, ge=0, le=1, description="When the IP-Adapter is last applied (% of total steps)"
)
def invoke(self, context: InvocationContext) -> IPAdapterOutput:
# Lookup the CLIP Vision encoder that is intended to be used with the IP-Adapter model.
ip_adapter_info = context.services.model_manager.model_info(
self.ip_adapter_model.model_name, self.ip_adapter_model.base_model, ModelType.IPAdapter
)
# HACK(ryand): This is bad for a couple of reasons: 1) we are bypassing the model manager to read the model
# directly, and 2) we are reading from disk every time this invocation is called without caching the result.
# A better solution would be to store the image encoder model reference in the IP-Adapter model info, but this
# is currently messy due to differences between how the model info is generated when installing a model from
# disk vs. downloading the model.
image_encoder_model_id = get_ip_adapter_image_encoder_model_id(
os.path.join(context.services.configuration.get_config().models_path, ip_adapter_info["path"])
)
image_encoder_model_name = image_encoder_model_id.split("/")[-1].strip()
image_encoder_model = CLIPVisionModelField(
model_name=image_encoder_model_name,
base_model=BaseModelType.Any,
)
return IPAdapterOutput(
ip_adapter=IPAdapterField(
image=self.image,
ip_adapter_model=self.ip_adapter_model,
image_encoder_model=image_encoder_model,
weight=self.weight,
begin_step_percent=self.begin_step_percent,
end_step_percent=self.end_step_percent,
),
)

View File

@ -1,16 +1,13 @@
# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654) # Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654)
from contextlib import ExitStack from contextlib import ExitStack
from functools import singledispatchmethod
from typing import List, Literal, Optional, Union from typing import List, Literal, Optional, Union
import einops import einops
import numpy as np import numpy as np
import torch import torch
import torchvision.transforms as T import torchvision.transforms as T
from diffusers import AutoencoderKL, AutoencoderTiny
from diffusers.image_processor import VaeImageProcessor from diffusers.image_processor import VaeImageProcessor
from diffusers.models.adapter import T2IAdapter
from diffusers.models.attention_processor import ( from diffusers.models.attention_processor import (
AttnProcessor2_0, AttnProcessor2_0,
LoRAAttnProcessor2_0, LoRAAttnProcessor2_0,
@ -19,10 +16,10 @@ from diffusers.models.attention_processor import (
) )
from diffusers.schedulers import DPMSolverSDEScheduler from diffusers.schedulers import DPMSolverSDEScheduler
from diffusers.schedulers import SchedulerMixin as Scheduler from diffusers.schedulers import SchedulerMixin as Scheduler
from pydantic import field_validator from pydantic import validator
from torchvision.transforms.functional import resize as tv_resize from torchvision.transforms.functional import resize as tv_resize
from invokeai.app.invocations.ip_adapter import IPAdapterField from invokeai.app.invocations.metadata import CoreMetadata
from invokeai.app.invocations.primitives import ( from invokeai.app.invocations.primitives import (
DenoiseMaskField, DenoiseMaskField,
DenoiseMaskOutput, DenoiseMaskOutput,
@ -32,39 +29,33 @@ from invokeai.app.invocations.primitives import (
LatentsOutput, LatentsOutput,
build_latents_output, build_latents_output,
) )
from invokeai.app.invocations.t2i_adapter import T2IAdapterField
from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
from invokeai.app.shared.fields import FieldDescriptions
from invokeai.app.util.controlnet_utils import prepare_control_image from invokeai.app.util.controlnet_utils import prepare_control_image
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.ip_adapter.ip_adapter import IPAdapter, IPAdapterPlus
from invokeai.backend.model_management.models import ModelType, SilenceWarnings from invokeai.backend.model_management.models import ModelType, SilenceWarnings
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningData, IPAdapterConditioningInfo
from ...backend.model_management.lora import ModelPatcher from ...backend.model_management.lora import ModelPatcher
from ...backend.model_management.models import BaseModelType from ...backend.model_management.models import BaseModelType
from ...backend.model_management.seamless import set_seamless from ...backend.model_management.seamless import set_seamless
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,
ControlNetData, ControlNetData,
IPAdapterData,
StableDiffusionGeneratorPipeline, StableDiffusionGeneratorPipeline,
T2IAdapterData,
image_resized_to_grid_as_tensor, image_resized_to_grid_as_tensor,
) )
from ...backend.stable_diffusion.diffusion.shared_invokeai_diffusion import 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.util.devices import choose_precision, choose_torch_device from ...backend.util.devices import choose_precision, choose_torch_device
from ..models.image import ImageCategory, ResourceOrigin
from .baseinvocation import ( from .baseinvocation import (
BaseInvocation, BaseInvocation,
BaseInvocationOutput, BaseInvocationOutput,
FieldDescriptions,
Input, Input,
InputField, InputField,
InvocationContext, InvocationContext,
OutputField, OutputField,
UIType, UIType,
WithMetadata,
WithWorkflow,
invocation, invocation,
invocation_output, invocation_output,
) )
@ -77,7 +68,8 @@ if choose_torch_device() == torch.device("mps"):
DEFAULT_PRECISION = choose_precision(choose_torch_device()) DEFAULT_PRECISION = choose_precision(choose_torch_device())
SAMPLER_NAME_VALUES = Literal[tuple(SCHEDULER_MAP.keys())]
SAMPLER_NAME_VALUES = Literal[tuple(list(SCHEDULER_MAP.keys()))]
@invocation_output("scheduler_output") @invocation_output("scheduler_output")
@ -85,20 +77,12 @@ class SchedulerOutput(BaseInvocationOutput):
scheduler: SAMPLER_NAME_VALUES = OutputField(description=FieldDescriptions.scheduler, ui_type=UIType.Scheduler) scheduler: SAMPLER_NAME_VALUES = OutputField(description=FieldDescriptions.scheduler, ui_type=UIType.Scheduler)
@invocation( @invocation("scheduler", title="Scheduler", tags=["scheduler"], category="latents", version="1.0.0")
"scheduler",
title="Scheduler",
tags=["scheduler"],
category="latents",
version="1.0.0",
)
class SchedulerInvocation(BaseInvocation): class SchedulerInvocation(BaseInvocation):
"""Selects a scheduler.""" """Selects a scheduler."""
scheduler: SAMPLER_NAME_VALUES = InputField( scheduler: SAMPLER_NAME_VALUES = InputField(
default="euler", default="euler", description=FieldDescriptions.scheduler, ui_type=UIType.Scheduler
description=FieldDescriptions.scheduler,
ui_type=UIType.Scheduler,
) )
def invoke(self, context: InvocationContext) -> SchedulerOutput: def invoke(self, context: InvocationContext) -> SchedulerOutput:
@ -106,11 +90,7 @@ class SchedulerInvocation(BaseInvocation):
@invocation( @invocation(
"create_denoise_mask", "create_denoise_mask", title="Create Denoise Mask", tags=["mask", "denoise"], category="latents", version="1.0.0"
title="Create Denoise Mask",
tags=["mask", "denoise"],
category="latents",
version="1.0.0",
) )
class CreateDenoiseMaskInvocation(BaseInvocation): class CreateDenoiseMaskInvocation(BaseInvocation):
"""Creates mask for denoising model run.""" """Creates mask for denoising model run."""
@ -119,11 +99,7 @@ class CreateDenoiseMaskInvocation(BaseInvocation):
image: Optional[ImageField] = InputField(default=None, description="Image which will be masked", ui_order=1) image: Optional[ImageField] = InputField(default=None, description="Image which will be masked", ui_order=1)
mask: ImageField = InputField(description="The mask to use when pasting", ui_order=2) mask: ImageField = InputField(description="The mask to use when pasting", ui_order=2)
tiled: bool = InputField(default=False, description=FieldDescriptions.tiled, ui_order=3) tiled: bool = InputField(default=False, description=FieldDescriptions.tiled, ui_order=3)
fp32: bool = InputField( fp32: bool = InputField(default=DEFAULT_PRECISION == "float32", description=FieldDescriptions.fp32, ui_order=4)
default=DEFAULT_PRECISION == "float32",
description=FieldDescriptions.fp32,
ui_order=4,
)
def prep_mask_tensor(self, mask_image): def prep_mask_tensor(self, mask_image):
if mask_image.mode != "L": if mask_image.mode != "L":
@ -151,7 +127,7 @@ class CreateDenoiseMaskInvocation(BaseInvocation):
if image is not None: if image is not None:
vae_info = context.services.model_manager.get_model( vae_info = context.services.model_manager.get_model(
**self.vae.vae.model_dump(), **self.vae.vae.dict(),
context=context, context=context,
) )
@ -184,7 +160,7 @@ def get_scheduler(
) -> 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.model_dump(), **scheduler_info.dict(),
context=context, context=context,
) )
with orig_scheduler_info as orig_scheduler: with orig_scheduler_info as orig_scheduler:
@ -215,7 +191,7 @@ def get_scheduler(
title="Denoise Latents", title="Denoise Latents",
tags=["latents", "denoise", "txt2img", "t2i", "t2l", "img2img", "i2i", "l2l"], tags=["latents", "denoise", "txt2img", "t2i", "t2l", "img2img", "i2i", "l2l"],
category="latents", category="latents",
version="1.4.0", version="1.0.0",
) )
class DenoiseLatentsInvocation(BaseInvocation): class DenoiseLatentsInvocation(BaseInvocation):
"""Denoises noisy latents to decodable images""" """Denoises noisy latents to decodable images"""
@ -226,64 +202,29 @@ class DenoiseLatentsInvocation(BaseInvocation):
negative_conditioning: ConditioningField = InputField( negative_conditioning: ConditioningField = InputField(
description=FieldDescriptions.negative_cond, input=Input.Connection, ui_order=1 description=FieldDescriptions.negative_cond, input=Input.Connection, ui_order=1
) )
noise: Optional[LatentsField] = InputField( noise: Optional[LatentsField] = InputField(description=FieldDescriptions.noise, input=Input.Connection, ui_order=3)
default=None,
description=FieldDescriptions.noise,
input=Input.Connection,
ui_order=3,
)
steps: int = InputField(default=10, gt=0, description=FieldDescriptions.steps) steps: int = InputField(default=10, gt=0, description=FieldDescriptions.steps)
cfg_scale: Union[float, List[float]] = InputField( cfg_scale: Union[float, List[float]] = InputField(
default=7.5, ge=1, description=FieldDescriptions.cfg_scale, title="CFG Scale" default=7.5, ge=1, description=FieldDescriptions.cfg_scale, ui_type=UIType.Float, title="CFG Scale"
)
denoising_start: float = InputField(
default=0.0,
ge=0,
le=1,
description=FieldDescriptions.denoising_start,
) )
denoising_start: float = InputField(default=0.0, ge=0, le=1, description=FieldDescriptions.denoising_start)
denoising_end: float = InputField(default=1.0, ge=0, le=1, description=FieldDescriptions.denoising_end) denoising_end: float = InputField(default=1.0, ge=0, le=1, description=FieldDescriptions.denoising_end)
scheduler: SAMPLER_NAME_VALUES = InputField( scheduler: SAMPLER_NAME_VALUES = InputField(
default="euler", default="euler", description=FieldDescriptions.scheduler, ui_type=UIType.Scheduler
description=FieldDescriptions.scheduler,
ui_type=UIType.Scheduler,
) )
unet: UNetField = InputField( unet: UNetField = InputField(description=FieldDescriptions.unet, input=Input.Connection, title="UNet", ui_order=2)
description=FieldDescriptions.unet, control: Union[ControlField, list[ControlField]] = InputField(
input=Input.Connection,
title="UNet",
ui_order=2,
)
control: Optional[Union[ControlField, list[ControlField]]] = InputField(
default=None, default=None,
description=FieldDescriptions.control,
input=Input.Connection, input=Input.Connection,
ui_order=5, ui_order=5,
) )
ip_adapter: Optional[Union[IPAdapterField, list[IPAdapterField]]] = InputField( latents: Optional[LatentsField] = InputField(description=FieldDescriptions.latents, input=Input.Connection)
description=FieldDescriptions.ip_adapter,
title="IP-Adapter",
default=None,
input=Input.Connection,
ui_order=6,
)
t2i_adapter: Optional[Union[T2IAdapterField, list[T2IAdapterField]]] = InputField(
description=FieldDescriptions.t2i_adapter,
title="T2I-Adapter",
default=None,
input=Input.Connection,
ui_order=7,
)
latents: Optional[LatentsField] = InputField(
default=None, description=FieldDescriptions.latents, input=Input.Connection
)
denoise_mask: Optional[DenoiseMaskField] = InputField( denoise_mask: Optional[DenoiseMaskField] = InputField(
default=None, default=None, description=FieldDescriptions.mask, input=Input.Connection, ui_order=6
description=FieldDescriptions.mask,
input=Input.Connection,
ui_order=8,
) )
@field_validator("cfg_scale") @validator("cfg_scale")
def ge_one(cls, v): def ge_one(cls, v):
"""validate that all cfg_scale values are >= 1""" """validate that all cfg_scale values are >= 1"""
if isinstance(v, list): if isinstance(v, list):
@ -306,7 +247,7 @@ class DenoiseLatentsInvocation(BaseInvocation):
stable_diffusion_step_callback( stable_diffusion_step_callback(
context=context, context=context,
intermediate_state=intermediate_state, intermediate_state=intermediate_state,
node=self.model_dump(), node=self.dict(),
source_node_id=source_node_id, source_node_id=source_node_id,
base_model=base_model, base_model=base_model,
) )
@ -382,6 +323,8 @@ class DenoiseLatentsInvocation(BaseInvocation):
def prep_control_data( def prep_control_data(
self, self,
context: InvocationContext, context: InvocationContext,
# really only need model for dtype and device
model: StableDiffusionGeneratorPipeline,
control_input: Union[ControlField, List[ControlField]], control_input: Union[ControlField, List[ControlField]],
latents_shape: List[int], latents_shape: List[int],
exit_stack: ExitStack, exit_stack: ExitStack,
@ -401,207 +344,57 @@ class DenoiseLatentsInvocation(BaseInvocation):
else: else:
control_list = None control_list = None
if control_list is None: if control_list is None:
return None control_data = None
# After 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:
# FIXME: add checks to skip entry if model or image is None # FIXME: add checks to skip entry if model or image is None
# and if weight is None, populate with default 1.0? # and if weight is None, populate with default 1.0?
controlnet_data = [] control_data = []
for control_info in control_list: control_models = []
control_model = exit_stack.enter_context( for control_info in control_list:
context.services.model_manager.get_model( control_model = exit_stack.enter_context(
model_name=control_info.control_model.model_name, context.services.model_manager.get_model(
model_type=ModelType.ControlNet, model_name=control_info.control_model.model_name,
base_model=control_info.control_model.base_model, model_type=ModelType.ControlNet,
context=context, base_model=control_info.control_model.base_model,
) context=context,
) )
# control_models.append(control_model)
control_image_field = control_info.image
input_image = context.services.images.get_pil_image(control_image_field.image_name)
# self.image.image_type, self.image.image_name
# FIXME: still need to test with different widths, heights, devices, dtypes
# and add in batch_size, num_images_per_prompt?
# and do real check for classifier_free_guidance?
# prepare_control_image should return torch.Tensor of shape(batch_size, 3, height, width)
control_image = prepare_control_image(
image=input_image,
do_classifier_free_guidance=do_classifier_free_guidance,
width=control_width_resize,
height=control_height_resize,
# batch_size=batch_size * num_images_per_prompt,
# num_images_per_prompt=num_images_per_prompt,
device=control_model.device,
dtype=control_model.dtype,
control_mode=control_info.control_mode,
resize_mode=control_info.resize_mode,
)
control_item = ControlNetData(
model=control_model, # model object
image_tensor=control_image,
weight=control_info.control_weight,
begin_step_percent=control_info.begin_step_percent,
end_step_percent=control_info.end_step_percent,
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,
)
controlnet_data.append(control_item)
# MultiControlNetModel has been refactored out, just need list[ControlNetData]
return controlnet_data
def prep_ip_adapter_data(
self,
context: InvocationContext,
ip_adapter: Optional[Union[IPAdapterField, list[IPAdapterField]]],
conditioning_data: ConditioningData,
exit_stack: ExitStack,
) -> Optional[list[IPAdapterData]]:
"""If IP-Adapter is enabled, then this function loads the requisite models, and adds the image prompt embeddings
to the `conditioning_data` (in-place).
"""
if ip_adapter is None:
return None
# ip_adapter could be a list or a single IPAdapterField. Normalize to a list here.
if not isinstance(ip_adapter, list):
ip_adapter = [ip_adapter]
if len(ip_adapter) == 0:
return None
ip_adapter_data_list = []
conditioning_data.ip_adapter_conditioning = []
for single_ip_adapter in ip_adapter:
ip_adapter_model: Union[IPAdapter, IPAdapterPlus] = exit_stack.enter_context(
context.services.model_manager.get_model(
model_name=single_ip_adapter.ip_adapter_model.model_name,
model_type=ModelType.IPAdapter,
base_model=single_ip_adapter.ip_adapter_model.base_model,
context=context,
)
)
image_encoder_model_info = context.services.model_manager.get_model(
model_name=single_ip_adapter.image_encoder_model.model_name,
model_type=ModelType.CLIPVision,
base_model=single_ip_adapter.image_encoder_model.base_model,
context=context,
)
# `single_ip_adapter.image` could be a list or a single ImageField. Normalize to a list here.
single_ipa_images = single_ip_adapter.image
if not isinstance(single_ipa_images, list):
single_ipa_images = [single_ipa_images]
single_ipa_images = [context.services.images.get_pil_image(image.image_name) for image in single_ipa_images]
# TODO(ryand): With some effort, the step of running the CLIP Vision encoder could be done before any other
# models are needed in memory. This would help to reduce peak memory utilization in low-memory environments.
with image_encoder_model_info as image_encoder_model:
# Get image embeddings from CLIP and ImageProjModel.
image_prompt_embeds, uncond_image_prompt_embeds = ip_adapter_model.get_image_embeds(
single_ipa_images, image_encoder_model
) )
conditioning_data.ip_adapter_conditioning.append( control_models.append(control_model)
IPAdapterConditioningInfo(image_prompt_embeds, uncond_image_prompt_embeds) control_image_field = control_info.image
input_image = context.services.images.get_pil_image(control_image_field.image_name)
# self.image.image_type, self.image.image_name
# FIXME: still need to test with different widths, heights, devices, dtypes
# and add in batch_size, num_images_per_prompt?
# and do real check for classifier_free_guidance?
# prepare_control_image should return torch.Tensor of shape(batch_size, 3, height, width)
control_image = prepare_control_image(
image=input_image,
do_classifier_free_guidance=do_classifier_free_guidance,
width=control_width_resize,
height=control_height_resize,
# batch_size=batch_size * num_images_per_prompt,
# num_images_per_prompt=num_images_per_prompt,
device=control_model.device,
dtype=control_model.dtype,
control_mode=control_info.control_mode,
resize_mode=control_info.resize_mode,
) )
control_item = ControlNetData(
ip_adapter_data_list.append( model=control_model,
IPAdapterData( image_tensor=control_image,
ip_adapter_model=ip_adapter_model, weight=control_info.control_weight,
weight=single_ip_adapter.weight, begin_step_percent=control_info.begin_step_percent,
begin_step_percent=single_ip_adapter.begin_step_percent, end_step_percent=control_info.end_step_percent,
end_step_percent=single_ip_adapter.end_step_percent, 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)
# MultiControlNetModel has been refactored out, just need list[ControlNetData]
return ip_adapter_data_list return control_data
def run_t2i_adapters(
self,
context: InvocationContext,
t2i_adapter: Optional[Union[T2IAdapterField, list[T2IAdapterField]]],
latents_shape: list[int],
do_classifier_free_guidance: bool,
) -> Optional[list[T2IAdapterData]]:
if t2i_adapter is None:
return None
# Handle the possibility that t2i_adapter could be a list or a single T2IAdapterField.
if isinstance(t2i_adapter, T2IAdapterField):
t2i_adapter = [t2i_adapter]
if len(t2i_adapter) == 0:
return None
t2i_adapter_data = []
for t2i_adapter_field in t2i_adapter:
t2i_adapter_model_info = context.services.model_manager.get_model(
model_name=t2i_adapter_field.t2i_adapter_model.model_name,
model_type=ModelType.T2IAdapter,
base_model=t2i_adapter_field.t2i_adapter_model.base_model,
context=context,
)
image = context.services.images.get_pil_image(t2i_adapter_field.image.image_name)
# The max_unet_downscale is the maximum amount that the UNet model downscales the latent image internally.
if t2i_adapter_field.t2i_adapter_model.base_model == BaseModelType.StableDiffusion1:
max_unet_downscale = 8
elif t2i_adapter_field.t2i_adapter_model.base_model == BaseModelType.StableDiffusionXL:
max_unet_downscale = 4
else:
raise ValueError(
f"Unexpected T2I-Adapter base model type: '{t2i_adapter_field.t2i_adapter_model.base_model}'."
)
t2i_adapter_model: T2IAdapter
with t2i_adapter_model_info as t2i_adapter_model:
total_downscale_factor = t2i_adapter_model.total_downscale_factor
# Resize the T2I-Adapter input image.
# We select the resize dimensions so that after the T2I-Adapter's total_downscale_factor is applied, the
# result will match the latent image's dimensions after max_unet_downscale is applied.
t2i_input_height = latents_shape[2] // max_unet_downscale * total_downscale_factor
t2i_input_width = latents_shape[3] // max_unet_downscale * total_downscale_factor
# Note: We have hard-coded `do_classifier_free_guidance=False`. This is because we only want to prepare
# a single image. If CFG is enabled, we will duplicate the resultant tensor after applying the
# T2I-Adapter model.
#
# Note: We re-use the `prepare_control_image(...)` from ControlNet for T2I-Adapter, because it has many
# of the same requirements (e.g. preserving binary masks during resize).
t2i_image = prepare_control_image(
image=image,
do_classifier_free_guidance=False,
width=t2i_input_width,
height=t2i_input_height,
num_channels=t2i_adapter_model.config.in_channels,
device=t2i_adapter_model.device,
dtype=t2i_adapter_model.dtype,
resize_mode=t2i_adapter_field.resize_mode,
)
adapter_state = t2i_adapter_model(t2i_image)
if do_classifier_free_guidance:
for idx, value in enumerate(adapter_state):
adapter_state[idx] = torch.cat([value] * 2, dim=0)
t2i_adapter_data.append(
T2IAdapterData(
adapter_state=adapter_state,
weight=t2i_adapter_field.weight,
begin_step_percent=t2i_adapter_field.begin_step_percent,
end_step_percent=t2i_adapter_field.end_step_percent,
)
)
return t2i_adapter_data
# original idea by https://github.com/AmericanPresidentJimmyCarter # original idea by https://github.com/AmericanPresidentJimmyCarter
# TODO: research more for second order schedulers timesteps # TODO: research more for second order schedulers timesteps
@ -674,15 +467,6 @@ class DenoiseLatentsInvocation(BaseInvocation):
mask, masked_latents = self.prep_inpaint_mask(context, latents) mask, masked_latents = self.prep_inpaint_mask(context, latents)
# TODO(ryand): I have hard-coded `do_classifier_free_guidance=True` to mirror the behaviour of ControlNets,
# below. Investigate whether this is appropriate.
t2i_adapter_data = self.run_t2i_adapters(
context,
self.t2i_adapter,
latents.shape,
do_classifier_free_guidance=True,
)
# 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]
@ -693,7 +477,7 @@ class DenoiseLatentsInvocation(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.model_dump(exclude={"weight"}), **lora.dict(exclude={"weight"}),
context=context, context=context,
) )
yield (lora_info.context.model, lora.weight) yield (lora_info.context.model, lora.weight)
@ -701,18 +485,12 @@ class DenoiseLatentsInvocation(BaseInvocation):
return return
unet_info = context.services.model_manager.get_model( unet_info = context.services.model_manager.get_model(
**self.unet.unet.model_dump(), **self.unet.unet.dict(),
context=context, context=context,
) )
with ( with ExitStack() as exit_stack, ModelPatcher.apply_lora_unet(
ExitStack() as exit_stack, unet_info.context.model, _lora_loader()
ModelPatcher.apply_lora_unet(unet_info.context.model, _lora_loader()), ), set_seamless(unet_info.context.model, self.unet.seamless_axes), unet_info as unet:
ModelPatcher.apply_freeu(unet_info.context.model, self.unet.freeu_config),
set_seamless(unet_info.context.model, self.unet.seamless_axes),
unet_info as unet,
# Apply the LoRA after unet has been moved to its target device for faster patching.
ModelPatcher.apply_lora_unet(unet, _lora_loader()),
):
latents = latents.to(device=unet.device, dtype=unet.dtype) latents = latents.to(device=unet.device, dtype=unet.dtype)
if noise is not None: if noise is not None:
noise = noise.to(device=unet.device, dtype=unet.dtype) noise = noise.to(device=unet.device, dtype=unet.dtype)
@ -731,7 +509,8 @@ class DenoiseLatentsInvocation(BaseInvocation):
pipeline = self.create_pipeline(unet, scheduler) pipeline = self.create_pipeline(unet, scheduler)
conditioning_data = self.get_conditioning_data(context, scheduler, unet, seed) conditioning_data = self.get_conditioning_data(context, scheduler, unet, seed)
controlnet_data = self.prep_control_data( control_data = self.prep_control_data(
model=pipeline,
context=context, context=context,
control_input=self.control, control_input=self.control,
latents_shape=latents.shape, latents_shape=latents.shape,
@ -740,13 +519,6 @@ class DenoiseLatentsInvocation(BaseInvocation):
exit_stack=exit_stack, exit_stack=exit_stack,
) )
ip_adapter_data = self.prep_ip_adapter_data(
context=context,
ip_adapter=self.ip_adapter,
conditioning_data=conditioning_data,
exit_stack=exit_stack,
)
num_inference_steps, timesteps, init_timestep = self.init_scheduler( num_inference_steps, timesteps, init_timestep = self.init_scheduler(
scheduler, scheduler,
device=unet.device, device=unet.device,
@ -755,10 +527,7 @@ class DenoiseLatentsInvocation(BaseInvocation):
denoising_end=self.denoising_end, denoising_end=self.denoising_end,
) )
( result_latents, result_attention_map_saver = pipeline.latents_from_embeddings(
result_latents,
result_attention_map_saver,
) = pipeline.latents_from_embeddings(
latents=latents, latents=latents,
timesteps=timesteps, timesteps=timesteps,
init_timestep=init_timestep, init_timestep=init_timestep,
@ -768,9 +537,7 @@ class DenoiseLatentsInvocation(BaseInvocation):
masked_latents=masked_latents, masked_latents=masked_latents,
num_inference_steps=num_inference_steps, num_inference_steps=num_inference_steps,
conditioning_data=conditioning_data, conditioning_data=conditioning_data,
control_data=controlnet_data, control_data=control_data, # list[ControlNetData]
ip_adapter_data=ip_adapter_data,
t2i_adapter_data=t2i_adapter_data,
callback=step_callback, callback=step_callback,
) )
@ -786,13 +553,9 @@ class DenoiseLatentsInvocation(BaseInvocation):
@invocation( @invocation(
"l2i", "l2i", title="Latents to Image", tags=["latents", "image", "vae", "l2i"], category="latents", version="1.0.0"
title="Latents to Image",
tags=["latents", "image", "vae", "l2i"],
category="latents",
version="1.0.0",
) )
class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow): class LatentsToImageInvocation(BaseInvocation):
"""Generates an image from latents.""" """Generates an image from latents."""
latents: LatentsField = InputField( latents: LatentsField = InputField(
@ -805,13 +568,18 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow):
) )
tiled: bool = InputField(default=False, description=FieldDescriptions.tiled) tiled: bool = InputField(default=False, description=FieldDescriptions.tiled)
fp32: bool = InputField(default=DEFAULT_PRECISION == "float32", description=FieldDescriptions.fp32) fp32: bool = InputField(default=DEFAULT_PRECISION == "float32", description=FieldDescriptions.fp32)
metadata: CoreMetadata = InputField(
default=None,
description=FieldDescriptions.core_metadata,
ui_hidden=True,
)
@torch.no_grad() @torch.no_grad()
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
latents = context.services.latents.get(self.latents.latents_name) latents = context.services.latents.get(self.latents.latents_name)
vae_info = context.services.model_manager.get_model( vae_info = context.services.model_manager.get_model(
**self.vae.vae.model_dump(), **self.vae.vae.dict(),
context=context, context=context,
) )
@ -873,7 +641,7 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata, metadata=self.metadata.dict() if self.metadata else None,
workflow=self.workflow, workflow=self.workflow,
) )
@ -887,13 +655,7 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow):
LATENTS_INTERPOLATION_MODE = Literal["nearest", "linear", "bilinear", "bicubic", "trilinear", "area", "nearest-exact"] LATENTS_INTERPOLATION_MODE = Literal["nearest", "linear", "bilinear", "bicubic", "trilinear", "area", "nearest-exact"]
@invocation( @invocation("lresize", title="Resize Latents", tags=["latents", "resize"], category="latents", version="1.0.0")
"lresize",
title="Resize Latents",
tags=["latents", "resize"],
category="latents",
version="1.0.0",
)
class ResizeLatentsInvocation(BaseInvocation): class ResizeLatentsInvocation(BaseInvocation):
"""Resizes latents to explicit width/height (in pixels). Provided dimensions are floor-divided by 8.""" """Resizes latents to explicit width/height (in pixels). Provided dimensions are floor-divided by 8."""
@ -939,13 +701,7 @@ class ResizeLatentsInvocation(BaseInvocation):
return build_latents_output(latents_name=name, latents=resized_latents, seed=self.latents.seed) return build_latents_output(latents_name=name, latents=resized_latents, seed=self.latents.seed)
@invocation( @invocation("lscale", title="Scale Latents", tags=["latents", "resize"], category="latents", version="1.0.0")
"lscale",
title="Scale Latents",
tags=["latents", "resize"],
category="latents",
version="1.0.0",
)
class ScaleLatentsInvocation(BaseInvocation): class ScaleLatentsInvocation(BaseInvocation):
"""Scales latents by a given factor.""" """Scales latents by a given factor."""
@ -984,11 +740,7 @@ class ScaleLatentsInvocation(BaseInvocation):
@invocation( @invocation(
"i2l", "i2l", title="Image to Latents", tags=["latents", "image", "vae", "i2l"], category="latents", version="1.0.0"
title="Image to Latents",
tags=["latents", "image", "vae", "i2l"],
category="latents",
version="1.0.0",
) )
class ImageToLatentsInvocation(BaseInvocation): class ImageToLatentsInvocation(BaseInvocation):
"""Encodes an image into latents.""" """Encodes an image into latents."""
@ -1040,7 +792,8 @@ class ImageToLatentsInvocation(BaseInvocation):
# non_noised_latents_from_image # non_noised_latents_from_image
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():
latents = ImageToLatentsInvocation._encode_to_tensor(vae, image_tensor) 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 = vae.config.scaling_factor * latents latents = vae.config.scaling_factor * latents
latents = latents.to(dtype=orig_dtype) latents = latents.to(dtype=orig_dtype)
@ -1052,7 +805,7 @@ 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( vae_info = context.services.model_manager.get_model(
**self.vae.vae.model_dump(), **self.vae.vae.dict(),
context=context, context=context,
) )
@ -1067,26 +820,8 @@ class ImageToLatentsInvocation(BaseInvocation):
context.services.latents.save(name, latents) context.services.latents.save(name, latents)
return build_latents_output(latents_name=name, latents=latents, seed=None) return build_latents_output(latents_name=name, latents=latents, seed=None)
@singledispatchmethod
@staticmethod
def _encode_to_tensor(vae: AutoencoderKL, image_tensor: torch.FloatTensor) -> torch.FloatTensor:
image_tensor_dist = vae.encode(image_tensor).latent_dist
latents = image_tensor_dist.sample().to(dtype=vae.dtype) # FIXME: uses torch.randn. make reproducible!
return latents
@_encode_to_tensor.register @invocation("lblend", title="Blend Latents", tags=["latents", "blend"], category="latents", version="1.0.0")
@staticmethod
def _(vae: AutoencoderTiny, image_tensor: torch.FloatTensor) -> torch.FloatTensor:
return vae.encode(image_tensor).latents
@invocation(
"lblend",
title="Blend Latents",
tags=["latents", "blend"],
category="latents",
version="1.0.0",
)
class BlendLatentsInvocation(BaseInvocation): class BlendLatentsInvocation(BaseInvocation):
"""Blend two latents using a given alpha. Latents must have same size.""" """Blend two latents using a given alpha. Latents must have same size."""
@ -1105,7 +840,7 @@ class BlendLatentsInvocation(BaseInvocation):
latents_b = context.services.latents.get(self.latents_b.latents_name) latents_b = context.services.latents.get(self.latents_b.latents_name)
if latents_a.shape != latents_b.shape: if latents_a.shape != latents_b.shape:
raise Exception("Latents to blend must be the same size.") raise "Latents to blend must be the same size."
# TODO: # TODO:
device = choose_torch_device() device = choose_torch_device()

View File

@ -3,12 +3,11 @@
from typing import Literal from typing import Literal
import numpy as np import numpy as np
from pydantic import ValidationInfo, field_validator from pydantic import validator
from invokeai.app.invocations.primitives import FloatOutput, IntegerOutput from invokeai.app.invocations.primitives import FloatOutput, IntegerOutput
from invokeai.app.shared.fields import FieldDescriptions
from .baseinvocation import BaseInvocation, InputField, InvocationContext, invocation from .baseinvocation import BaseInvocation, FieldDescriptions, InputField, InvocationContext, invocation
@invocation("add", title="Add Integers", tags=["math", "add"], category="math", version="1.0.0") @invocation("add", title="Add Integers", tags=["math", "add"], category="math", version="1.0.0")
@ -66,34 +65,13 @@ class DivideInvocation(BaseInvocation):
class RandomIntInvocation(BaseInvocation): class RandomIntInvocation(BaseInvocation):
"""Outputs a single random integer.""" """Outputs a single random integer."""
low: int = InputField(default=0, description=FieldDescriptions.inclusive_low) low: int = InputField(default=0, description="The inclusive low value")
high: int = InputField(default=np.iinfo(np.int32).max, description=FieldDescriptions.exclusive_high) high: int = InputField(default=np.iinfo(np.int32).max, description="The exclusive high value")
def invoke(self, context: InvocationContext) -> IntegerOutput: def invoke(self, context: InvocationContext) -> IntegerOutput:
return IntegerOutput(value=np.random.randint(self.low, self.high)) return IntegerOutput(value=np.random.randint(self.low, self.high))
@invocation(
"rand_float",
title="Random Float",
tags=["math", "float", "random"],
category="math",
version="1.0.1",
use_cache=False,
)
class RandomFloatInvocation(BaseInvocation):
"""Outputs a single random float"""
low: float = InputField(default=0.0, description=FieldDescriptions.inclusive_low)
high: float = InputField(default=1.0, description=FieldDescriptions.exclusive_high)
decimals: int = InputField(default=2, description=FieldDescriptions.decimal_places)
def invoke(self, context: InvocationContext) -> FloatOutput:
random_float = np.random.uniform(self.low, self.high)
rounded_float = round(random_float, self.decimals)
return FloatOutput(value=rounded_float)
@invocation( @invocation(
"float_to_int", "float_to_int",
title="Float To Integer", title="Float To Integer",
@ -145,17 +123,17 @@ INTEGER_OPERATIONS = Literal[
] ]
INTEGER_OPERATIONS_LABELS = { INTEGER_OPERATIONS_LABELS = dict(
"ADD": "Add A+B", ADD="Add A+B",
"SUB": "Subtract A-B", SUB="Subtract A-B",
"MUL": "Multiply A*B", MUL="Multiply A*B",
"DIV": "Divide A/B", DIV="Divide A/B",
"EXP": "Exponentiate A^B", EXP="Exponentiate A^B",
"MOD": "Modulus A%B", MOD="Modulus A%B",
"ABS": "Absolute Value of A", ABS="Absolute Value of A",
"MIN": "Minimum(A,B)", MIN="Minimum(A,B)",
"MAX": "Maximum(A,B)", MAX="Maximum(A,B)",
} )
@invocation( @invocation(
@ -183,16 +161,16 @@ class IntegerMathInvocation(BaseInvocation):
operation: INTEGER_OPERATIONS = InputField( operation: INTEGER_OPERATIONS = InputField(
default="ADD", description="The operation to perform", ui_choice_labels=INTEGER_OPERATIONS_LABELS default="ADD", description="The operation to perform", ui_choice_labels=INTEGER_OPERATIONS_LABELS
) )
a: int = InputField(default=1, description=FieldDescriptions.num_1) a: int = InputField(default=0, description=FieldDescriptions.num_1)
b: int = InputField(default=1, description=FieldDescriptions.num_2) b: int = InputField(default=0, description=FieldDescriptions.num_2)
@field_validator("b") @validator("b")
def no_unrepresentable_results(cls, v: int, info: ValidationInfo): def no_unrepresentable_results(cls, v, values):
if info.data["operation"] == "DIV" and v == 0: if values["operation"] == "DIV" and v == 0:
raise ValueError("Cannot divide by zero") raise ValueError("Cannot divide by zero")
elif info.data["operation"] == "MOD" and v == 0: elif values["operation"] == "MOD" and v == 0:
raise ValueError("Cannot divide by zero") raise ValueError("Cannot divide by zero")
elif info.data["operation"] == "EXP" and v < 0: elif values["operation"] == "EXP" and v < 0:
raise ValueError("Result of exponentiation is not an integer") raise ValueError("Result of exponentiation is not an integer")
return v return v
@ -231,17 +209,17 @@ FLOAT_OPERATIONS = Literal[
] ]
FLOAT_OPERATIONS_LABELS = { FLOAT_OPERATIONS_LABELS = dict(
"ADD": "Add A+B", ADD="Add A+B",
"SUB": "Subtract A-B", SUB="Subtract A-B",
"MUL": "Multiply A*B", MUL="Multiply A*B",
"DIV": "Divide A/B", DIV="Divide A/B",
"EXP": "Exponentiate A^B", EXP="Exponentiate A^B",
"ABS": "Absolute Value of A", ABS="Absolute Value of A",
"SQRT": "Square Root of A", SQRT="Square Root of A",
"MIN": "Minimum(A,B)", MIN="Minimum(A,B)",
"MAX": "Maximum(A,B)", MAX="Maximum(A,B)",
} )
@invocation( @invocation(
@ -257,16 +235,16 @@ class FloatMathInvocation(BaseInvocation):
operation: FLOAT_OPERATIONS = InputField( operation: FLOAT_OPERATIONS = InputField(
default="ADD", description="The operation to perform", ui_choice_labels=FLOAT_OPERATIONS_LABELS default="ADD", description="The operation to perform", ui_choice_labels=FLOAT_OPERATIONS_LABELS
) )
a: float = InputField(default=1, description=FieldDescriptions.num_1) a: float = InputField(default=0, description=FieldDescriptions.num_1)
b: float = InputField(default=1, description=FieldDescriptions.num_2) b: float = InputField(default=0, description=FieldDescriptions.num_2)
@field_validator("b") @validator("b")
def no_unrepresentable_results(cls, v: float, info: ValidationInfo): def no_unrepresentable_results(cls, v, values):
if info.data["operation"] == "DIV" and v == 0: if values["operation"] == "DIV" and v == 0:
raise ValueError("Cannot divide by zero") raise ValueError("Cannot divide by zero")
elif info.data["operation"] == "EXP" and info.data["a"] == 0 and v < 0: elif values["operation"] == "EXP" and values["a"] == 0 and v < 0:
raise ValueError("Cannot raise zero to a negative power") raise ValueError("Cannot raise zero to a negative power")
elif info.data["operation"] == "EXP" and isinstance(info.data["a"] ** v, complex): elif values["operation"] == "EXP" and type(values["a"] ** v) is complex:
raise ValueError("Root operation resulted in a complex number") raise ValueError("Root operation resulted in a complex number")
return v return v

View File

@ -1,151 +1,127 @@
from typing import Any, Literal, Optional, Union from typing import Optional
from pydantic import BaseModel, ConfigDict, Field from pydantic import Field
from invokeai.app.invocations.baseinvocation import ( from invokeai.app.invocations.baseinvocation import (
BaseInvocation, BaseInvocation,
BaseInvocationOutput, BaseInvocationOutput,
InputField, InputField,
InvocationContext, InvocationContext,
MetadataField,
OutputField, OutputField,
UIType,
invocation, invocation,
invocation_output, invocation_output,
) )
from invokeai.app.invocations.controlnet_image_processors import ControlField from invokeai.app.invocations.controlnet_image_processors import ControlField
from invokeai.app.invocations.ip_adapter import IPAdapterModelField
from invokeai.app.invocations.model import LoRAModelField, MainModelField, VAEModelField from invokeai.app.invocations.model import LoRAModelField, MainModelField, VAEModelField
from invokeai.app.invocations.primitives import ImageField from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
from invokeai.app.invocations.t2i_adapter import T2IAdapterField
from invokeai.app.shared.fields import FieldDescriptions
from ...version import __version__ from ...version import __version__
class MetadataItemField(BaseModel): class LoRAMetadataField(BaseModelExcludeNull):
label: str = Field(description=FieldDescriptions.metadata_item_label) """LoRA metadata for an image generated in InvokeAI."""
value: Any = Field(description=FieldDescriptions.metadata_item_value)
lora: LoRAModelField = Field(description="The LoRA model")
weight: float = Field(description="The weight of the LoRA model")
class LoRAMetadataField(BaseModel): class CoreMetadata(BaseModelExcludeNull):
"""LoRA Metadata Field""" """Core generation metadata for an image generated in InvokeAI."""
lora: LoRAModelField = Field(description=FieldDescriptions.lora_model) app_version: str = Field(default=__version__, description="The version of InvokeAI used to generate this image")
weight: float = Field(description=FieldDescriptions.lora_weight) generation_mode: str = Field(
class IPAdapterMetadataField(BaseModel):
"""IP Adapter Field, minus the CLIP Vision Encoder model"""
image: ImageField = Field(description="The IP-Adapter image prompt.")
ip_adapter_model: IPAdapterModelField = Field(
description="The IP-Adapter model.",
)
weight: Union[float, list[float]] = Field(
description="The weight given to the IP-Adapter",
)
begin_step_percent: float = Field(description="When the IP-Adapter is first applied (% of total steps)")
end_step_percent: float = Field(description="When the IP-Adapter is last applied (% of total steps)")
@invocation_output("metadata_item_output")
class MetadataItemOutput(BaseInvocationOutput):
"""Metadata Item Output"""
item: MetadataItemField = OutputField(description="Metadata Item")
@invocation("metadata_item", title="Metadata Item", tags=["metadata"], category="metadata", version="1.0.0")
class MetadataItemInvocation(BaseInvocation):
"""Used to create an arbitrary metadata item. Provide "label" and make a connection to "value" to store that data as the value."""
label: str = InputField(description=FieldDescriptions.metadata_item_label)
value: Any = InputField(description=FieldDescriptions.metadata_item_value, ui_type=UIType.Any)
def invoke(self, context: InvocationContext) -> MetadataItemOutput:
return MetadataItemOutput(item=MetadataItemField(label=self.label, value=self.value))
@invocation_output("metadata_output")
class MetadataOutput(BaseInvocationOutput):
metadata: MetadataField = OutputField(description="Metadata Dict")
@invocation("metadata", title="Metadata", tags=["metadata"], category="metadata", version="1.0.0")
class MetadataInvocation(BaseInvocation):
"""Takes a MetadataItem or collection of MetadataItems and outputs a MetadataDict."""
items: Union[list[MetadataItemField], MetadataItemField] = InputField(
description=FieldDescriptions.metadata_item_polymorphic
)
def invoke(self, context: InvocationContext) -> MetadataOutput:
if isinstance(self.items, MetadataItemField):
# single metadata item
data = {self.items.label: self.items.value}
else:
# collection of metadata items
data = {item.label: item.value for item in self.items}
# add app version
data.update({"app_version": __version__})
return MetadataOutput(metadata=MetadataField.model_validate(data))
@invocation("merge_metadata", title="Metadata Merge", tags=["metadata"], category="metadata", version="1.0.0")
class MergeMetadataInvocation(BaseInvocation):
"""Merged a collection of MetadataDict into a single MetadataDict."""
collection: list[MetadataField] = InputField(description=FieldDescriptions.metadata_collection)
def invoke(self, context: InvocationContext) -> MetadataOutput:
data = {}
for item in self.collection:
data.update(item.model_dump())
return MetadataOutput(metadata=MetadataField.model_validate(data))
GENERATION_MODES = Literal[
"txt2img", "img2img", "inpaint", "outpaint", "sdxl_txt2img", "sdxl_img2img", "sdxl_inpaint", "sdxl_outpaint"
]
@invocation("core_metadata", title="Core Metadata", tags=["metadata"], category="metadata", version="1.0.0")
class CoreMetadataInvocation(BaseInvocation):
"""Collects core generation metadata into a MetadataField"""
generation_mode: Optional[GENERATION_MODES] = InputField(
default=None,
description="The generation mode that output this image", description="The generation mode that output this image",
) )
positive_prompt: Optional[str] = InputField(default=None, description="The positive prompt parameter") created_by: Optional[str] = Field(description="The name of the creator of the image")
negative_prompt: Optional[str] = InputField(default=None, description="The negative prompt parameter") positive_prompt: str = Field(description="The positive prompt parameter")
width: Optional[int] = InputField(default=None, description="The width parameter") negative_prompt: str = Field(description="The negative prompt parameter")
height: Optional[int] = InputField(default=None, description="The height parameter") width: int = Field(description="The width parameter")
seed: Optional[int] = InputField(default=None, description="The seed used for noise generation") height: int = Field(description="The height parameter")
rand_device: Optional[str] = InputField(default=None, description="The device used for random number generation") seed: int = Field(description="The seed used for noise generation")
cfg_scale: Optional[float] = InputField(default=None, description="The classifier-free guidance scale parameter") rand_device: str = Field(description="The device used for random number generation")
steps: Optional[int] = InputField(default=None, description="The number of steps used for inference") cfg_scale: float = Field(description="The classifier-free guidance scale parameter")
scheduler: Optional[str] = InputField(default=None, description="The scheduler used for inference") steps: int = Field(description="The number of steps used for inference")
seamless_x: Optional[bool] = InputField(default=None, description="Whether seamless tiling was used on the X axis") scheduler: str = Field(description="The scheduler used for inference")
seamless_y: Optional[bool] = InputField(default=None, description="Whether seamless tiling was used on the Y axis") clip_skip: int = Field(
clip_skip: Optional[int] = InputField(
default=None,
description="The number of skipped CLIP layers", description="The number of skipped CLIP layers",
) )
model: Optional[MainModelField] = InputField(default=None, description="The main model used for inference") model: MainModelField = Field(description="The main model used for inference")
controlnets: Optional[list[ControlField]] = InputField( controlnets: list[ControlField] = Field(description="The ControlNets used for inference")
default=None, description="The ControlNets used for inference" loras: list[LoRAMetadataField] = Field(description="The LoRAs used for inference")
vae: Optional[VAEModelField] = Field(
default=None,
description="The VAE used for decoding, if the main model's default was not used",
) )
ipAdapters: Optional[list[IPAdapterMetadataField]] = InputField(
default=None, description="The IP Adapters used for inference" # Latents-to-Latents
strength: Optional[float] = Field(
default=None,
description="The strength used for latents-to-latents",
) )
t2iAdapters: Optional[list[T2IAdapterField]] = InputField( init_image: Optional[str] = Field(default=None, description="The name of the initial image")
default=None, description="The IP Adapters used for inference"
# SDXL
positive_style_prompt: Optional[str] = Field(default=None, description="The positive style prompt parameter")
negative_style_prompt: Optional[str] = Field(default=None, description="The negative style prompt parameter")
# SDXL Refiner
refiner_model: Optional[MainModelField] = Field(default=None, description="The SDXL Refiner model used")
refiner_cfg_scale: Optional[float] = Field(
default=None,
description="The classifier-free guidance scale parameter used for the refiner",
) )
loras: Optional[list[LoRAMetadataField]] = InputField(default=None, description="The LoRAs used for inference") refiner_steps: Optional[int] = Field(default=None, description="The number of steps used for the refiner")
refiner_scheduler: Optional[str] = Field(default=None, description="The scheduler used for the refiner")
refiner_positive_aesthetic_score: Optional[float] = Field(
default=None, description="The aesthetic score used for the refiner"
)
refiner_negative_aesthetic_score: Optional[float] = Field(
default=None, description="The aesthetic score used for the refiner"
)
refiner_start: Optional[float] = Field(default=None, description="The start value used for refiner denoising")
class ImageMetadata(BaseModelExcludeNull):
"""An image's generation metadata"""
metadata: Optional[dict] = Field(
default=None,
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")
@invocation_output("metadata_accumulator_output")
class MetadataAccumulatorOutput(BaseInvocationOutput):
"""The output of the MetadataAccumulator node"""
metadata: CoreMetadata = OutputField(description="The core metadata for the image")
@invocation(
"metadata_accumulator", title="Metadata Accumulator", tags=["metadata"], category="metadata", version="1.0.0"
)
class MetadataAccumulatorInvocation(BaseInvocation):
"""Outputs a Core Metadata Object"""
generation_mode: str = InputField(
description="The generation mode that output this image",
)
positive_prompt: str = InputField(description="The positive prompt parameter")
negative_prompt: str = InputField(description="The negative prompt parameter")
width: int = InputField(description="The width parameter")
height: int = InputField(description="The height parameter")
seed: int = InputField(description="The seed used for noise generation")
rand_device: str = InputField(description="The device used for random number generation")
cfg_scale: float = InputField(description="The classifier-free guidance scale parameter")
steps: int = InputField(description="The number of steps used for inference")
scheduler: str = InputField(description="The scheduler used for inference")
clip_skip: int = InputField(
description="The number of skipped CLIP layers",
)
model: MainModelField = InputField(description="The main model used for inference")
controlnets: list[ControlField] = InputField(description="The ControlNets used for inference")
loras: list[LoRAMetadataField] = InputField(description="The LoRAs used for inference")
strength: Optional[float] = InputField( strength: Optional[float] = InputField(
default=None, default=None,
description="The strength used for latents-to-latents", description="The strength used for latents-to-latents",
@ -159,21 +135,6 @@ class CoreMetadataInvocation(BaseInvocation):
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",
) )
# High resolution fix metadata.
hrf_enabled: Optional[float] = InputField(
default=None,
description="Whether or not high resolution fix was enabled.",
)
# TODO: should this be stricter or do we just let the UI handle it?
hrf_method: Optional[str] = InputField(
default=None,
description="The high resolution fix upscale method.",
)
hrf_strength: Optional[float] = InputField(
default=None,
description="The high resolution fix img2img strength used in the upscale pass.",
)
# SDXL # SDXL
positive_style_prompt: Optional[str] = InputField( positive_style_prompt: Optional[str] = InputField(
default=None, default=None,
@ -214,13 +175,7 @@ class CoreMetadataInvocation(BaseInvocation):
description="The start value used for refiner denoising", description="The start value used for refiner denoising",
) )
def invoke(self, context: InvocationContext) -> MetadataOutput: def invoke(self, context: InvocationContext) -> MetadataAccumulatorOutput:
"""Collects and outputs a CoreMetadata object""" """Collects and outputs a CoreMetadata object"""
return MetadataOutput( return MetadataAccumulatorOutput(metadata=CoreMetadata(**self.dict()))
metadata=MetadataField.model_validate(
self.model_dump(exclude_none=True, exclude={"id", "type", "is_intermediate", "use_cache"})
)
)
model_config = ConfigDict(extra="allow")

View File

@ -1,15 +1,13 @@
import copy import copy
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, Field
from invokeai.app.shared.fields import FieldDescriptions
from invokeai.app.shared.models import FreeUConfig
from ...backend.model_management import BaseModelType, ModelType, SubModelType from ...backend.model_management import BaseModelType, ModelType, SubModelType
from .baseinvocation import ( from .baseinvocation import (
BaseInvocation, BaseInvocation,
BaseInvocationOutput, BaseInvocationOutput,
FieldDescriptions,
Input, Input,
InputField, InputField,
InvocationContext, InvocationContext,
@ -26,8 +24,6 @@ class ModelInfo(BaseModel):
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")
model_config = ConfigDict(protected_namespaces=())
class LoraInfo(ModelInfo): class LoraInfo(ModelInfo):
weight: float = Field(description="Lora's weight which to use when apply to model") weight: float = Field(description="Lora's weight which to use when apply to model")
@ -38,7 +34,6 @@ class UNetField(BaseModel):
scheduler: ModelInfo = Field(description="Info to load scheduler submodel") scheduler: ModelInfo = Field(description="Info to load scheduler submodel")
loras: List[LoraInfo] = Field(description="Loras to apply on model loading") loras: List[LoraInfo] = Field(description="Loras to apply on model loading")
seamless_axes: List[str] = Field(default_factory=list, description='Axes("x" and "y") to which apply seamless') seamless_axes: List[str] = Field(default_factory=list, description='Axes("x" and "y") to which apply seamless')
freeu_config: Optional[FreeUConfig] = Field(default=None, description="FreeU configuration")
class ClipField(BaseModel): class ClipField(BaseModel):
@ -54,32 +49,13 @@ class VaeField(BaseModel):
seamless_axes: List[str] = Field(default_factory=list, description='Axes("x" and "y") to which apply seamless') seamless_axes: List[str] = Field(default_factory=list, description='Axes("x" and "y") to which apply seamless')
@invocation_output("unet_output")
class UNetOutput(BaseInvocationOutput):
"""Base class for invocations that output a UNet field"""
unet: UNetField = OutputField(description=FieldDescriptions.unet, title="UNet")
@invocation_output("vae_output")
class VAEOutput(BaseInvocationOutput):
"""Base class for invocations that output a VAE field"""
vae: VaeField = OutputField(description=FieldDescriptions.vae, title="VAE")
@invocation_output("clip_output")
class CLIPOutput(BaseInvocationOutput):
"""Base class for invocations that output a CLIP field"""
clip: ClipField = OutputField(description=FieldDescriptions.clip, title="CLIP")
@invocation_output("model_loader_output") @invocation_output("model_loader_output")
class ModelLoaderOutput(UNetOutput, CLIPOutput, VAEOutput): class ModelLoaderOutput(BaseInvocationOutput):
"""Model loader output""" """Model loader output"""
pass unet: UNetField = OutputField(description=FieldDescriptions.unet, title="UNet")
clip: ClipField = OutputField(description=FieldDescriptions.clip, title="CLIP")
vae: VaeField = OutputField(description=FieldDescriptions.vae, title="VAE")
class MainModelField(BaseModel): class MainModelField(BaseModel):
@ -89,8 +65,6 @@ class MainModelField(BaseModel):
base_model: BaseModelType = Field(description="Base model") base_model: BaseModelType = Field(description="Base model")
model_type: ModelType = Field(description="Model Type") model_type: ModelType = Field(description="Model Type")
model_config = ConfigDict(protected_namespaces=())
class LoRAModelField(BaseModel): class LoRAModelField(BaseModel):
"""LoRA model field""" """LoRA model field"""
@ -98,16 +72,8 @@ class LoRAModelField(BaseModel):
model_name: str = Field(description="Name of the LoRA model") model_name: str = Field(description="Name of the LoRA model")
base_model: BaseModelType = Field(description="Base model") base_model: BaseModelType = Field(description="Base model")
model_config = ConfigDict(protected_namespaces=())
@invocation("main_model_loader", title="Main Model", tags=["model"], category="model", version="1.0.0")
@invocation(
"main_model_loader",
title="Main Model",
tags=["model"],
category="model",
version="1.0.0",
)
class MainModelLoaderInvocation(BaseInvocation): class MainModelLoaderInvocation(BaseInvocation):
"""Loads a main model, outputting its submodels.""" """Loads a main model, outputting its submodels."""
@ -214,16 +180,10 @@ class LoraLoaderInvocation(BaseInvocation):
lora: LoRAModelField = InputField(description=FieldDescriptions.lora_model, input=Input.Direct, title="LoRA") lora: LoRAModelField = InputField(description=FieldDescriptions.lora_model, input=Input.Direct, title="LoRA")
weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight) weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
unet: Optional[UNetField] = InputField( unet: Optional[UNetField] = InputField(
default=None, default=None, description=FieldDescriptions.unet, input=Input.Connection, title="UNet"
description=FieldDescriptions.unet,
input=Input.Connection,
title="UNet",
) )
clip: Optional[ClipField] = InputField( clip: Optional[ClipField] = InputField(
default=None, default=None, description=FieldDescriptions.clip, input=Input.Connection, title="CLIP"
description=FieldDescriptions.clip,
input=Input.Connection,
title="CLIP",
) )
def invoke(self, context: InvocationContext) -> LoraLoaderOutput: def invoke(self, context: InvocationContext) -> LoraLoaderOutput:
@ -284,35 +244,20 @@ class SDXLLoraLoaderOutput(BaseInvocationOutput):
clip2: Optional[ClipField] = OutputField(default=None, description=FieldDescriptions.clip, title="CLIP 2") clip2: Optional[ClipField] = OutputField(default=None, description=FieldDescriptions.clip, title="CLIP 2")
@invocation( @invocation("sdxl_lora_loader", title="SDXL LoRA", tags=["lora", "model"], category="model", version="1.0.0")
"sdxl_lora_loader",
title="SDXL LoRA",
tags=["lora", "model"],
category="model",
version="1.0.0",
)
class SDXLLoraLoaderInvocation(BaseInvocation): class SDXLLoraLoaderInvocation(BaseInvocation):
"""Apply selected lora to unet and text_encoder.""" """Apply selected lora to unet and text_encoder."""
lora: LoRAModelField = InputField(description=FieldDescriptions.lora_model, input=Input.Direct, title="LoRA") lora: LoRAModelField = InputField(description=FieldDescriptions.lora_model, input=Input.Direct, title="LoRA")
weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight) weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
unet: Optional[UNetField] = InputField( unet: Optional[UNetField] = InputField(
default=None, default=None, description=FieldDescriptions.unet, input=Input.Connection, title="UNet"
description=FieldDescriptions.unet,
input=Input.Connection,
title="UNet",
) )
clip: Optional[ClipField] = InputField( clip: Optional[ClipField] = InputField(
default=None, default=None, description=FieldDescriptions.clip, input=Input.Connection, title="CLIP 1"
description=FieldDescriptions.clip,
input=Input.Connection,
title="CLIP 1",
) )
clip2: Optional[ClipField] = InputField( clip2: Optional[ClipField] = InputField(
default=None, default=None, description=FieldDescriptions.clip, input=Input.Connection, title="CLIP 2"
description=FieldDescriptions.clip,
input=Input.Connection,
title="CLIP 2",
) )
def invoke(self, context: InvocationContext) -> SDXLLoraLoaderOutput: def invoke(self, context: InvocationContext) -> SDXLLoraLoaderOutput:
@ -385,7 +330,12 @@ class VAEModelField(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_config = ConfigDict(protected_namespaces=())
@invocation_output("vae_loader_output")
class VaeLoaderOutput(BaseInvocationOutput):
"""VAE output"""
vae: VaeField = OutputField(description=FieldDescriptions.vae, title="VAE")
@invocation("vae_loader", title="VAE", tags=["vae", "model"], category="model", version="1.0.0") @invocation("vae_loader", title="VAE", tags=["vae", "model"], category="model", version="1.0.0")
@ -393,13 +343,10 @@ class VaeLoaderInvocation(BaseInvocation):
"""Loads a VAE model, outputting a VaeLoaderOutput""" """Loads a VAE model, outputting a VaeLoaderOutput"""
vae_model: VAEModelField = InputField( vae_model: VAEModelField = InputField(
description=FieldDescriptions.vae_model, description=FieldDescriptions.vae_model, input=Input.Direct, ui_type=UIType.VaeModel, title="VAE"
input=Input.Direct,
ui_type=UIType.VaeModel,
title="VAE",
) )
def invoke(self, context: InvocationContext) -> VAEOutput: def invoke(self, context: InvocationContext) -> VaeLoaderOutput:
base_model = self.vae_model.base_model base_model = self.vae_model.base_model
model_name = self.vae_model.model_name model_name = self.vae_model.model_name
model_type = ModelType.Vae model_type = ModelType.Vae
@ -410,7 +357,7 @@ class VaeLoaderInvocation(BaseInvocation):
model_type=model_type, model_type=model_type,
): ):
raise Exception(f"Unkown vae name: {model_name}!") raise Exception(f"Unkown vae name: {model_name}!")
return VAEOutput( return VaeLoaderOutput(
vae=VaeField( vae=VaeField(
vae=ModelInfo( vae=ModelInfo(
model_name=model_name, model_name=model_name,
@ -425,31 +372,19 @@ class VaeLoaderInvocation(BaseInvocation):
class SeamlessModeOutput(BaseInvocationOutput): class SeamlessModeOutput(BaseInvocationOutput):
"""Modified Seamless Model output""" """Modified Seamless Model output"""
unet: Optional[UNetField] = OutputField(default=None, description=FieldDescriptions.unet, title="UNet") unet: Optional[UNetField] = OutputField(description=FieldDescriptions.unet, title="UNet")
vae: Optional[VaeField] = OutputField(default=None, description=FieldDescriptions.vae, title="VAE") vae: Optional[VaeField] = OutputField(description=FieldDescriptions.vae, title="VAE")
@invocation( @invocation("seamless", title="Seamless", tags=["seamless", "model"], category="model", version="1.0.0")
"seamless",
title="Seamless",
tags=["seamless", "model"],
category="model",
version="1.0.0",
)
class SeamlessModeInvocation(BaseInvocation): class SeamlessModeInvocation(BaseInvocation):
"""Applies the seamless transformation to the Model UNet and VAE.""" """Applies the seamless transformation to the Model UNet and VAE."""
unet: Optional[UNetField] = InputField( unet: Optional[UNetField] = InputField(
default=None, default=None, description=FieldDescriptions.unet, input=Input.Connection, title="UNet"
description=FieldDescriptions.unet,
input=Input.Connection,
title="UNet",
) )
vae: Optional[VaeField] = InputField( vae: Optional[VaeField] = InputField(
default=None, default=None, description=FieldDescriptions.vae_model, input=Input.Connection, title="VAE"
description=FieldDescriptions.vae_model,
input=Input.Connection,
title="VAE",
) )
seamless_y: bool = InputField(default=True, input=Input.Any, description="Specify whether Y axis is seamless") seamless_y: bool = InputField(default=True, input=Input.Any, description="Specify whether Y axis is seamless")
seamless_x: bool = InputField(default=True, input=Input.Any, description="Specify whether X axis is seamless") seamless_x: bool = InputField(default=True, input=Input.Any, description="Specify whether X axis is seamless")
@ -472,24 +407,3 @@ class SeamlessModeInvocation(BaseInvocation):
vae.seamless_axes = seamless_axes_list vae.seamless_axes = seamless_axes_list
return SeamlessModeOutput(unet=unet, vae=vae) return SeamlessModeOutput(unet=unet, vae=vae)
@invocation("freeu", title="FreeU", tags=["freeu"], category="unet", version="1.0.0")
class FreeUInvocation(BaseInvocation):
"""
Applies FreeU to the UNet. Suggested values (b1/b2/s1/s2):
SD1.5: 1.2/1.4/0.9/0.2,
SD2: 1.1/1.2/0.9/0.2,
SDXL: 1.1/1.2/0.6/0.4,
"""
unet: UNetField = InputField(description=FieldDescriptions.unet, input=Input.Connection, title="UNet")
b1: float = InputField(default=1.2, ge=-1, le=3, description=FieldDescriptions.freeu_b1)
b2: float = InputField(default=1.4, ge=-1, le=3, description=FieldDescriptions.freeu_b2)
s1: float = InputField(default=0.9, ge=-1, le=3, description=FieldDescriptions.freeu_s1)
s2: float = InputField(default=0.2, ge=-1, le=3, description=FieldDescriptions.freeu_s2)
def invoke(self, context: InvocationContext) -> UNetOutput:
self.unet.freeu_config = FreeUConfig(s1=self.s1, s2=self.s2, b1=self.b1, b2=self.b2)
return UNetOutput(unet=self.unet)

View File

@ -2,16 +2,16 @@
import torch import torch
from pydantic import field_validator from pydantic import validator
from invokeai.app.invocations.latent import LatentsField from invokeai.app.invocations.latent import LatentsField
from invokeai.app.shared.fields import FieldDescriptions
from invokeai.app.util.misc import SEED_MAX, get_random_seed from invokeai.app.util.misc import SEED_MAX, get_random_seed
from ...backend.util.devices import choose_torch_device, torch_dtype from ...backend.util.devices import choose_torch_device, torch_dtype
from .baseinvocation import ( from .baseinvocation import (
BaseInvocation, BaseInvocation,
BaseInvocationOutput, BaseInvocationOutput,
FieldDescriptions,
InputField, InputField,
InvocationContext, InvocationContext,
OutputField, OutputField,
@ -65,7 +65,7 @@ Nodes
class NoiseOutput(BaseInvocationOutput): class NoiseOutput(BaseInvocationOutput):
"""Invocation noise output""" """Invocation noise output"""
noise: LatentsField = OutputField(description=FieldDescriptions.noise) noise: LatentsField = OutputField(default=None, description=FieldDescriptions.noise)
width: int = OutputField(description=FieldDescriptions.width) width: int = OutputField(description=FieldDescriptions.width)
height: int = OutputField(description=FieldDescriptions.height) height: int = OutputField(description=FieldDescriptions.height)
@ -78,13 +78,7 @@ def build_noise_output(latents_name: str, latents: torch.Tensor, seed: int):
) )
@invocation( @invocation("noise", title="Noise", tags=["latents", "noise"], category="latents", version="1.0.0")
"noise",
title="Noise",
tags=["latents", "noise"],
category="latents",
version="1.0.0",
)
class NoiseInvocation(BaseInvocation): class NoiseInvocation(BaseInvocation):
"""Generates latent noise.""" """Generates latent noise."""
@ -111,7 +105,7 @@ class NoiseInvocation(BaseInvocation):
description="Use CPU for noise generation (for reproducible results across platforms)", description="Use CPU for noise generation (for reproducible results across platforms)",
) )
@field_validator("seed", mode="before") @validator("seed", pre=True)
def modulo_seed(cls, v): def modulo_seed(cls, v):
"""Returns the seed modulo (SEED_MAX + 1) to ensure it is within the valid range.""" """Returns the seed modulo (SEED_MAX + 1) to ensure it is within the valid range."""
return v % (SEED_MAX + 1) return v % (SEED_MAX + 1)

View File

@ -4,34 +4,33 @@ import inspect
import re import re
# from contextlib import ExitStack # from contextlib import ExitStack
from typing import List, Literal, Union from typing import List, Literal, Optional, Union
import numpy as np import numpy as np
import torch import torch
from diffusers.image_processor import VaeImageProcessor from diffusers.image_processor import VaeImageProcessor
from pydantic import BaseModel, ConfigDict, Field, field_validator from pydantic import BaseModel, Field, validator
from tqdm import tqdm from tqdm import tqdm
from invokeai.app.invocations.metadata import CoreMetadata
from invokeai.app.invocations.primitives import ConditioningField, ConditioningOutput, ImageField, ImageOutput from invokeai.app.invocations.primitives import ConditioningField, ConditioningOutput, ImageField, ImageOutput
from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
from invokeai.app.shared.fields import FieldDescriptions
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 import BaseModelType, ModelType, SubModelType from invokeai.backend import BaseModelType, ModelType, SubModelType
from ...backend.model_management import ONNXModelPatcher from ...backend.model_management import ONNXModelPatcher
from ...backend.stable_diffusion import PipelineIntermediateState from ...backend.stable_diffusion import PipelineIntermediateState
from ...backend.util import choose_torch_device from ...backend.util import choose_torch_device
from ..models.image import ImageCategory, ResourceOrigin
from .baseinvocation import ( from .baseinvocation import (
BaseInvocation, BaseInvocation,
BaseInvocationOutput, BaseInvocationOutput,
FieldDescriptions,
Input, Input,
InputField, InputField,
InvocationContext, InvocationContext,
OutputField, OutputField,
UIComponent, UIComponent,
UIType, UIType,
WithMetadata,
WithWorkflow,
invocation, invocation,
invocation_output, invocation_output,
) )
@ -54,7 +53,7 @@ ORT_TO_NP_TYPE = {
"tensor(double)": np.float64, "tensor(double)": np.float64,
} }
PRECISION_VALUES = Literal[tuple(ORT_TO_NP_TYPE.keys())] PRECISION_VALUES = Literal[tuple(list(ORT_TO_NP_TYPE.keys()))]
@invocation("prompt_onnx", title="ONNX Prompt (Raw)", tags=["prompt", "onnx"], category="conditioning", version="1.0.0") @invocation("prompt_onnx", title="ONNX Prompt (Raw)", tags=["prompt", "onnx"], category="conditioning", version="1.0.0")
@ -64,17 +63,14 @@ class ONNXPromptInvocation(BaseInvocation):
def invoke(self, context: InvocationContext) -> ConditioningOutput: def invoke(self, context: InvocationContext) -> ConditioningOutput:
tokenizer_info = context.services.model_manager.get_model( tokenizer_info = context.services.model_manager.get_model(
**self.clip.tokenizer.model_dump(), **self.clip.tokenizer.dict(),
) )
text_encoder_info = context.services.model_manager.get_model( text_encoder_info = context.services.model_manager.get_model(
**self.clip.text_encoder.model_dump(), **self.clip.text_encoder.dict(),
) )
with tokenizer_info as orig_tokenizer, text_encoder_info as text_encoder: # , ExitStack() as stack: with tokenizer_info as orig_tokenizer, text_encoder_info as text_encoder: # , ExitStack() as stack:
loras = [ loras = [
( (context.services.model_manager.get_model(**lora.dict(exclude={"weight"})).context.model, lora.weight)
context.services.model_manager.get_model(**lora.model_dump(exclude={"weight"})).context.model,
lora.weight,
)
for lora in self.clip.loras for lora in self.clip.loras
] ]
@ -99,10 +95,9 @@ class ONNXPromptInvocation(BaseInvocation):
print(f'Warn: trigger: "{trigger}" not found') print(f'Warn: trigger: "{trigger}" not found')
if loras or ti_list: if loras or ti_list:
text_encoder.release_session() text_encoder.release_session()
with ( with ONNXModelPatcher.apply_lora_text_encoder(text_encoder, loras), ONNXModelPatcher.apply_ti(
ONNXModelPatcher.apply_lora_text_encoder(text_encoder, loras), orig_tokenizer, text_encoder, ti_list
ONNXModelPatcher.apply_ti(orig_tokenizer, text_encoder, ti_list) as (tokenizer, ti_manager), ) as (tokenizer, ti_manager):
):
text_encoder.create_session() text_encoder.create_session()
# copy from # copy from
@ -170,6 +165,7 @@ class ONNXTextToLatentsInvocation(BaseInvocation):
default=7.5, default=7.5,
ge=1, ge=1,
description=FieldDescriptions.cfg_scale, description=FieldDescriptions.cfg_scale,
ui_type=UIType.Float,
) )
scheduler: SAMPLER_NAME_VALUES = InputField( scheduler: SAMPLER_NAME_VALUES = InputField(
default="euler", description=FieldDescriptions.scheduler, input=Input.Direct, ui_type=UIType.Scheduler default="euler", description=FieldDescriptions.scheduler, input=Input.Direct, ui_type=UIType.Scheduler
@ -179,14 +175,15 @@ class ONNXTextToLatentsInvocation(BaseInvocation):
description=FieldDescriptions.unet, description=FieldDescriptions.unet,
input=Input.Connection, input=Input.Connection,
) )
control: Union[ControlField, list[ControlField]] = InputField( control: Optional[Union[ControlField, list[ControlField]]] = InputField(
default=None, default=None,
description=FieldDescriptions.control, description=FieldDescriptions.control,
ui_type=UIType.Control,
) )
# seamless: bool = InputField(default=False, description="Whether or not to generate an image that can tile without seams", ) # seamless: bool = InputField(default=False, description="Whether or not to generate an image that can tile without seams", )
# seamless_axes: str = InputField(default="", description="The axes to tile the image on, 'x' and/or 'y'") # seamless_axes: str = InputField(default="", description="The axes to tile the image on, 'x' and/or 'y'")
@field_validator("cfg_scale") @validator("cfg_scale")
def ge_one(cls, v): def ge_one(cls, v):
"""validate that all cfg_scale values are >= 1""" """validate that all cfg_scale values are >= 1"""
if isinstance(v, list): if isinstance(v, list):
@ -245,28 +242,25 @@ class ONNXTextToLatentsInvocation(BaseInvocation):
stable_diffusion_step_callback( stable_diffusion_step_callback(
context=context, context=context,
intermediate_state=intermediate_state, intermediate_state=intermediate_state,
node=self.model_dump(), node=self.dict(),
source_node_id=source_node_id, source_node_id=source_node_id,
) )
scheduler.set_timesteps(self.steps) scheduler.set_timesteps(self.steps)
latents = latents * np.float64(scheduler.init_noise_sigma) latents = latents * np.float64(scheduler.init_noise_sigma)
extra_step_kwargs = {} extra_step_kwargs = dict()
if "eta" in set(inspect.signature(scheduler.step).parameters.keys()): if "eta" in set(inspect.signature(scheduler.step).parameters.keys()):
extra_step_kwargs.update( extra_step_kwargs.update(
eta=0.0, eta=0.0,
) )
unet_info = context.services.model_manager.get_model(**self.unet.unet.model_dump()) unet_info = context.services.model_manager.get_model(**self.unet.unet.dict())
with unet_info as unet: # , ExitStack() as stack: with unet_info as unet: # , ExitStack() as stack:
# loras = [(stack.enter_context(context.services.model_manager.get_model(**lora.dict(exclude={"weight"}))), lora.weight) for lora in self.unet.loras] # loras = [(stack.enter_context(context.services.model_manager.get_model(**lora.dict(exclude={"weight"}))), lora.weight) for lora in self.unet.loras]
loras = [ loras = [
( (context.services.model_manager.get_model(**lora.dict(exclude={"weight"})).context.model, lora.weight)
context.services.model_manager.get_model(**lora.model_dump(exclude={"weight"})).context.model,
lora.weight,
)
for lora in self.unet.loras for lora in self.unet.loras
] ]
@ -328,7 +322,7 @@ class ONNXTextToLatentsInvocation(BaseInvocation):
category="image", category="image",
version="1.0.0", version="1.0.0",
) )
class ONNXLatentsToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow): class ONNXLatentsToImageInvocation(BaseInvocation):
"""Generates an image from latents.""" """Generates an image from latents."""
latents: LatentsField = InputField( latents: LatentsField = InputField(
@ -339,6 +333,11 @@ class ONNXLatentsToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow):
description=FieldDescriptions.vae, description=FieldDescriptions.vae,
input=Input.Connection, input=Input.Connection,
) )
metadata: Optional[CoreMetadata] = InputField(
default=None,
description=FieldDescriptions.core_metadata,
ui_hidden=True,
)
# tiled: bool = InputField(default=False, description="Decode latents by overlaping tiles(less memory consumption)") # tiled: bool = InputField(default=False, description="Decode latents by overlaping tiles(less memory consumption)")
def invoke(self, context: InvocationContext) -> ImageOutput: def invoke(self, context: InvocationContext) -> ImageOutput:
@ -348,7 +347,7 @@ class ONNXLatentsToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow):
raise Exception(f"Expected vae_decoder, found: {self.vae.vae.model_type}") raise Exception(f"Expected vae_decoder, found: {self.vae.vae.model_type}")
vae_info = context.services.model_manager.get_model( vae_info = context.services.model_manager.get_model(
**self.vae.vae.model_dump(), **self.vae.vae.dict(),
) )
# clear memory as vae decode can request a lot # clear memory as vae decode can request a lot
@ -377,7 +376,7 @@ class ONNXLatentsToImageInvocation(BaseInvocation, WithMetadata, WithWorkflow):
node_id=self.id, node_id=self.id,
session_id=context.graph_execution_state_id, session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate, is_intermediate=self.is_intermediate,
metadata=self.metadata, metadata=self.metadata.dict() if self.metadata else None,
workflow=self.workflow, workflow=self.workflow,
) )
@ -405,8 +404,6 @@ class OnnxModelField(BaseModel):
base_model: BaseModelType = Field(description="Base model") base_model: BaseModelType = Field(description="Base model")
model_type: ModelType = Field(description="Model Type") model_type: ModelType = Field(description="Model Type")
model_config = ConfigDict(protected_namespaces=())
@invocation("onnx_model_loader", title="ONNX Main Model", tags=["onnx", "model"], category="model", version="1.0.0") @invocation("onnx_model_loader", title="ONNX Main Model", tags=["onnx", "model"], category="model", version="1.0.0")
class OnnxModelLoaderInvocation(BaseInvocation): class OnnxModelLoaderInvocation(BaseInvocation):

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